GASとMessaging APIで自分専用辞書を作る

# はじめに
語彙力を増やすために、自分用の辞書を作ろうとしたが、せっかくなのでGASとLINEのMessaging API、どこからでも辞書に登録できるようにする。
# 前提
javascriptの基本的な文法を理解していること。
# 内容
## 完成物のイメージ
LINEで単語と説明を送信すると、それをスプレッドシートの最終行に登録してくれる。単語のみを入れると、単語の説明を返してくれる。
説明に改行を入れたいので、単語と説明は、改行二回で区切る。以下のような感じ。

```
単語

説明だよ。
これだと単語=説明みたいになってややこしいね。
```

また、スプレッドシ－トには、1列目に単語、2列目に説明を記録する。  
単語を探す関数、単語を追加する関数、メッセージを送信する関数、LINEから受け取った情報を処理して、単語を探すのか、追加するのかを判別する関数を作ればよいだろう。関数をどの程度細かく分ければよいかが、まだあまり分かっていないので、是非コメントで教えてほしい。
## 作業
### 前準備
はじめに、[Google ドライブ](https://www.google.com/intl/ja_jp/drive/)を開いて、**新規**から**Google スプレッドシート**を作成する。拡張機能から**Apps Script**を選んで、GASのエディタを開く。プログラムはここに書いていく。  
次に、Messaging APIの準備を行う。[ここ](https://developers.line.biz/ja/services/messaging-api/)に行って、**今すぐ始めよう**と書かれたボタンを押す。流れに沿って必要事項の入力をしていき、チャンネルを作成する。
### グローバルで行うこと
いくつかの関数でスプレッドシートのデータを扱うので、グローバルで取得しておく。

```js
//gasと関連付いたスプレッドシートを取得する。
let spreadSheet=SpreadsheetApp.getActiveSpreadsheet();

//スプレッドシートの開いているシートを取得する。
let sheet=spreadSheet.getActiveSheet();

//シートで、データが入っているセルの範囲を取得する。
let range=sheet.getDataRange();

//範囲のセルの情報を２次配列にして取得。
let values=range.getValues();
```

### 単語を探す関数
最初から順番に値を見ていき、単語と合うものがあればその場所を返す。無ければ-1を返す。

```js
function searchWord(word){
  for(let i=0;i<values.length;i++){
    if(values[i][0]==word)
      return i;
  }
  return -1;
}
```

### 単語を追加する関数
単語は、既に存在する場合は上書き、しない場合は最終行に追加することにする。
最終行を取得するには、getLastRowを使えばよいそうだが([参考](https://blog.8basetech.com/google-apps-script/gas-lastrow-append/))、今回は先にvaluesを取得しているので、その長さを使って最終行を取得する。

```js
function addDictionary(word,description){
  let wordIndex=searchWord(word);
  if(wordIndex==-1){
    let lastRow=values.length+1;
    sheet.getRange(lastRow,1).setValue(word);
    sheet.getRange(lastRow,2).setValue(description);
  }else{
    sheet.getRange(wordIndex+1,2).setValue(description);
  }
}
```

getRangeでは、セルの位置が(1,1)から始まっていることに注意する。
### メッセージを送信する関数
Messaging APIを使って、ユーザーにメッセージを送信する。

```js
function sendMessage(messageText,userId){
  let url="https://api.line.me/v2/bot/message/push";
  let token="チャンネルのアクセストークン"

  let message={
    "type":"text",
    "text":messageText
  };

  let params={
    "headers":{
      "Content-Type":"application/json",
      "Authorization":"Bearer "+token
    },
    "method":"post",
    "payload":JSON.stringify({
      "to":userId,
      "messages":[message]
    })
  };

  UrlFetchApp.fetch(url,params);
}
```

チャンネルのトークンは、作ったチャンネルの**Messaging API設定**の**チャネルアクセストークン**と書かれたところから発行する。表示された文字列がトークンとなるので、それをコピペして貼り付けよう。
### メッセージを受け取った時の処理
プログラムを書く前に、いくつかやることがある。  
はじめに、**Messaging API設定**から**応答メッセージ**を編集する。
応答モードをBOTに、応答メッセージをオフに、Webhookをオンにする。応答モードをオンにすると、BOTへメッセージを送った時に、自動で返信してしまう。WebhookはLINEでメッセージ等を受け取った時、それをGASへ送信するのに必要となる。  
次に、LINEから送信された情報を受け取るために、プロジェクトを公開したい。そのためには、**デプロイ**から新しいデプロイを作成する。デプロイタイプはウェブアプリに、アクセスできるユーザーを全員にして(LINEもアクセスできるようにするため)、デプロイする。そして、ウェブアプリのURLをコピーしておく。  
次に、**Messaging API設定**の**Webhook 設定**で、さっきコピーしたURLを貼り付け、Webhookの利用をオンにする。  
ここからやっとプログラムを書いていく。

```js
function doPost(e){
  let userId="自分のユーザーid";
  let contents=JSON.parse(e.postData.contents);
  let text=contents.events[0].message.text;

  //送ってきたのが自分でなければ、終了する。
  if(userId!=contents.events[0].source.userId)
    return;
  
  //改行二回で分割してる。もし、この配列の長さが1ならば検索、2ならば登録とわかる。
  let textArr=text.split("\n\n");
  let sendText;
  if(textArr.length==1){
    let index=searchWord(textArr[0]);
    if(index==-1){
      sendText="単語が見つかりませんでした。"
    }else{
      sendText=sheet.getRange(index+1,2).getValue();
    }
  }else{
    addDictionary(textArr[0],textArr[1]);
    sendText="追加しました。"
  }
  sendMessage(sendText,userId);
}
```

doPostは、LINEからメッセージを受け取った時に勝手に実行される。  
メッセージを送ってきた相手が自分の時のみ、動かしたいので、ユーザーidを照合している。  
自身のユーザーidは**チャネル基本設定**の**あなたのユーザーID**から見られる([参考](https://www.line-community.me/ja/question/5fc8910b4016904e521e5085/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BCid%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95))。
### バグの修正
プログラムを保存した後、**デプロイ->デプロイの管理->編集**からバージョンを新しくし、デプロイすることで、更新することができる。
実際にアカウントを友達登録し、辞書の登録を行ってみると、スプレッドシートが少しおかしなことになっている。
![2021-11-22 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906164/a8723e5b-19df-f3db-d0e7-2c8758450064.png)

このように、最初の行が空いてしまっているのである。
原因は、スプレッドシートにデータの入ったセルが全くないときも、valuesが[[""]]になってしまうからだ。
動作はするので特に問題はないが、少し気持ち悪いので、修正する。

```js
function addDictionary(word,description){
  let wordIndex=searchWord(word);
  if(wordIndex==-1){
    let lastRow;

    //最初の要素の長さが1,つまりvaluesが[[""]]のとき
    if(values[0].length==1){
      lastRow=1;
    }else{
      lastRow=values.length+1;
    }
    sheet.getRange(lastRow,1).setValue(word);
    sheet.getRange(lastRow,2).setValue(description);
  }else{
    sheet.getRange(wordIndex+1,2).setValue(description);
  }
}
```

これで、無事にバグは治った。
# さいごに
最終的に、このような感じになった。  
LINEでの表示
![2021-11-22 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906164/272f6671-3196-e592-80a1-8e22edba5dab.png)  
スプレッドシートでの表示
![2021-11-22 (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906164/18730228-39e3-2760-5aeb-c874fa56efaf.png)  

すべてのコードを乗せておく。早く動かしたい人は、これをコピペして使えばいい。

```js
"use strict"

let spreadSheet=SpreadsheetApp.getActiveSpreadsheet(),
sheet=spreadSheet.getActiveSheet(),
range=sheet.getDataRange(),
values=range.getValues();

function searchWord(word){
  for(let i=0;i<values.length;i++){
    if(values[i][0]==word)
      return i;
  }
  return -1;
}

function addDictionary(word,description){
  let wordIndex=searchWord(word);
  if(wordIndex==-1){
    let lastRow;

    //最初の要素の長さが1,つまりvaluesが[[""]]のとき
    if(values[0].length==1){
      lastRow=1;
    }else{
      lastRow=values.length+1;
    }
    sheet.getRange(lastRow,1).setValue(word);
    sheet.getRange(lastRow,2).setValue(description);
  }else{
    sheet.getRange(wordIndex+1,2).setValue(description);
  }
}

function sendMessage(messageText,userId){
  let url="https://api.line.me/v2/bot/message/push";
  let token="チャンネルのアクセストークン"
  let message={
    "type":"text",
    "text":messageText
  };
  let params={
    "headers":{
      "Content-Type":"application/json",
      "Authorization":"Bearer "+token
    },
    "method":"post",
    "payload":JSON.stringify({
      "to":userId,
      "messages":[message]
    })
  };
  UrlFetchApp.fetch(url,params);
}

function doPost(e){
  let userId="自分のユーザーid";
  let contents=JSON.parse(e.postData.contents);
  let text=contents.events[0].message.text;

  //送ってきたのが自分でなければ、終了する。
  if(userId!=contents.events[0].source.userId)
    return;
  
  //改行二回で分割してる。もし、この配列の長さが1ならば検索、2ならば登録とわかる。
  let textArr=text.split("\n\n");
  let sendText;
  if(textArr.length==1){
    let index=searchWord(textArr[0]);
    if(index==-1){
      sendText="単語が見つかりませんでした。"
    }else{
      sendText=sheet.getRange(index+1,2).getValue();
    }
  }else{
    addDictionary(textArr[0],textArr[1]);
    sendText="追加しました。"
  }
  sendMessage(sendText,userId);
}
```

最後まで読んでくれてありがとう。
分かりにくかったところや、改善できるところなどは、是非コメントで教えてほしい。