WEB

【GAS】CodeWars を ChatWork, Slack, Teams に導入してみよう

WEB
この記事は約18分で読めます。

CodeWarsとは、プログラミング力を高める/身につけていく問題を提供するサービスです。
CodeWars

この記事では、GAS を使って、CodeWars の問題をChatWork, Slack, Teams上で1日1題出題させる実装についてザッと書いていきます。
毎日問題を探すのが億劫だったり、ついつい解くのを忘れていくのを防止するため、BOTを設定して、定刻にスプレッドシートに置いてある問題を出題させるようにします。
対象読者は、CodeWarsに興味がある方です。

はじめに

ChatWorkに導入する方法から進めていきます。最後らへんにSlack, Teams対応もします。ただ、Slack, Teams の対応は1ファイルを変更するだけなので、上から順番に読み進めていくことを勧めます。

1:仕様確認、環境設定

仕様は、下記となります。

<br>1:平日のみ出題<br>2:スプレッドシートのPickUp_Listsシートからランダムで毎日一つ、N時台に出題される。<br>3:一度出題された問題はスプレッドシートから削除される。<br>4:スプレッドシート上で、残りの問題数が3つ以下になると、投稿時に、残り[X]なので問題を補充してねと表示される。(gsファイル)

スプレッドシートの提供

スプレッドシートは、こちらを使います(セキュリティの関係上、githubに落としました。DL後に、各自のspreadsheetに貼っていただければ、と)。
https://github.com/Kojiro-schatten/codewars_sheet
使うのはPickUp_Listsです。
一度出題された問題はPickUp_Listsシートから削除されるため、All_Listsシートに全問を先に載せておき、都度問題を補充していきます。その際に、「投稿時に、残り[X]なので問題を補充してね」と表示させるようにします。

別に削除する必要ない場合でも改変が楽なため、その辺はカスタマイズしていただいて下さい。

ChatWorkクライアントの設定

ChatWorkで設定する場合のみ、実は、ChatWorkライブラリがGAS上に存在しているため、そちらを使うことで簡単にメッセージをGASから送ったりできます。GAS環境を作成後、早速ライブラリの +ボタンを押して、スクリプトIDを入れましょう。Slack, TeamsのはこのスクリプトIDは無いので入力の必要もありません。

(ChatWorkのみ使う)スクリプトID:
1nf253qsOnZ-RcdcFu1Y2v4pGwTuuDxN5EbuvKEZprBWg764tjwA5fLav

これで環境設定は終了です。
再喝: Slack,Teamsでは、スクリプトIDが無いため、こちらを設定することはありません。

2:平日のみ出題されるようにする

環境設定が完了したら、とりあえず動くコード を書いていきます。
コード.gsをmain.gsに名前変更して、下記のようにします。
ChatWork の TOKEN、ROOM_ID、は各自で設定していただいて下さい。

main.gs

function test() {
  let today = new Date();
  today = new Date(2021, 4, 28) // test mode

  if (doCodeWars(today) == true) {
    const getCodeWarsSheet = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/1IlBfmRk8JlJ0Ldz52QcDTLdclrsw6DHhRwx_29O2MEY/edit#gid=0").getActiveSheet()
    const client = ChatWorkClient.factory({token: "XXXXXXXXXXXXXX"}) // Chatwork API
    const randomPicked = 1 + Math.floor(Math.random() * getCodeWarsSheet.getLastRow())
    const range = getCodeWarsSheet.getRange(randomPicked, 2, 1).getValues() //行、列、数
    const convertSingleArray = range.reduce((pre, cur) => {
      pre.push(...cur)
      return pre
    }, [])

    const pickedChallenge = convertSingleArray.join('')
    const uriFetch = UrlFetchApp.fetch(`https://www.codewars.com/api/v1/code-challenges/${pickedChallenge}`).getContentText()
    const jsonUriFetch = JSON.parse(uriFetch)
    const shapeLanguageView = jsonUriFetch.languages.join(', ')

    const response = {
      name: jsonUriFetch.name,
      rank: jsonUriFetch.rank.name,
      link: jsonUriFetch.url,
      languages: shapeLanguageView,
    }

    client.sendMessage({
      room_id: XXXXXXX, // ROOM_ID
      body: `[info][title]${response.rank} ${response.name}[/title]${response.link}\n言語: ${response.languages}[/info]`
    })
  }
}

function doCodeWars(targetDay) {
  const rest_or_work = ["rest", "mon", "tue", "wed", "thu", "fri", "rest"][new Date(targetDay).getDay()]
  if(rest_or_work === "rest") {
    return false
  }
  return true
}


3:スプレッドシートからランダムで毎日一つ、N時台に出題する

スプレッドシートが持っているトリガーファンクションは、GUI上でイベントを起こすアクションを指定できるため、とても楽です(曜日ごとのアクションもあればもっと嬉しいのですが)。

トリガー画面では、右下の +トリガーを追加 ボタンをクリックしてお好みの時間に指定しましょう。保存を押せば、完了です。選択した時間帯に、実行する関数で選ばれたモノ(今回ならtest関数)が実行されます。
この時点で、最低限のことはできました。

次からはオプションに近いですが、進めていきましょう。

4:一度出題された問題はスプレッドシートから削除する。

さて、ランダムに出題されるようにしているので3日連続で同じ問題が出たら嫌ですよね。なので、一度出題された問題は、とりあえず削除することにしました。gasはとても便利なので一行でそれが出来てしまいます。
具体的には、client.sendMessage の後ろに

main.gs

.....
    client.sendMessage({
      room_id: 227617281, // room ID
      body: `[info][title]${response.rank} ${response.name}[/title]${response.link}\n言語: ${response.languages}[/info]`
    })
        getCodeWarsSheet.deleteRow(randomPicked) // deleteRow() = 引数で指定された列を削除してくれる
  }
}

と書けば、スプレッドシートからrandomPickedされたものを削除してくれます。

5:責務分けて見やすくする

作成したはいいけど、大分見づらくなったような気がしますので、責務を分けていきましょう。
main.gsの他に4つのファイルを作成します。

main.gs
bodyContents.gs //実際に送るcodewarsのデータを整形する
sendListsInfo.gs //スプレッドシートから問題をランダムピックしてきて、path と PickUp_Lists に残り何問あるのか、というremaining データを渡す
isCodeWarsDay.gs //codewarsの出題の有無を曜日ごとに決める
env.gs

の5ファイルをエディタで作成します。それぞれのファイルの中身を、下記のようにします。
なお、main.gs では、 test() ではなく main() と関数名も変更しておりますので、注意して下さい。

main.gs

function main() {
  let today = new Date();
  // 土日はOFFになるよう設定されています。テストモードとして、下記で 2021/5/28(金)に、つまり平日に動いているかどうかをチェックしています
  // today = new Date(2021, 4, 28) // use as test mode

  if (isCodeWarsDay(today) == true) {
    const client = ChatWorkClient.factory({token: ENV.CHATWORK_TOKEN}) // Chatwork API
    const response = bodyContents(sendListsInfo()) //pathとremainingを渡す

    const moreThanThree = `[info][title]${response.rank} ${response.name}[/title]${response.link}\n言語: ${response.languages}[/info]`
    const lessThanThree = `[info][title]${response.rank} ${response.name}[/title]${response.link}\n言語: ${response.languages}[/info]\n残り問題数が${response.remaining - 1}つなのでスプレッドシート内の問題を補充してください` //getLastRowは出題前の個数なため-1する

    const checkRemaining = () => response.remaining <= 4 ? lessThanThree : moreThanThree
    const body = checkRemaining()

    try {
      client.sendMessage({
        room_id: ENV.ROOM_ID,
        body: body
      })
    }
    catch(e) {
      console.error(e)
    }
  }
}

bodyContents.gs

function bodyContents(codeWars) {
  try {
    const path = codeWars.path
    const uriFetch = UrlFetchApp.fetch(`${ENV.CODEWARS_URL}${path}`).getContentText()
    const jsonUriFetch = JSON.parse(uriFetch)
    const shapeLanguageView = jsonUriFetch.languages.join(', ')
    const response = {
      name: jsonUriFetch.name,
      rank: jsonUriFetch.rank.name,
      link: jsonUriFetch.url,
      languages: shapeLanguageView,
      remaining: codeWars.remaining,
    }
    return response
  }
  catch (e) {
    console.error('引数エラー')
    return false
  }
}

sendListsInfo.gs

function sendListsInfo() {
  const getCodeWarsSheet = SpreadsheetApp.openByUrl(ENV.SPREADSHEET_URL).getActiveSheet()
  const getLastRow = getCodeWarsSheet.getLastRow()
  if (getLastRow === 0) {
    console.error('問題が無いお(^ω^;)')
    return false
  }
  const randomPicked = 1 + Math.floor(Math.random() * getLastRow)
  const range = getCodeWarsSheet.getRange(randomPicked, 2, 1).getValues() //getRange(行、列、数)
  const convertedChallengePath = range.reduce((pre, cur) => {
    pre.push(...cur)
    return pre
  }, []).join('')

  //重複無く問題を出すため、一度出された問題の行は削除する
  getCodeWarsSheet.deleteRow(randomPicked)

  const sendListsInfo = {
    path: convertedChallengePath,
    remaining: getLastRow,
  }
  return sendListsInfo
}

isCodeWars.gs

function isCodeWarsDay(targetDay) {
  const rest_or_work = ["rest", "mon", "tue", "wed", "thu", "fri", "rest"][new Date(targetDay).getDay()]
  if(rest_or_work === "rest") {
    return false
  }
  return true
}

env.gs

const ENV = {
  CHATWORK_TOKEN: 'XXXXXXXXXXXXXXXXX',
  ROOM_ID: 'XXXXXXXXXXXXXXXXX',
  SPREADSHEET_URL: 'XXXXXXXXXXXXXXXXX',
  CODEWARS_URL: 'https://www.codewars.com/api/v1/code-challenges/',
  TEAMS_URL: 'XXXXXXXXXXXXXXXXX',
  SLACK_URL: 'XXXXXXXXXXXXXXXXX',
}

以上で、完了となります。ここからは、Slack, Teamsの対応をしていきます。
変更するファイルはmain.gsのみです。

6:Slack対応

main.gs

function main() {
  let today = new Date();

  if (isCodeWarsDay(today) == true) {
    const client = ChatWorkClient.factory({token: ENV.CHATWORK_TOKEN}) // Chatwork API
    const response = bodyContents(sendListsInfo()) //pathとremainingを渡す

    const moreThanThree = `\`${response.rank} ${response.name}\`\n${response.link}\n言語: ${response.languages}`
    const lessThanThree = `${response.rank} ${response.name}\n${response.link}\n言語: ${response.languages}\n残り問題数が${response.remaining - 1}つなのでスプレッドシート内の問題を補充してください` //getLastRowは出題前の個数なため-1する

    const checkRemaining = () => response.remaining <= 4 ? lessThanThree : moreThanThree
    const body = checkRemaining()

    const options = {
      "method" : "post",
      "contentType" : "application/json",
      "payload" : JSON.stringify({"text" : body,})
    };

    try {
      UrlFetchApp.fetch(ENV.SLACK_URL, options);
    }
    catch(e) {
      console.error(e)
    }
  }
}

7:Teams対応

main.gs

function main() {
  let today = new Date();
  today = new Date(2021, 4, 28) // use as test mode

  if (isCodeWarsDay(today) == true) {
    const response = bodyContents(sendListsInfo()) //pathとremainingを渡す
    const lessThanThree = [{
      "name": "残り問題数",
      "value": `${response.remaining - 1}つなのでスプレッドシート内の問題を補充してください`
    }]
    const checkRemaining = () => response.remaining <= 4 ? lessThanThree : ''
    const body = checkRemaining()

    //Official:https://docs.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using
    var data = {
    "@type": "MessageCard",
    "@context": `${response.link}`,
    "themeColor": "0076D7",
    "summary": "summary",
    "sections": [{
        "activityTitle": `${response.rank} ${response.name}`,
        "activitySubTitle": `言語: ${response.languages}`,
        "activityImage": "https://teamsnodesample.azurewebsites.net/static/img/image1.png",
        "facts": body,
        "markdown": true
    }],
    "potentialAction": [{
        "@type": "OpenUri",
        "name": "Challenge",
        "targets": [{
            "os": "default",
            "uri": `${response.link}`
        }]
    }]
}
    var payload = JSON.stringify(data);
    var headers = {
        "Content-Type": "application/json"
    };
    var options = {
        "headers": headers,
        "method": "post",
        "payload": payload
    };

    const res = UrlFetchApp.fetch(ENV.TEAMS_URL, options);
    Logger.log(res)
  }
}

最後に

ChatWorkだけ対応するつもりでした。

が、なんと弊社
Teamsに移行するよということで、3つとも色々設定していました。
GASはとても使いやすく始めてから2日ほどで今回のbotが作れました。
個人的に、今後もお世話になろうと思っています。

参考記事

制御フローとエラー処理 – JavaScript | MDNhttps://developer.mozilla.org

Google Apps Script(GAS)でスプレッドシートの最終行を取得する(getLastRow)
https://auto-worker.com/blog/?p=1354

GASでスプレッドシートの選択しているセル・範囲の位置(行番号・列番号)を取得する方法
https://auto-worker.com/blog/?p=1344

https://auto-worker.com/blog/?p=607
Google Apps Script(GAS)入門 スプレッドシートを取得・読み込む3種類の方法を解説

Google Apps Scriptでスプレッドシート内を検索して行番号を返す関数(高速版)
https://tonari-it.com/gas-spreadsheet-find/

Google Apps Scriptでスプレッドシートの行を削除する2つの方法
https://tonari-it.com/gas-spreadsheet-delete-rows-splice/

【Google Apps Script入門】セルの取得・変更をする
https://uxmilk.jp/25841