はじめに
本コンテンツは、GAS初心者が作成したため、
エラー処理がされていないところが多々あります。
ご自身の環境で試す際は自己責任でお願いいたします。
0.制作の経緯(読み飛ばしてもらって構いません。)
学習記録系のアプリはそこそこ存在しているが、
私はその手のアプリの通知がうざかったり、
そもそも入れて維持できるだけのスマホの容量がなかったり、
広告があったり(まああって当然ですけれども)と、
極めつきは、結局やらなくなってアンインストールへと回されてしまう悪循環を引き起こしていました。
というわけで
そのすべてを解決する良いアプリを作ってしまおう。
まあそんなこと意気込んでもアプリを作る技術はほとんど持ち合わせておらず。
仮に作れたとしても、OS間の引き継ぎなどを考えると、ローカルで完結するアプリは難しいと思いました。
そこで今まで触れてきた事のあるGASとMessaging APIで、学習記録ボットでも作ろうかなと考えたわけです。
というわけで本編始まります。
注意事項
今回のプログラムは、思いつきで機能の実装等をしているため、
命名規則とか、配置とかがやばいですがあしからず。
開発環境
Windows10のPC
Chrome
コーダーにはスクリプトエディタを利用していますが、
Html部分に関してはChromeの開発者ツールで作成してコピペしています。
やったこと一覧
LineのBotアカウントの取得(省略)
ユーザー登録
記録関係の実装
Line上での記録閲覧操作
グラフの生成
WEBサイトの作成
今回は上3つをやっていきます。
シート構成
*log:記録操作等を保管するためのシート
(の予定だったが後に高速化のために記録処理以外の記録はしなくなったシート)
*db:多分アクセス回数の一番多いシート。
各ユーザーの記録データの保持に使われている。
(記録方向を間違えたために処理に少々苦戦する。)
*setting:言わずもがな各種設定データを記述している。関数実行エリアとしても利用している。
db_daily:直近一週間分の記録データをまとめるのに利用。weekly_dbよりあとにできた。
weekly_db:週別記録を保管する所。名前をつけ間違えたが諦めている。
cmd_list:書き込み操作等のマニュアル。普通になくてもいいかと思った。
その他色々
またいろいろな理由があって一部は楽をするために、
スプレッドシート関数の、IMPORTRANGEを利用しdbからユーザーデータを複製させています。
(つまりDBで事故ると死ぬ。)
ユーザー登録を作ろう
follow=友達になったらDBに追加させたい
followイベントのときに実行する関数を用意しよう。
functionfollow_init(userid){setsh.getRange("B3").setValue(userid);if(setsh.getRange("A3").getValue()!="#N/A"){}else{vardb_user_n=setsh.getRange("A4").getValue()+2dbsh.getRange(1,db_user_n).setValue(userid);dbsh.getRange(2,db_user_n).setValue("user名"+user_pos);Utilities.sleep(100);varcolm=address_serch(db_user_n);dbsh.getRange(3,db_user_n).setValue("=sum("+colm+"11:"+colm+")");dbsh.getRange(4,db_user_n).setValue("=COUNTA("+colm+"11:"+colm+")+11");wlogsh.getRange(3,db_user_n).setValue("=sum("+colm+"4:"+colm+")/60");wlogsh.getRange(4,db_user_n).setValue("=RANK.EQ("+colm+"3,$B$3:$3)");dlogsh.getRange(3,db_user_n).setValue("=RANK.EQ("+colm+"4,$B$4:$4)");dlogsh.getRange(4,db_user_n).setValue("=sum("+colm+"5:"+colm+")/60");}}address_serch関数はDB内の列番地を知るときに使う。
ここで言うABCとかのこと
関数はこんな感じ
// var colm=address_serch(user_pos); と宣言して使う。functionaddress_serch(user_pos){setsh.getRange("A6").setValue("=ADDRESS(3,"+user_pos+",4)");varcell_id=setsh.getRange("A6").getValue().split("");returncell_id[0]}できた。
これでスプレッドシートの関数も一緒に追加できる。
先にメイン処理を作ろう。
メイン処理ことコマンド選択部分の作成
functiondoPost(e){varjson=JSON.parse(e.postData.contents);start_action(json);returnContentService.createTextOutput(JSON.stringify({'content':'post ok'})).setMimeType(ContentService.MimeType.JSON);}functionstart_action(json){for(vari=0;i<json.events.length;i++){vartype=json.events[i].type;varuserId=json.events[i].source.userId;vargroupId=json.events[i].source.groupId;varroomId=json.events[i].source.roomId;if(type==="follow"){follow_init(userId);}elseif(type==="message"){varcontent_type=json.events[i].message.type;//送られたメッセージ内容を取得if(content_type==="text"){varreply_token=json.events[i].replyToken;varmessage=json.events[i].message.text.toLowerCase();varcmd_type=check_cmd(message);switch(true){casecmd_type==="rec":varlastRow1=setsh.getRange("A2").getValue();logsh.getRange(lastRow1,1).setValue(i);varuser_pos=db_user_search(userId);varcmd_data=text_event(userId,message,groupId,roomId,json,lastRow1,user_pos);record(cmd_data,userId,reply_token);breakcasecmd_type==="ch":show_user_log(userId,reply_token);break// case cmd_type==="wg" :// wchart_make(userId,reply_token);// breakcasecmd_type==="rem":remove_action(userId,reply_token);breakcasecmd_type==="remove_request":varlastRow1=setsh.getRange("A2").getValue();logsh.getRange(lastRow1,1).setValue(i);varcmd_data=text_event(userId,message,groupId,roomId,json,lastRow1,user_pos);// case cmd_type==="dg":// dchart_make(userId,reply_token);// breakcasecmd_type==="set_username":set_username(userId,message,reply_token);breakcasecmd_type==="set_password":set_password(userId,message,reply_token);breakcasecmd_type==="access":access_conf(userId,message,reply_token);break}}}}}//コマンド解析用関数functioncheck_cmd(message){// 分割する数値varbeforeText=message;// 数値を文字列に変換して、一文字ずつ分割varbeforeTextArr=String(beforeText).split(/\s/);returnbeforeTextArr[0];}//おわりCase文で処理を分けた。
仕組み的には
dopost:データ受け取り、関数への引き渡し役
start_action:コマンド分類
各処理へ
記録処理を作ろう
いけそう。
//start_actionの該当部分の抜粋casecmd_type==="rec":vargroupId=json.events[i].source.groupId;varroomId=json.events[i].source.roomId;varlastRow1=setsh.getRange("A2").getValue();logsh.getRange(lastRow1,1).setValue(i);varuser_pos=db_user_search(userId);varcmd_data=text_event(userId,message,groupId,roomId,json,lastRow1,user_pos);record(cmd_data,userId,reply_token);break//抜粋部分おわり//メッセージ送信関数functionsend_message(send,reply_token){UrlFetchApp.fetch(line_endpoint,{'headers':{'Content-Type':'application/json; charset=UTF-8','Authorization':'Bearer '+CHANNEL_ACCESS_TOKEN,},'method':'post','payload':JSON.stringify({'replyToken':reply_token,'messages':[{'type':'text','text':send,}],}),});}//おわり//操作記録関数functiontext_event(userId,message,groupId,roomId,json,lastRow1,user_pos){vardate=newDate();vartime=Utilities.formatDate(date,'Asia/Tokyo','yyyy/MM/dd|HH:mm:ss');varwrite_fill=[[time,userId,message,"0",userdata_get(user_pos)]];//記録処理 logsh.getRange(lastRow1,1,1,5).setValues(write_fill);if(groupId!=undefined){logsh.getRange(lastRow1,4).setValue(groupId);}elseif(roomId!=undefined){logsh.getRange(lastRow1,4).setValue(roomId);}else{logsh.getRange(lastRow1,4).setValue(userId);}returneditcmd(message);}//おわり//コマンド解析2functioneditcmd(message){// 分割する数値varbeforeText=message;// 数値を文字列に変換して、一文字ずつ分割varbeforeTextArr=String(beforeText).split(/\s/);returnbeforeTextArr;}//おわり//ユーザー名取得functionuserdata_get(user_pos){returndbsh.getRange(2,user_pos).getValue();}//おわり//学習時間登録操作functionrecord(data,userId,reply_token){varuser_pos=db_user_search(userId);vardata_c=dbsh.getRange(4,user_pos).getValue();varstudy_time=data[1];vartime_mes=data[2];if(isNaN(study_time)){varsend="パラメータに数値が入力されていません。\nなお漢数字は使用できません"}else{if(time_mes==="h"){study_time=study_time*60}dbsh.getRange(data_c,user_pos).setValue(study_time);//時間書き込みvarinfo=cmdsh.getRange("N1").getValue();if(info===undefined){varsend="お疲れ様でした。"+userdata_get(user_pos)+"さんは今回"+study_time+"分勉強しました。\nこれであなたは、今日トータルで"+dbsh.getRange(3,user_pos).getValue()+"分しました。\n次も頑張りましょう。"}else{varsend="お疲れ様でした。"+userdata_get(user_pos)+"さんは今回"+study_time+"分勉強しました。\nこれであなたは、今日トータルで"+dbsh.getRange(3,user_pos).getValue()+"分しました。\n次も頑張りましょう。\n"+info}}//メッセージを返信 send_message(send,reply_token);}//おわり良さげ
こだわりポイント
1
functiondoPost(e){varjson=JSON.parse(e.postData.contents);start_action(json);returnContentService.createTextOutput(JSON.stringify({'content':'post ok'})).setMimeType(ContentService.MimeType.JSON);}functionstart_action(json){for(vari=0;i<json.events.length;i++){(略)ループさせてできる限り全部のリクエストを漏らさないように工夫した。
ホントはもうちょい処理を短くすべき。
2
よく使う操作は大半関数にした。(当然だと思う)
Line上での記録閲覧を作ろう。
とりあえず実装。
functionshow_user_log(userId,reply_token){varuser_pos=db_user_search(userId);varweekly_log=dlogsh.getRange(4,user_pos).getValue();vartotal_log=wlogsh.getRange(3,user_pos).getValue()+weekly_log;if(total_log!=0){total_log=total_log;}else{total_log=0;}varsend=userdata_get(user_pos)+"さんの記録データはこんな感じです。\n直近1週間|"+weekly_log+"時間 \n 全統計(毎週月曜更新)|"+total_log+"時間でした。"send_message(send,reply_token);}出来上がり
特にこだわりは無い。
今パートで作ったもののテスト
良さそう
ただ記録の時間表示は工夫したほうがいいかも。
次回予告
機能の拡充編
最難関の敵グラフ生成機能
その他おまけのような機能たちを紹介します。
See you again.




