slackに画像を投稿したらテキストに変換して返す

# google Driveで、画像からドキュメント作成

## 概要
技術書を電子書籍で購入した場合、固定型レイアウトになっていることが多く、コードをコピペしようにもそのままではコピーできません。

webサービスなどでpdfファイルを丸ごとテキストに変換してくれるサービスもありますが、スクリーンショットをちょいとテキストに変換するようなサービスは意外とありませんでした。

そこで、slackにスクリーンショットを投稿したらテキストに変換して返してくれるBotのようなプログラムを作りました

※ slackは2020年5月よりこれひとつでオールオッケーなトークン（legacy token）が作成できなくなっており、使い勝手がものすごく悪くなっています。
また、今回は画像をテキスト変換する時にGASを使っていますが、GASのようにレスポンスが遅いプログラムだとslackは複数回通知してしまって、レスポンスが何通も来てしまいます。

このような不便不具合があるので、Botを作るならLINE APIのほうがおすすめかもしれません。（機会があれば作ります）

## 画像を保存
画像のテキスト変換（OCR）はgoogleが非常に優秀です。
まずは画像のテキスト変換を手動で確認しましょう。

適当な文章をスクリーンショットで撮って、googleドライブに保存します。
そして「アプリで開く」→「Googleドキュメント」で開いてみましょう。

そうすると、自動的に画像の文字がテキストに変換されました（すごーい）
![IMAGE](quiver-image-url/2BBD76F0F55C174A26192DD34CAE93F6.jpg =661x144)

## GASで記述
次に、この動作をGASで記述します。

画像ファイルのBlobデータ元にドキュメントファイルを作成し、
できたドキュメントファイルからテキストの部分だけを抽出、
そして画像ファイルとドキュメントファイルを削除という流れです。

Drive APIを使いますので、「リソース」→「Googleの拡張サービス」からDriveをオンにしてください。
![IMAGE](quiver-image-url/ED70A6FED95160709D50EA052E0D4C4F.jpg =634x377)

画像ファイルを指定するには、名前でもIDでも良いのですが、今回はIDにします。（IDは「共有可能なリンクを取得」の中程の英数字がごちゃっと並んでいる部分です。）
![IMAGE](quiver-image-url/71F316618E0AB8A795C17B4226C1E0BE.jpg =656x258)

``````javascript
function getTextFromImage(){
  var file=DriveApp.getFileById("1LupHTxgZf_7pC6FskwdDUxsqMXVZTvVS")
  //作成するドキュメントの情報
    var resource={
      title: file.getName(), //ファイル名
      mineType: 'pdf'  //ファイルタイプ
    };
  var data=file.getBlob()
  Drive.Files.insert(resource, data, {ocr: true}); //これでドキュメントファイルが作成される
}
``````
実行したら、driveのトップページにドキュメントファイルが作成されます。

次に、このドキュメントファイルからテキストだけを抜き出しましょう。
ドキュメントファイル作成時にドキュメントファイルのIDを取得しておきます。
そして、ドキュメントファイルからgetText()で抜き出せます

``````JavaScript
function getTextFromImage(){
  var file=DriveApp.getFileById("1LupHTxgZf_7pC6FskwdDUxsqMXVZTvVS")
  //作成するドキュメントの情報
    var resource={
      title: file.getName(), //ファイル名
      mineType: 'pdf'  //ファイルタイプ
    };
  var data=file.getBlob()
- Drive.Files.insert(resource, data, {ocr: true});

+ var docFileId = Drive.Files.insert(resource, data, {ocr: true}).id;
+ var document=DocumentApp.openById(docFileId)
+ var text=document.getText()
+ console.log(text)
+ return(text)
}
``````

本番で使うにあたっては、テキストさえ抜き取ってしまえば、もとの画像やドキュメントファイルは要りません。

なので、textを抜き取った後は消去します。

``````javascript
function getTextFromImage(){
  var file=DriveApp.getFileById("1LupHTxgZf_7pC6FskwdDUxsqMXVZTvVS")
  //作成するドキュメントの情報
    var resource={
      title: file.getName(), //ファイル名
      mineType: 'pdf'  //ファイルタイプ
    };
  var data=file.getBlob()

  var docFileId = Drive.Files.insert(resource, data, {ocr: true}).id;
  var document=DocumentApp.openById(docFileId)
  var text=document.getText()
  console.log(text)

+ DriveApp.removeFile(file)
+ var DocFile=DriveApp.getFileById(document.getId())
+ DriveApp.removeFile(DocFile)
  return text
}
``````

これでGASでイメージからテキストを抽出する作業はいったん終わりです。

# slack側準備
slackは2020年5月にlegacy tokenが廃止になり、わかりにくく使いにくいOAuth & Permissionsを使って設定しなければならなくなりました。

やり方としては、
1. Appを作成
2. Appに権限を与える

まず、 https://api.slack.com/apps にアクセスして新しくアプリを作成します
次に、OAuth & PermissionsよりBot Token Scopesを設定します。
本当は許可を与えるものに絞るべきなのでしょうが、いろいろエラー出たので結局全部許可しました。

## GASからslackにメッセージを飛ばす
GASからslackに"Hello"のメッセージを送ってみます。
slackに送るには「Incoming Webhook」を使う方法もありますが、今回はgoogle用ライブラリでSlackAppという便利なライブラリを作ってくださった方がいるので、これをありがたく使わせてもらいます。

SlackAppのLibrary keyは「M3W5Ut3Q39AaIwLquryEPMwV62A3znfOO」です。
SlackAppを使うにあたっては、slackAccessTokenが必要で、これはOAuth & PermissionsのBot User OAuth Access Tokenのことです。
![IMAGE](quiver-image-url/65811DC612B4AA28CB6116B3D0F57EEC.jpg =777x457)

チャンネルIDはチャンネル名のリンクを取得して、URLの右端
![IMAGE](quiver-image-url/63982012A07141F10865ED92F37BBE96.jpg =228x300)
`https://w1561862973-zio274796.slack.com/archives/******** <-これ`

slackAppはpostMessage()メソッドで指定したslackチャンネルにメッセージを飛ばせます

実装例

``````javascript
const slackAccessToken="****************"

function sendHello(){
  var slackApp = SlackApp.create(slackAccessToken);
  
  slackApp.postMessage(
    "C018338C9TK", //チャンネル名 
    "Hello",  //メッセージ
  );
}
``````

これでGASからslackに投稿する準備はできました。

# slackからGASにメッセージを送る

## doPostの準備
slackのメッセージをGAS受け取るにはdoPostで処理します。

まずはslackからGASを認証させる必要があります。
半分形式的なものなので、以下手順で認証させてください。

1. GASにdoPost関数を準備

``````javascript
function doPost(e){
  var params = JSON.parse(e.postData.getDataAsString());
  // 初回の認証時のみ必要
  if(params.type === "url_verification"){
    return ContentService.createTextOutput(params.challenge);
  }
  return ContentService.createTextOutput('ok');
}
``````

2. 以下設定で公開する

- 「Execute the app as:」は「Me」
- 「Who has access to the app:」は 「Anyone, even anonymus」

ここで注意するのは、doPost内を更新したら、Project versionをNewで更新することです。
（versionを新しくしないと更新されません！！）

発行されたURLを控えておきましょう。

3. slack側 Event Subscriptionsで認証させる

slack apiのEvent Subscriptionsで、Request URLに先ほど控えたGASのweb app URLを貼り付けます。
![IMAGE](quiver-image-url/CAFF349ACA633196B62B7044C0B36F77.jpg =805x613)
これがverifyになれば、slackに投稿したらGASにwebhookとして飛んでくるようになります。

ついでにSubscribe to bot eventsに「file_shared」を追加しておきます。
![IMAGE](quiver-image-url/0D3E491EA749E69DE56A4C5E9470C121.jpg =801x700)

## slack Botの確認

連携が終わったのでslackからGASにメッセージを飛ばして、GASが受け取っていることを確認したいのですが、なぜかslackからdoPostを起動させた場合は、console.log()で出力しようにもApps Script ダッシュボードに表示されません。（非常に困る欠陥です）

GCPと連携させる方法で対処しましたが、使いにくいのでコンソールログをslackに返してやります。

``````javascript
const slackAccessToken="****************"

function doPost(e){
  var params=e
  var slackApp = SlackApp.create(slackAccessToken);
  slackApp.postMessage(
    "C018338C9TK", //チャンネル名 
    params,  //メッセージ
  );
}
``````
これでslackに投稿したら、パラメーターeが帰ってくるようになりました。
（ところが無限に返答が続くと思いますので、どこかで終了させてください）

## 無限Botをなくす
上記で無限Botしたのは、slackが貴方の投稿だけでなくBotの投稿にも反応してメッセージを返したからです。

そこで、doPostの中身をいじってBotのメッセージには反応させないようにします。

先ほどの無限リプライの中身を見ますと、user_idというのが含まれています。
投稿がBotのユーザーidの場合は、GASからslackにメッセージを送らないようにします

``````javascript
const slackAccessToken="****************"

function doPost(e){
  var params=e
  
+ if(e.parameter.user_id=="UKU2NUY49")return; //Botの投稿には反応しない
  
  var slackApp = SlackApp.create(slackAccessToken);
  slackApp.postMessage(
    "C018338C9TK", //チャンネル名 
    params,  //メッセージ
  );
}
``````

## slackから画像を取得する
最後に画像を送信したらgoogle driveに保存できるようにしましょう。

slackに投稿した画像は、画像のIDを含めたURLで取得することができます。

slackに画像を投稿して、パラメーターを読みますと、fileとidがあることがわかります。

``````
"{ token: 'o4xJfzvdUepIv545O8itroZ5',
  team_id: 'TL2H8LK2S',
  api_app_id: 'A018EA3K0JC',
  event: 
   { type: 'file_shared',
     channel_id: 'C018338C9TK',
     file_id: 'F018F1ETZJ4', //これ
     user_id: 'UKU2NUY49',
     file: { id: 'F018F1ETZJ4' },  
     event_ts: '1595751612.017000' },
  type: 'event_callback',
  event_id: 'Ev018F1EUW80',
  event_time: 1595751612,
  authed_users: [ 'U017QNUJ19R' ] }"
``````
まずは、このfileIDを直接指示して入手しましょう。

``````javascript
function getImage(){
  var fileId="F018F1ETZJ4"
  
  var url='https://slack.com/api/files.info?token='+slackAccessToken+'&file='+fileId
  var fileResponse = UrlFetchApp.fetch(url)
  var fileInfo = JSON.parse(fileResponse.getContentText());
  var dlUrl = fileInfo.file.url_private;
  
    // Slackからファイル取得
    var headers = {
      "Authorization" : "Bearer " + slackAccessToken
    };    
    var params2 = {
      "method":"GET",
      "headers":headers
    };
    var dlData = UrlFetchApp.fetch(dlUrl, params2).getBlob();
  
  // フォルダを指定
  var driveFile = FOLDER.createFile(dlData);
}
``````
画像を取得できましたか

## 最小限の完成

doPostに渡されるパラメーターは、e.postData.getDataAsString()で取得します。
投稿が画像だった場合は、paramsのevent.typeがfile_sharedとなっています。
なので、if文で画像が投稿されたらGASで画像をダウンロードするメソッド（getFileToGoogleDrive）に画像のfile_idを渡します。
（無限Botを防ぐための`if(e.parameter.user_id!="UKU2NUY49")return;`は不要です）

``````javascript
const slackAccessToken="****************"

function doPost(e) {
  var params = JSON.parse(e.postData.getDataAsString());
  console.log(params)
//  初回の認証時のみ必要
//  if(params.type === "url_verification"){
//    return ContentService.createTextOutput(params.challenge);
//  }
  
  var slackApp = SlackApp.create(slackAccessToken);
  
  //自分の投稿以外には反応しない（これをしとかないと、Botの返信にBotが反応して無限にメッセージが送られる）
  //  if(e.parameter.user_id!="UKU2NUY49")return;
  // ファイルをgoogle driveに移す
  if(params.event.type === "file_shared") {
    console.log("event")
    var file_id=params.event.file_id
    console.log(file_id)
    var text=getFileToGoogleDrive(file_id);
  }
  var message = text;
  
  slackApp.postMessage(
    channelId, 
    message,
  );
}

function getFileToGoogleDrive(file_id){
  var url='https://slack.com/api/files.info?token='+slackAccessToken+'&file='+file_id
  console.log(url)
  var fileResponse = UrlFetchApp.fetch(url)
  var fileInfo = JSON.parse(fileResponse.getContentText());
  var dlUrl = fileInfo.file.url_private;
  console.log(dlUrl)
  
    // Slackからファイル取得
    var headers = {
      "Authorization" : "Bearer " + slackAccessToken
    };    
    var params2 = {
      "method":"GET",
      "headers":headers
    };
    var dlData = UrlFetchApp.fetch(dlUrl, params2).getBlob();
  
  // Drive上にファイルを作成
  var imgFile=DriveApp.createFile(dlData)
  var text= getTextFromImage(imgFile)
  
  return text
}
    
function getTextFromImage(imgFile){
  //作成するドキュメントの情報
    var resource={
      title: imgFile.getName(), //ファイル名
      mineType: 'pdf'  //ファイルタイプ
    };
  var data=imgFile.getBlob()

  var docFileId = Drive.Files.insert(resource, data, {ocr: true}).id;
  var docFile=DocumentApp.openById(docFileId)
  var text=docFile.getText()
  console.log(text)

  DriveApp.removeFile(imgFile)
  DriveApp.removeFile(DriveApp.getFileById(docFile.getId()))
  return text
}

``````

# 参考
SlackAppライブラリ
> https://qiita.com/soundTricker/items/43267609a870fc9c7453

基本設定はこれを参考
> https://qiita.com/GMA/items/7e3e7cedcb880f2c1dc9