GASとSlackを使った社内問い合わせ対応フォーム

####[情シスSlack(corp-engr)#2AdventCalendar](https://adventar.org/calendars/4173) 2019の15日目の記事です。

##先にアウトプット
だいたいこんな感じになります

![aie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528736/66cc4734-3614-f1a0-8f5c-febfeff4d796.gif)

作成されたチャンネルには、フォーム入力者とフォーム項目に合わせた担当者が追加されますので、チャンネル上でコミュニケーションを取り問題解決、解決したらチャンネルをアーカイブすることによりクローズ、という運用をしています。

※上記gifでは名前を隠すため自分のSlack上での氏名を「情シス Slack」という名前に変更してテストしているので、「問い合わせ_情シス」となっています。情シスの部分には問い合わせ者の名前が入ります。


##自己紹介
都内不動産系のIT企業で
情シス？(キッティングヘルプデスクシステム導入スプレッドシートの先生電話コピー機オフィス移転他色々対応)
をしています。


###全体の仕組み
1.Googleフォーム
2.Googleスプレッドシート
3.GoogleAppsScript
4.Slack
上記4つを連携させています。
　Googleフォームに項目が入力されるとAppsScriptが起動し、
　Slackの部屋を立ち上げてそこに関係メンバーを招待します。
　同時にスプレッドシートにはフォームに入力した情報が転記されます。

####GASの大まかな仕組み
1.フォームからデータを受け取る
2.データをもとに
　Slackのチャンネルの名前、
　入れるメンバー
　を決定する
3.チャンネルを作る
4.メンバーを招待する
5.フォームに入れた項目をチャットする

###前準備
前提:同一メールアドレスで登録されたGsuiteとSlackがそれぞれ存在する

1.[コンテナバインドスクリプト](https://tonari-it.com/gas-script-editor/#toc7)でスプレッドシート、AppScript，フォームをそれぞれ作成
2.[SlackAPIトークンの取得](https://qiita.com/ykhirao/items/0d6b9f4a0cc626884dbb)
3.[Googleフォームの質問項目設定](https://ferret-plus.com/6385#p8)及び[「メールアドレス取得」の設定]
(imuza.com/entry/2018/05/19/120338)
4.[Moment.js]()の導入(Slackに生成するチャンネルの名前に日付を入力するのに使用)


####テスト用に作成するシートの項目
![screencapture-docs-google-forms-d-1pvDfWb1sep-knLgk3nMbczAv1t74AjwISg1XY0GmMVs-edit-2019-12-15-04_38_06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528736/b2501272-c755-26f6-1f71-2b5bb0681232.jpeg)



今回はテストとして上記のようなフォームを使用します。
このスクリプトでは項目の名前でフォームの値をScriptに拾わせています、
なのでフォーム上の項目の名前とコードの項目を合わせるように気をつけてください。
フォーム上の項目の順番変更は問題有りません


####実際のコード
※実際に使用しているコードはもっと長いですが、必要最小限の処理のみ抜粋して書いています。
　注釈に、実際に僕の運用ではどんな処理を追加しているかは記述しています。




```inquiry.gs
function makeroom_slack(e){
  var category=e.namedValues["問い合わせの種類"][0];
  var roomName=make_channel_name(category,e)
  var member=selectMember(category,e)
  var channelData=makeChannel(roomName,member,e)
  makeMessage(category,channelData,e)
}
```
これは全体の流れを纏めたメソッドです。
※ここは僕の運用では全体を例外処理
>try{}catch(e){}

で囲ってエラーが出たら特定のチャンネルに通知する機能を追加しています。

```make_channel_name.gs
function make_channel_name(category,e){
  var date   =Moment.moment().format('YYYYMMDD');
  var rand   =Math.random().toString(36).slice(-3);

  const userName=getNameForEmail(e.namedValues["メールアドレス"][0]);
  switch (category) {
    case "IT備品に関する問い合わせ(購入申請など)":
      return "_is-備品_"+userName+"-"+date+"_"+rand  
    case "アカウントに関する問い合わせ(作成申請など)":
      return "_is-アカウント_"+userName+"-"+date+"_"+rand
    case "その他問い合わせ":
      return "_is-問い合わせ"+userName+"-"+date+"_"+rand
  }
}
```
申し込みのカテゴリごとに異なる日付、申請者名(userName)などの情報を組み込んだチャンネル名を生成しています。
Googleフォームにログインしているメールアドレスを使用しSlackから該当ユーザーの情報を取得し、名前を取得しています。
(詳しくはslack.gsのgetNameForEmail()に書いています)



```selectMember.gs
function selectMember(category,e){
  switch (category) {
    case "IT備品に関する問い合わせ(購入申請など)":
      var userList="XXXX,CCCC,ZZZZ,"
    case "アカウントに関する問い合わせ(作成申請など)":
      var userList="AAAA,YYYY,ZZZZ,"
    case "その他問い合わせ":
      var userList="XXXX,YYYY,ZZZZ,"
    default:
      return findUserByEmails(userList)
  }
}
```
問い合わせカテゴリごとに異なる担当者を入力しています。
現状の僕の運用しているコードでは、ここはスプレッドシートに入力したメールアドレスを取得して担当者を変更できるようにしていますが、(法務やCSなど他部署にも展開しているため)最初はベタ書きで動かしちゃっていいと思います。


```makeChannel.gs
function makeChannel(roomName,member,e){
  var isPrivate=true
  return slack_setup(member,roomName,isPrivate)
}

```

```makeMessage.gs
function makeMessage(category,channelData,e){
　　slack_postMessage(channelData,toiawaseText(e))
  }
}

```
Slackに関する処理は最後のslack.gsにまとめています。

```maketext.gs
function toiawaseText(e){
  return "```<!here>\n■問い合わせの種類\n"+e.namedValues["問い合わせの種類"][0]+"\n■概要\n"+e.namedValues["概要"]+"\n■詳細\n"+e.namedValues["詳細"]+"```";
}
```
実運用では選択した項目によって違うメッセージを出力しているため、
ここで複数のメッセージを作成して、場合によって呼び出し分けています。
>e.namedValues["問い合わせ種類"][0]
>e.namedValues["概要"]

上記なぜ異なる書き方になっているかというと、上はラジオボタンの場合は値が配列に入っているため、下はテキストなのでそのまま取得できるためです。

```slack.gs
function slack_setup(roomMember,roomName,isPrivate){
  const channelData=slack_create(roomName,isPrivate);
  slack_invite(channelData,roomMember);
  return channelData;
}

function slack_create(roomName,isPrivate){
  const url  = "https://slack.com/api/conversations.create"
  var data = {
    "token":"XXXXXX",
    "name" :check_name_for_slack(roomName),
    "is_private": isPrivate
  }
  var json_data=UrlFetchApp.fetch(url, options("post",data));
  Logger.log(JSON.parse(json_data))
  return JSON.parse(json_data);
}

function slack_invite(channelData,roomMember){
  var notDuplicateMember = roomMember.filter(function (x, i, self) {
            return self.indexOf(x) === i;
        });
  const url  = "https://slack.com/api/conversations.invite"
  var data = {
    "token":"XXXXXX",
    "channel":channelData.channel.id,
    "users" :notDuplicateMember.join(",")+","
  }
  var json_data=UrlFetchApp.fetch(url, options("post",data));
  return JSON.parse(json_data);
}

function slack_postMessage(channelData,text){
  const url  = "https://slack.com/api/chat.postMessage"
  var data = {
    "token":"XXXXXX",
    "channel":channelData.channel.id,
    "text" :text
  }
  var json_data=UrlFetchApp.fetch(url, options("post",data));
  return JSON.parse(json_data);
}


function getNameForEmail(email) {
  var url="https://slack.com/api/users.list"  
  var data = {
    "token":"XXXXXX",
  }
  var json_data=UrlFetchApp.fetch(url, options("post",data));
  json_data=JSON.parse(json_data)
  
  for(i in json_data["members"]){
    if(String(json_data["members"][i]["profile"]["email"])===email){
      return check_name_for_slack(json_data["members"][i]["real_name"]).substring(0,10);
    }
  }
}


function findUserByEmails(emails){
  var userList=[]
  var url="https://slack.com/api/users.list"  
  var data = {
    "token":"XXXXXX",
  }
  var json_data=UrlFetchApp.fetch(url, options("post",data));
  json_data=JSON.parse(json_data)
  
  for (var i = 0, len = emails.length; i < len; i++) {
    for(j in json_data["members"]){
      if(String(json_data["members"][j]["profile"]["email"])==emails[i]){
        userList.push(String(json_data["members"][j]["id"]))
      }
    }
  }
  Logger.log(userList)
  return userList
}

function options(protocol,data){
  return {"method": protocol,"payload": data,};
}
```

>"token":"XXXXXX",

の部分は[PropertiesService](https://qiita.com/unsoluble_sugar/items/ec5c935c4bfc2e06b246)を使って書き換えるのがおすすめです。
また、
>check_name_for_slack(name)

を使って、Slackのチャンネル名称に使うと弾かれる文字(英語大文字、記号、空白など)をカットしています。

###おわりに1.GAS最高という話
環境構築とデプロイがいらなく爆速でテスト、実行ができるGASは、細かいイレギュラーなタスクが大量にありまとまった時間が取れない情シスの強い味方です。
下書きの段階ではここでGASの良さを延々説明していたんですが、
本筋と逸れるのでそれは別の機会に書くことにします。
ちょっとだけ簡単に説明すると、
デプロイがいらないので修正が容易
フォームで集計すれば、その後のスクリプトの処理が失敗してもフォームへの入力結果は保存されるためデータ消失という最悪の結果にはつながらない
gitなどのようなエンジニアのバックボーンの無いメンバーには難しい共有方法を使わずとも共有できる
など色々あります。

###おわりに2.Slack時代に対応したヘルプデスク体制を構成したい
今回このようなヘルプデスクの仕組みを作ったのは、非情シスから現職に転職してきて飛び交うDMを取り敢えず稟議を出さない範囲で治めようとサクッと作ってみたのが始まりです。
そこから業務が一旦落ち着いて、属人化させず機能強化できないかと、Zendesk,ServiceNowあたりのヘルプデスクソリューションを実現するSaaSを調べてみたのですが、どれも僕が調べた範囲では問い合わせの受付チャネルはメールかサイトに備え付けられたチャットが中心で(APIからインテグレーションゴリゴリ組めば実現できるかもですが)
Slackベースに社内問い合わせのスムーズな解決を実現するものではありませんでした。

ちょうど[Notion](https://note.com/yriica/n/nc079db469d47)みたいな1アプリで色々完結させるサービスが次の流行りなのかなという風潮がみえてきたこの頃、
エンドユーザーをチャットアプリ外に飛ばすことなく問題解決するのが、一つコーポレートエンジニア(ここに来てカッコつけた名前を使います)の解決すべき問題だと思うので、引き続き方法を考えていきたい＆なにかあったら教えてほしいです！