Slackへ勤怠連絡を入力するAlexaスキルを作ってみた

# 概要
弊社は出勤や休憩などの勤怠連絡をSlackのチャンネルへのメッセージ送信で行っています
毎日何度も行うことなので手入力は結構面倒です
そこで、最近我が家にやってきたAmazon Echo Showに話しかけて勤怠連絡を行えるAlexaスキルを作りました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/9ec76584-08da-ed4f-8139-d3a2a473f531.png)

ちなみに、Echo Showが無くてもスマホのAlexaアプリでAlexaスキルを実行することはできます
音声入力で色々なものを操作するのは魔法感があって楽しいのでAlexaスキル開発はおすすめです！

# システム構成
![勤怠入力スキル構成図 (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/0b8214ed-9921-5472-d822-715873c51e29.jpeg)

# 手順
手順は大きく分けて以下の4つです

1. Alexaスキルセットアップ
2. Slack連携
3. Lambda関数セットアップ
4. 配布

## 1. Alexaスキルセットアップ
まずは、ユーザーからの入力を受ける部分を作成します

### 1.1 スキル作成
[Alexaデベロッパーコンソール](https://developer.amazon.com/ja-JP/alexa)にアクセスしてAmazonアカウントでログインします

その後、スキルの作成ボタンを押下して以下の設定でスキルを作成します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/eca44c29-4a29-b690-db3e-65b9e4d885ae.png)
バックエンドリソースは「ユーザー定義のプロビジョニング」にします
「Alexa-hosted」にするとわざわざLambdaで関数を作成する必要がなくて楽ですが、テスト機能が不十分なのとデプロイが遅いので今回は使用しません

今回は一から作成するのでテンプレートは使用せず、「スクラッチで作成」を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/7e35f518-36ae-5bce-b69d-48067b3fbe50.png)

### 1.2 スキルの呼び出し方法について
Alexaスキルの呼び出し方法は様々ありますが、今回は以下のような発話を使って勤怠連絡を入力しようと思います

「<font color="SkyBlue">アレクサ</font>、<font color="Orange">勤怠入力</font>で<font color="YellowGreen">出勤</font>して」

この発話を3つの部分に分けて解説します

- <font color="SkyBlue">アレクサ</font>
    - ウェイクワード
    - Alexaデバイスがユーザーの発話を聞き始めるトリガーとなる文言です
    - Alexaデバイス毎に設定することができます。「アレクサ」がデフォルトですが、その他にも「Amazon」、「Computer」などがあります
- <font color="Orange">勤怠入力</font>
    - 呼び出し名
    - この文言でAlexaはスキルを判別します
- <font color="YellowGreen">出勤</font>
    - スロット
    - 同じ属性の文言を列挙型の様に定義できます
    - 定義したスロットの文言が発話に含まれていると、それがスキルリクエストに含まれてLambdaに渡されます

### 1.3 呼び出し名設定
スキルのセットアップ画面に入り、左のメニュー一覧から「呼び出し名」を開きます。
この画面で呼び出し名を設定することができます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/7d3864aa-cfe0-7cc9-7646-77cc0050aacb.png)

呼び出し名は既存スキルと似たものにしてしまうと、うまく認識されません
また、2語以上を組み合わせた単語である必要があります([公式リファレンス](https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/choose-the-invocation-name-for-a-custom-skill.html))

今回は「勤怠連絡」を呼び出し名とします

### 1.4 スロット作成
「アセット」→「スロット」を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/87064dd5-4bde-0969-da4b-ce21188a8f91.png)

「+スロットタイプ」ボタンを押下してスロットを追加します
スロット名は「KintaiType」にします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/06bc3052-12be-969e-3865-42551abb0b34.png)

必要の種類は「出勤」、「退勤」、「休憩開始」、「休憩終了」の4つです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/5a84b459-b5c5-5184-2f32-02e2ba267828.png)

### 1.5 インテント作成
「対話モデル」→「インテント」を開きます。
Alexaスキルにおけるインテントとは、ユーザーの発話によって引き起こされるアクションのことです。([公式リファレンス](https://developer.amazon.com/ja-JP/docs/alexa/ask-overviews/alexa-skills-kit-glossary.html#i))
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/404907f1-788d-ba65-8d82-87e889d57362.png)

インテント名が「Amazon」から始まるものは標準ビルトインインテントです
「HelloWorldIntent」はサンプルなので削除します

それでは、「出勤して」などのコマンドを受け付けるためのインテントを作成するために「インテントを追加」ボタンを押下します
インテント名は「KintaiIntent」にしました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/867f3a4b-114d-6fce-bd6f-ec54a4e9c30b.png)

インテントには以下2種類のサンプル発話を設定しました
サンプル発話はなるべく多く登録した方が認識されやすいそうです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/eca6e474-4590-063e-f338-5e3c4e0e527a.png)
このとき、「{kintaiType}」と「して」の間に半角スペースが入っていないと以降のビルドでエラーになるので気を付けましょう

以上でインテントの設定は完了です

そうしたらページ上部の「モデルをビルド」ボタンを押します
これでこのスキルが設定した発話を認識できるようになります

### 1.6 発話テスト
ユーザーの発話がちゃんと認識されるかテストしてみましょう
テスト方法は以下の2種類があります

1. Echo端末やスマホからAlexaに話しかける
2. デベロッパーコンソールのテスト機能を使う

今回は2の方法でテストします。
PCのみで完結しますし、認識された発話から生成されたスキルリクエストも確認できるのでおすすめです

Alexa Developer Console上部の「テスト」タブを開きます
そして画面左上のテキストボックスへ発話を入力するか、マイクボタンを長押ししながらPCのマイクへ発話します
すると、それによって生成されたjson形式のスキルリクエストを確認することができます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/5414a121-3953-4ade-ae43-c3d373253440.png)

以上でAlexaスキルセットアップは一旦完了です！

## 2. Slack連携
今回作成するアプリはSlackへのメッセージ送信を行うため、この作業が必要です

### 2.1 Slackアプリ作成
[こちら](https://api.slack.com/apps)のページの「Create New App」ボタンから新しいアプリを作成します
App Nameは「勤怠連絡」、
Development Slack Workspaceは勤怠連絡を送信したいワークスペースを設定します

作成ができたら編集ページに遷移します

### 2.2 権限設定
アプリからSlackを操作するには、使用するAPIに必要な権限を追加する必要があります。

今回使用するAPIは[chat.postMessage](https://api.slack.com/methods/chat.postMessage)です
今回はユーザーとしてこのAPIを使用するので、「ユーザースコープ」の「chat:write」権限が必要です

アプリ編集ページのOAuth & Permissionsタブを開き、Scopes → User Token Scopes欄から以下の様に追加すればOKです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/bbb58f19-7bba-0123-c9c6-7bcbe115da5f.png)

### 2.3 ワークスペースへのインストール
アプリを使用するためにはワークスペースへのインストールが必要です
OAuth & Permissionsタブの上部の緑のボタンからインストールをします

### 2.4 Alexaスキルとのアカウントリンク設定
Alexaスキルを使用するユーザーが使用しているSlackアカウントとしてメッセージを送信するためには、Alexaスキルのアカウントリンク機能を使用します

まずは、Slackアプリの編集ページ → Basic Information → App Credentials欄から「Client ID」と「Client Secret」をコピーしておきます

次に、Alexa Developer Console → ツールタブ → アカウントリンクタブを開き、以下のように設定します
Web認証画面のURI: https://slack.com/oauth/authorize
アクセストークンのURI: https://slack.com/api/oauth.access
ユーザーのクライアントID: 先程コピーしたClient ID
クライアントのシークレット: 先程コピーしたClient Secret
スコープ: chat:write:user
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/f90dffd6-d3d5-d92c-d5ec-d6b5ed07a45d.png)
Alexaのリダイレクト先のURL欄は次の手順で使用するので3つともコピーしておきます

先程コピーしたリダイレクト先のURLを以下の欄に全て登録します
Slackアプリの編集ページ → OAuth & Permissions → Redirect URLs
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/4c712027-22d2-76ba-7b0f-74ca122fad16.png)

これで、Alexaアプリを使うためにはSlackアカウントとの連携が必要になりました

### 2.5 Slack連携テスト
実際にSlackとの連携ができるかテストしてみます

スマホのAlexaアプリ下部のその他タブ → スキル・ゲームページを開きます
そして、有効なスキルタブ → 開発タブ → 勤怠入力スキルへと進みます
勤怠入力スキルページが開けたら上部の「設定」ボタンを押下してアカウントのリンクを行ってみます
以下の様に表示されればOKです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/1758cadf-7f0b-1c3e-fc16-0b8264f8dd56.png)

## 3. Lambda関数セットアップ
それではいよいよ、Alexaからのスキルリクエストを受け取って処理をするLambda関数を作ります
AWSアカウントは作成済みの前提です

### 3.1 関数作成
[Serverless Application Repository](https://ap-northeast-1.console.aws.amazon.com/serverlessrepo/home)のページを開き、
「利用可能なアプリケーション」→「alexa-skills-kit-nodejs-factskill」を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/cfe84aee-1525-e2ff-3880-49e480ccab5f.png)
これはアレクサスキル作成用のテンプレートのようなものです

アプリケーション名を「KintaiRenraku」とし、「デプロイ」ボタンを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/b684b548-2f8c-a07c-75e3-03b82440d66d.png)
デプロイには数十秒かかりました

デプロイが終了するとLambdaのアプリケーション一覧ページに遷移します
遷移しなければ[こちら](https://ap-northeast-1.console.aws.amazon.com/lambda/home)から遷移できます
アプリケーション一覧には先程作成したアプリケーションが「serverlessrepo-KintaiRenraku」という名前で存在しているので名前をクリックします。

すると、このアプリケーション内に「alexaskillskitnodejsfactskill」というLabda関数が作成されていることが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/13f83a82-782e-d14c-a470-d821fae420dc.png)
関数名をクリックして編集画面に入りましょう

以上でLambda関数の作成ができました！

### 3.2 Lambda関数とAlexaスキルを接続
この関数がAlexaから呼ばれるためには以下の2つの作業が必要です

- Labda関数のトリガーにAlexaスキルIDを設定
- AlexaスキルのエンドポイントにLambda関数のARNを設定

まずは、「Labda関数のトリガーにAlexaスキルIDを設定」から行います
関数編集画面上部の「Alexa Skill Kit」トリガーをクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/5b1b0e93-a7eb-a50b-672c-ccbc0be8ef1c.png)

すると、トリガー一覧画面に入ります
既に追加済みのAlexa Skill Kitトリガーの説明欄に書いてあるとおり、追加済みのこのトリガーは削除し下さい
そして、「トリガーを追加」ボタンから改めてAlexa Skill Kitを追加します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/b792fd67-bb34-8c18-5022-f2792dffd657.png)

Alexa Skill Kitトリガーを追加しようとするとこの様な画面でスキルIDを求められます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/314cf40b-8fa4-d9f1-4b1a-a40b0878fe1e.png)

スキルIDは、手順1.1で開いたAlexa Developer Consoleから確認できます
作成したAlexaスキルの編集画面 → エンドポイントタブを開くとスキルIDが書いてあるのでコピーして先程のトリガー追加画面に入力します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/9dac0c23-5827-52a4-460c-f66ea29f123a.png)

続いて、「AlexaスキルのエンドポイントにLambda関数のARNを設定」を行います
Lambda関数の編集画面上部のボタンからARNをコピーします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/b81596cf-3207-3a9f-a7e3-cf11403580bc.png)

先程開いた、Alexaスキルの編集画面 → エンドポイントタブの「デフォルトの地域」欄にコピーしたARNを入力します
そしてページ上部の「エンドポイントを保存」ボタンを押下すればOKです

これでAlexaからLambda関数が呼び出されるようになりました！

試しにテストしてみましょう
「勤怠連絡を開いて」と話しかけると豆知識を披露してくれました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/1cd80a0b-294c-0107-f10c-9db9e8b7a659.png)
今回使用したテンプレートは豆知識を披露してくれるスキルなので、これでAlexaスキルとLambda関数の接続が確認できました

### 3.3 パッケージ追加
SlackAPIを叩くには色々な方法がありますが、今回は簡単にコードを書くために、`@slack/web-api`パッケージを使用します

以下の手順はローカル環境にnpmがインストールされていることが前提です

npmパッケージをインストールするためにはLambda関数の実行環境を一旦、ローカルに落としてくる必要があります
作成したLambda関数の編集ページ上部の「アクション」プルダウン → 関数をエクスポート → デプロイパッケージをエクスポートをします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/da1676eb-e50d-5821-1a4d-26adde19ed9f.png)

するとzipファイルがDLされるので、任意のフォルダに解凍します
次に、コマンドプロンプトを開いて解凍したフォルダに移動します
そして`npm install @slack/web-api`コマンドを実行します

解凍したフォルダ → 「node_modules」フォルダ内に@slack\web-apiというディレクトリができていれば成功です

アップロードするために、解凍したフォルダ内にあるファイルを全て選択してzip圧縮します

Lambda関数の編集ページを開き、コードソース → 「アップロード元」プルダウン → .zipファイルを選択し、先程作成したzipファイルをアップロードします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/05a50aa4-7738-e160-c8a8-d2c0d07cdb34.png)

アップロード後にコードソース欄の「Enviroment」タブがこの様になっていれば@slack/web-apiパッケージのインストールは完了です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/1f2d44c9-8a83-58f1-a164-fc0ff1c1c5db.png)

### 3.4 コーディング
今回は以下の様なコードを作成しました。
channelId_Kintai変数には勤怠連絡先のチャンネルIDを指定して下さい
詳細説明は割愛します

``` index.js
'use strict';
const Alexa = require('ask-sdk-core');
const { WebClient } = require('@slack/web-api');

// 喋る内容
const HELP_MESSAGE = "Slackの勤怠管理チャンネルで打刻します。";
const HELP_REPROMPT = 'どうしますか？';
const STOP_MESSAGE = "お疲れさまです";

// 打刻用の情報
const channelId_Kintai = "hoge";
const kanjiConversion = {
  "しゅっきん": "しゅっきん",
  "出勤": "しゅっきん",
  "きゅうけいかいし": "きゅうけいかいし",
  "休憩開始": "きゅうけいかいし",
  "きゅうけいしゅうりょう": "きゅうけいしゅうりょう",
  "休憩終了": "きゅうけいしゅうりょう",
  "たいきん": "たいきん",
  "退勤": "たいきん",
};
const commandInfos = {
  "しゅっきん": {
    "command": "in",
    "msg": "今日もいちにちがんばるぞい",
  },
  "きゅうけいかいし": {
    "command": "bi",
    "msg": "ゆっくりやすんでくださいね",
  },
  "きゅうけいしゅうりょう": {
    "command": "bo",
    "msg": "残りもがんばりましょう",
  },
  "たいきん": {
    "command": "out",
    "msg": "今日も一日お疲れさまでした",
  },
};

const KintaiHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return request.type === 'IntentRequest' && request.intent.name === 'KintaiIntent';
  },
   async handle(handlerInput) {
    // 勤怠チャンネルに打刻メッセージを送る
    const intent = handlerInput.requestEnvelope.request.intent;
    var commandInfo = commandInfos[kanjiConversion[intent.slots.kintaiType.value]];
    if(!commandInfo) {
      return handlerInput.responseBuilder
        .speak(intent.slots.kintaiType.value + "は存在しないコマンドです。使えるコマンドは「出勤」、「休憩開始」、「休憩終了」、「退勤」の4つです")
        .getResponse();
    }
    
    const client = new WebClient(handlerInput.requestEnvelope.context.System.user.accessToken)
    
    var params = {
      channel: channelId_Kintai,
      as_user: true,
      text: commandInfo["command"],
    };
    
    var msg;
    try {
      console.log("りくえすと(chat.postMessage): " + JSON.stringify(params));
      const response = await client.chat.postMessage(params);
      console.log("れすぽんす(chat.postMessage): " + JSON.stringify(response));
      
      msg = response.ok
        ? "打刻しました。" + commandInfo["msg"]
        : "打刻に失敗しました";
    } catch(e) {
      console.log("エラー: " + e);
      msg = "打刻に失敗しました";
    } finally {
      return handlerInput.responseBuilder
      .speak(msg)
      .getResponse();
    }
  },
};

const HelpHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return request.type === 'LaunchRequest'
      || (request.type === 'IntentRequest' && request.intent.name === 'AMAZON.HelpIntent');
  },
  handle(handlerInput) {
    let msg = "Slackの勤怠管理チャンネルで打刻します。使えるコマンドは「出勤」、「休憩開始」、「休憩終了」、「退勤」の4つです。「Alexa、勤怠で出勤して」と言ってみて下さい。"; 
    
    return handlerInput.responseBuilder
      .speak(msg)
      .reprompt("コマンドを発言して下さい")
      .getResponse();
  },
};

const ExitHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return request.type === 'IntentRequest'
      && (request.intent.name === 'AMAZON.CancelIntent'
        || request.intent.name === 'AMAZON.StopIntent');
  },
  handle(handlerInput) {
    return handlerInput.responseBuilder
      .speak("お疲れさまです")
      .getResponse();
  },
};

const SessionEndedRequestHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return request.type === 'SessionEndedRequest';
  },
  handle(handlerInput) {
    console.log(`セッションが以下の理由で終了しました: ${handlerInput.requestEnvelope.request.reason}`);
    return handlerInput.responseBuilder.getResponse();
  },
};

const skillBuilder = Alexa.SkillBuilders.custom();

exports.handler = skillBuilder
  .addRequestHandlers(
    KintaiHandler,
    HelpHandler,
    ExitHandler,
    SessionEndedRequestHandler,
  )
  .withCustomUserAgent('sample/basic-fact/v2')
  .lambda();
```

### 3.5 関数のテスト
リクエストが正しく処理されるかテストしてみましょう
Alexaに話しかけるか、Alexa Developer Consoleの「テスト」タブから発話をしてみます
無事、以下の様な返答が行われました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/2c4e2457-3163-05b2-42d2-a9283c0a7f4f.png)
また、Slackへのメッセージ送信も行われています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/e5cce5db-b32c-ef3e-0951-c7176e61abab.png)

以上でAlexaスキルの作成は完了です！

## 4. 配布
折角なので、作成したスキルを同僚の皆さんに使ってもらいましょう

Alexa Developer Consoleの「公開」タブ → 「公開範囲」タブを開きます
ベータテスト機能で配布するためには以下の作業を行う必要があるので全て済ませておきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413932/051c753d-1d12-4624-7bef-34824caaffa2.png)

必要な作業を全て済ませてから改めて「公開範囲」タブの「ベータテスト」欄を開くと、テスターのEメールアドレスを追加できるようになっています
ここに、アプリを配布したい方のメアドを追加すればその方へメールが飛び、今回作成したスキルを配布することができます

# 所感
便利なスキルを作れて楽しかったです。自分で毎日使っています
作り方をまとめるのが意外に大変で驚きました。書き始め当初は、コードの説明も細かく書く予定でしたが後半は疲れたので止めました
Alexaスキルの作り方の記事は古いものが多く、現在の開発環境の参考になる記事が見つけにくかった為、この様な記事を書きました
この記事を読んでAlexaスキルを作られた方も作り方をまとめて見られては如何でしょうか？

# 参考にさせて頂いたページ
- [Amazon EchoからSlackに通知を出してみた(Web API版)](https://nemuzuka.vss.jp.net/entry/2018/01/09/123510)