調べ物した結果

現役SEが仕事と直接関係ないことを調べた結果を。

SlackのAPI仕様が変わっててうまく動かなくなってたから直した channels.history->conversations.history

目次

以前
couraeg.hatenablog.com
こんな感じでSlack(channels.history メソッド)をGASでスプレッドシートに簡易の問題集を吐き出せるスクリプトを作ったんだけど(ずっと放置してたけど)
知らない間にAPIの仕様が変わっていたようで動かなくなっていた。

ということで動くようにした。

ざっと結果を先に。

・とりあえずうまくいく。
・channels.historyは非推奨だからconversations.historyを使おう。
・conversations.historyは親のメッセージしか読まないから、スレッドを掘りたかったらconversations.repliesも合わせて使おう。
ということでとりあえずいい感じになった。

発生している事象&トラブルシュート

ボタンをクリックして、スクリプトを実行。シートにSlackの問題を書きだすはずが・・・
gyazo.com
とまぁ。スクリプトは正常に動いてるけど全然吐き出されない。なんぞ。

おおよそ辺りはついていて、
gyazo.com
この辺(returnのところで)をブレイクしてみる。仕組みとしてはAPIで受け取ったレスポンスから
Questions(スレッドの開始)とそれにつながるAnswers(スレッドに紐づいてるメッセージ)
を振り分けている想定。
gyazo.com

Questionsが空っぽなので、まるで問題が生成されていない。。。
IsQuestionの実装は単純で

function isQuestion(message)
{
  return "replies" in message;
}

だ。なので、repliesさえちゃんとあればまともに動くはず。もしやと思ってmessageの中を開けてみると
gyazo.com
ご覧のありさまで。パラメータがなくなっている様子。
ひとまずchannels.historyメソッドの公式を見てみましたが・・・それらしきパラメータはresponseにはなさそうです・・・
api.slack.com
変わりにreply_countというパラメータが返ってきてます。こっちも公式には乗っていませんが、ひとまずこれを使ってみることにしました。
latest_replyにはTSが返ってきているのでこれも使えそうです。そのように変更してみます。

こんな感じに直します。

/**
 試験データとして扱いやすいように加工して返却する。
 */
function convertExamItems(response)
{
  var messages = fillExamMessages(response);
  var sortedMessages = orderByTimeStampeAscending(messages);
  Logger.log("formatExamItem:sortedMessages{" + sortedMessages + "}");
  
  // timeStampをキーとして、問題(リプライがあるか)と回答で分ける。
  var questions = {};
  var answers = {};
  for(i in sortedMessages)
  {
    var message = sortedMessages[i];
    if(isQuestion(message))
    {
      questions[message["ts"]] = message;
    }
    else
    {
      answers[message["ts"]] = message;
    }        
  }
  
  var examItems = [];
  for(key in questions)
  {
    var question = questions[key];
    //var replieKey = question["replies"][0]["ts"];
    var replieKey = question.latest_reply;
    question["answer"] = answers[replieKey];
    examItems.push(question);
  }
  return examItems;
}

function isQuestion(message)
{
  //return "replies" in message;
  return message.reply_count > 0;
}

試しに実行しました。
gyazo.com
とりあえずそれっぽく動くようになりました。あっていたらしい。

ところで、公式に不穏な記述があるぞ・・・

よく見るとAPIのレスポンスにも書いてあるんですが、いまつかっているAPIは非推奨なようで。。。
conversations.history
をつかっちゃいなよ!
みたいなことが書かれています。ということでとりあえず呼び出すAPIを変えてみます。
gyazo.com
あばばばばば。。。ということで公式見ながらデバッグします。
前と同じ場所にブレイクを張って見ます。
gyazo.com
今度はAnswerが一つもありません。母体となるmessagesのほうを見てみましょう。
gyazo.com
どうやら2件しか返ってきて稲用です。QとAで2組あるので、4件来る想定ですが。
どうやらQの部分しか取得できていないようです。公式説明を読んでみます。

conversations.repliesをつかう。

api.slack.com
残念ながらreplyに関する記述はここからは読み取れませんでした。観念してググって見ます。
dev.classmethod.jp
の中に答えがありました。
conversations.repliesを使えばよいようです。公式の方もみてみます。
api.slack.com

This Conversations API method returns an entire thread (a message plus all the messages in reply to it), while conversations.history method returns only parent messages.

ということで、こっちにはおもいっきりconversations.historyでは親しかとれないよ。と書いてました。うまく組み合わせて使う必要がありそうです。
conversations.repliesはthread_idの指定が必要です。なので、まとめて一気にとるような使い方ができません。
histolyでスレッドを特定して、内容はrepliesでとるようにしてみます。

function cannelReplyURL(thread_ts)
{
  var url = "https://slack.com/api/conversations.history";
  url += "?";
  url += "channel=" + channelID;
  url += "&"
  url += "ts=" + thread_ts;
  Logger.log("cannelReplyURL:{" + url + "}");
  return url;  
}
/**
 * UrlFetchAppを使って結果を返す。
 * @param {string} Request URL
 * @param {Object} Request Option Parameters
 * @return {Object} Get Request Response Json Object
 */
function fetchApp(url)
{
  var response = UrlFetchApp.fetch(url, token);
  var parsedResponse = JSON.parse(response.getContentText());
  Logger.log("getRequest:{" + parsedResponse + "}");
  return parsedResponse;
}

/**
 試験データとして扱いやすいように加工して返却する。
 */
function convertExamItems(response)
{
  var messages = fillExamMessages(response);
  var sortedMessages = orderByTimeStampeAscending(messages);
  Logger.log("formatExamItem:sortedMessages{" + sortedMessages + "}");
  
  // timeStampをキーとして、問題(リプライがあるか)と回答で分ける。
  /*
  var questions = {};
  var answers = {};
  for(i in sortedMessages)
  {
    var message = sortedMessages[i];
    if(isQuestion(message))
    {
      questions[message["ts"]] = message;
    }
    else
    {
      answers[message["ts"]] = message;
    }    
  }
  
  var examItems = [];
  for(key in questions)
  {
    var question = questions[key];
    //var replieKey = question["replies"][0]["ts"];
    var replieKey = question.latest_reply;
    question["answer"] = answers[replieKey];
    examItems.push(question);
  }
  */
  var examItems = [];
  for(i in sortedMessages)
  {
    var apiResponse = fetchApp(cannelReplyURL(sortedMessages[i].thread_ts));
    var question = apiResponse.messages[0];
    question["answer"] = apiResponse.messages[1];
    examItems.push(question);
  }
  return examItems;
}

/*function isQuestion(message)
{
  //return "replies" in message;
  return message.reply_count > 0;
}
*/

fetchAppはもともとあるものを再利用。なんだかよくわからないループもすっきりしました。バッチりです。