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