GoogleAppsScriptしか勝たん

本記事はWMMCAdventCalendar2021

adventar.org

の4日目の記事です。

昨日はImanishi君のEagleでユニバーサル基板の回路設計 - 気ままな日々日記でした。
自分が3年生の時の研究室課題でユニバーサル基板の回路設計があったんですけど、その時eagleでやろうとして面倒くささのあまり途中で投げてしまった記憶が…2.54mm…とか意識してビア開けたんですけど…心が折れたので来年の指導の際に参考にします…


 えーみなさん3日ぶりです。初日がついこないだ終わったと思ったらもう4日目が来てしまい、
記事が足りません。(OB・OGは参加してくださいね(圧力))
というかこのサークルにいる人間は24日と25日は絶対暇だろなんで空いてんだ忙しぶるな

12月4日追記:
aluminum君が24日に入れてくれました!
ありがとう!そして同志すぎる!
そして25に先輩が入ってくれました。
誰に脅迫されたんでしょうか…?
怖い人もいるものですねぇ…?

初日の記事は[これ](https://judge.hatenadiary.com/entry/2021/12/01/000000)です。

目次

0.まずはじめに
1.Google Apps scriptって?????
2.Google Apps ScriptでLINEに通知してみる
3.Google apps ScriptでSlackに通知してみる
4.Google apps Scriptでカレンダー情報を取得してみる
5.Google apps Scriptでのトリガー
 5-1.Google apps Scriptでトリガーを作製する
 5-2.Google apps Scriptでトリガーを削除してみる
6.これまでのすべてを組み合わせると...?


0.まずはじめに

 ことの始まりはすごく単純で、あまり研究室の話は外部に出してはならないので簡潔に話しますと、
今年から研究室のイベントが総務からメール通知されなくなりました。
以前までは会議やイベントの会場や議事録の提出先、資料のアップロード先などがリンクとともに通知されていたのですが
それが廃止となり、各自で「Google Calendar」を見る流れとなったわけです。

僕「いや~でもみるのめんどくさ。」

ちくいち開いて確認するのもだるいし、いきなり明日会議か!?になる未来が容易に想像できました。
そこで毎日1週間先のイベントまで通知してくれないかなって思ったのがGoogleAppsScriptを本格的に触るきっかけとなったわけです。

*実は1年前に仮想彼女を作ってLINEをしようという大層気色悪いユーモアにあふれた記事を投稿したのですが(リア滅の刃(無限爆破編) - judgeのブログ)
あの時は簡単にしか理解してなかったので再度やっていることになるのですが.....

1.Google Apps Scriptってなに

 Googleが提供するJavaScriptベースのプログラミング言語(というかGoogleソフトをCUIで扱うための開発環境に近い気がする)です。
何か自動化するとき、ラズパイだったりだと電源つけっぱにしないといけないのですが、
これは一度書いて実行してしまえば(かつトリガーを管理すれば)電源消しても動くので便利です。
まずは準備です。
Google Apps Scriptのエディタに移ってみましょう。
もちろんこれを見ている諸君はGoogle Chromeを使っているだろうから(エクスプローラerいないよな....?)
Googleアカウンコにログインし、Google Driveに移りましょう。
下の画像の①のところをクリックするとDriveがあるはずです。
f:id:WMMCsaiteihen:20211202011334p:plain

そうしたら②の「新規」→「その他」→「Google Apps Script」の順で進みますと以下のような画面に移ります。
なければ「アプリを追加」から追加してみてください

f:id:WMMCsaiteihen:20211202011832p:plain

これで一応GoogleAppsScriptのエディタ画面に移れたわけです。
これで一応の準備はできました。
ここでこの後の説明が面倒にならぬように「GoogleAppsScript」を「GAS」と略して表記いたします。
よろちくbi

2.Google Apps ScriptでLINEに通知してみる
 なぜ最初がLINEなのでしょうか。というのもSlackへの通知の布石です。
個人的にLINEのAPIの説明は初心者に優しいドキュメントなので実装が楽で勉強になります。
またここでLINEでの実装をしておくとほぼそのままSlackでの実装につなげられます。要するにエッセンス的な感じです。
では早速初めて行きましょう。
そもそもまずAPIをたたく手順を軽く説明します。
LINEからメッセージをいただくには以下の手順を踏みます。

①こちらから「リクエスト」と呼ばれるものをLINEのサーバに送ります。

②「リクエスト」を受け取ったサーバが「リクエスト」内容にそって情報を処理します

③サーバからデータが送られてきます。

④メッセージを受け取れます

これを実行するためにまずは「リクエスト」なるものの中身をどう書けばいいのか、それを調べる必要があるのです。

従ってLINEが公開しているAPIのドキュメント
developers.line.biz
を読みに行きます。が、その前に大切な準備があります。

上記のリンクを踏むと右上にログインボタンがあるのでLINEアカウントでログインしてください。
(日本人の70%がLINEユーザらしいのでさすがにアカウントはもっているものとします)
すると新規プロバイダーを「作製」というボタンがあるのでそれを押してLINEの通知用のアカウントを作成していきます。
名前を決めたら次に以下の画面になると思うので真ん中をクリックしてください
f:id:WMMCsaiteihen:20211202015844p:plain

するとアカウントの名前や説明などを入力することになるのでお好きにしてどーぞ
通知用アカウントを作製したら、まずは「Basic Setting(基本設定)」の項目に移りましょう。
一番下に「Your user ID」があると思うのでそれを保存しておきます
f:id:WMMCsaiteihen:20211202020951p:plain

また、その次に「Message APIの項目に移り、
その一番下にあるAccess Token」「issue」します。
(要するにアクセストークンというURlみたいなのを発行します)
f:id:WMMCsaiteihen:20211202021549p:plain

これもコピーして保存しておいてください
f:id:WMMCsaiteihen:20211202021729p:plain

ではここまでのことをコードに記述しておきます。

function NotifyLINE() {
  const USER_ID="あなたのUserIDを入力";
  const ACCESS_TOKEN="あなたのACCESS_TOKENを入力";
}

ここで関数の名前を「NotifyLINE」にかえておきました。

さぁ!ようやくドキュメント
Messaging APIリファレンス | LINE Developers
を読みます。

「Message」の項目のリクエスト部分の一行目は以下のようになっていると思います。
f:id:WMMCsaiteihen:20211202025557p:plain

その下は「ヘッダー」について
f:id:WMMCsaiteihen:20211202025620p:plain

更にその下は「ボディ」について
f:id:WMMCsaiteihen:20211202025644p:plain

書かれています。
「ボディ」の中の「messages」はさらに配列構造を持っていて
その中身は以下のようになっているということもわかりました。(テキストを送信する場合)
f:id:WMMCsaiteihen:20211202131132p:plain
今回はテキスト(文章)を送信するのでスタンプや動画を送りたいときは
同ドキュメントの別の項を参照してください。

「ヘッダー」やら「ボディ」やらと言いましたがこれらは「リクエスト」の中身です。
つまりこれらを含む「リクエスト」を作製して送り付ければよいのです。
これらをコードに落とし込んでいきましょう

function NotifyLINE() {
  const USER_ID="あなたのUserIDを入力";
  const ACCESS_TOKEN="あなたのACCESS_TOKENを入力";

  //以下が今回新しく記載した部分
  var url="https://api.line.me/v2/bot/message/push";

  var text="Hello, world";

  //リクエストヘッダー
  headers={
  "Content-Type":"application/json; charset=UTF-8",
  "Authorization":"Bearer "+ACCESS_TOKEN
  };

  //リクエストボディ
  body={
    "to":USER_ID,
    "messages":[{
      "type":"text",
     "text": text
    }]  
  };

  //リクエストの作製
  request={
    "method":"post",
    "headers":headers,
    "payload":JSON.stringify(body)
  };

  //fetch:引っ張ってくる
  UrlFetchApp.fetch(url,request);
}

これで準備ができました。
「Ctrl + s」で保存したら実行してみましょう!の前に、
ここで先程の「Message APIのところに行き、
QRコードから先程作製した通知用アカウントを友達登録しておきます。
f:id:WMMCsaiteihen:20211202133902p:plain

友達登録を終えたらGASのエディタから実行ボタンを押すことで通知が来るはずです。
         f:id:WMMCsaiteihen:20211202135521p:plain
これでGASを使ってLINEにメッセージを送ることができるようになりました。


3.Google apps ScriptでSlackに通知してみる

 先程のLINE通知を応用すればSlackへの通知も簡単です。
ちょちょいとコードを軽く変えるだけなのとドキュメントを読めばわかるので詳細は省きます。
それよりもLINEとは異なってSlackに「Incoming Webhook」を入れておく必要があります。
slackを開き①~③の順番でクリックし、
「カスタムインテグレーション」より
「Incoming Webhook」と検索し、インストールします。
f:id:WMMCsaiteihen:20211202142508p:plain
f:id:WMMCsaiteihen:20211202142821p:plain

その後、「Webhook URL」を保存しておきましょう
f:id:WMMCsaiteihen:20211202143537p:plain

これで準備が終わりました。
これらをコードに落とし込むと

function notifySlack() {
 //自分用のslack
  var url="先程のコピーした部分";
  var text="Hello, world";
  
  var body={
    "username":"予定リマインダー",
    
    "text":"<!channel>\n\n"+text,
    
    //自分のSlackの宛先チャンネル
    "channel":"#general"
  }

  var request={
    "method":"POST",
    "contentType":"application/json",
    "payload":JSON.stringify(body)
  } 
  
  UrlFetchApp.fetch(url,request);
}

通知は以下のようになります。
これでGASを使ってSlackへの通知をすることができました!
f:id:WMMCsaiteihen:20211202140434p:plain

4.Google apps Scriptでカレンダー情報を取得してみる
Google Chrome「GAS Calendar」などと検索すると公式のドキュメントに到達します。
これです。
developers.google.com

ではドキュメントを読んでいくのですがサポートされている関数が膨大なので今回は雑に省いていきます。

上から関数を見ていくと
「getEventById」という関数が見つけられま。
f:id:WMMCsaiteihen:20211202164039p:plain

クリックしてみると
関数の詳細が以下のように載っています。
f:id:WMMCsaiteihen:20211202164151p:plain

「Return」のところをみると「CalendarEvent」というものが返ってくるようです。
これをクリックしてみると
このように「CalendarEvent」クラスが表示されます。
クラスの中にある関数(=メソッド)も出てくるのでさらに調べることができます。
また、サンプルコードが出てくることもあります。
さぁコードを書いてみましょうと行きたいですがまず準備があります。
「GoogleCalendar」を開き、以下の①をクリックすると②の部分が出てくるので「設定と共有」をクリックします。
f:id:WMMCsaiteihen:20211202170133p:plain

その後下に移っていくと「カレンダーID」があるのでこれを保存しておきます。
f:id:WMMCsaiteihen:20211202170506p:plain

これにて準備は完了です。
ではまず簡単にカレンダー情報をとってくるコードを書いてみましょう
ここではまず例としてイベント名を取得するコードを書いてみます。

function getCalendar(){
  //もしgooglecalendarが変更になった場合、以下のCALENDAR_IDを変更
  const CALENDAR_ID="    ";//ここに先程のIDを入力!!!!!!

  //calendarIDを元にcalendar情報を取得
  var my_calendar=CalendarApp.getCalendarById(CALENDAR_ID);
  //イベント情報を取得する日を本日に指定
  var start_date=new Date();
  var my_events=my_calendar.getEventsForDay(start_date);
  
  my_events.forEach(function(my_event){
    //ここではためしにイベント名を取得してみる
    var title=my_event.getTitle().toString();
    Logger.log(title);
  })
}

上記の出力結果は以下のようになります
f:id:WMMCsaiteihen:20211202174848p:plain
今日のイベント名は「研究」でした。(してない)

他にも同様の情報を取得できます。
後のコードで紹介するのですが
大体はいかのこのメソッドで取得できます
f:id:WMMCsaiteihen:20211202180013p:plain

以下のコードでやってみます。

function getCalendar(){
  //もしgooglecalendarが変更になった場合、以下のCALENDAR_IDを変更
  const CALENDAR_ID="    ";//ここに先程のIDを入力!!!!!!

  //calendarIDを元にcalendar情報を取得
  var my_calendar=CalendarApp.getCalendarById(CALENDAR_ID);
  //イベント情報を取得する日を明日に指定
  var start_date=new Date();
  start_date.setDate( start_date.getDate() + 1 );

  var my_events=my_calendar.getEventsForDay(start_date);
  
  my_events.forEach(function(my_event){
    //ここではためしにイベント名を取得してみる
    var title=my_event.getTitle().toString();
    var mtg_url=my_event.getLocation();
    var my_event_description=my_event.getDescription();
    var event_start_time=my_event.getStartTime();   
    var event_end_time=my_event.getEndTime();

    Logger.log("イベント名:"+title);
    Logger.log("リンク:"+ mtg_url);
    Logger.log("開始時刻:"+ event_start_time);
    Logger.log("終了時刻:"+ event_end_time);
    Logger.log("説明:"+ my_event_description);
  })
}

結果はこうなります。
f:id:WMMCsaiteihen:20211202181049p:plain

しかしこれだと時刻が見にくいので修正します。
以下のコードに直すと

function getCalendar(){
  //もしgooglecalendarが変更になった場合、以下のCALENDAR_IDを変更
  const CALENDAR_ID="      ";//ここに先程のIDを入力!!!!!!

  //calendarIDを元にcalendar情報を取得
  var my_calendar=CalendarApp.getCalendarById(CALENDAR_ID);
  //イベント情報を取得する日を明日に指定
  var start_date=new Date();
  start_date.setDate( start_date.getDate() + 1 );

  var my_events=my_calendar.getEventsForDay(start_date);
  
  my_events.forEach(function(my_event){
    //ここではためしにイベント名を取得してみる
    var title=my_event.getTitle().toString();
    var mtg_url=my_event.getLocation();
    var my_event_description=my_event.getDescription();
    var event_start_time=my_event.getStartTime(); 
    event_start_time=Utilities.formatDate(event_start_time,"Asia/Tokyo","HH時mm分");  
    var event_end_time=my_event.getEndTime();
    event_end_time=Utilities.formatDate(event_end_time,"Asia/Tokyo","HH時mm分");

    Logger.log("イベント名:"+title);
    Logger.log("リンク:"+ mtg_url);
    Logger.log("開始時刻:"+ event_start_time);
    Logger.log("終了時刻:"+ event_end_time);
    Logger.log("説明:"+ my_event_description);
  })
}

結果はこうなります
f:id:WMMCsaiteihen:20211202181410p:plain

見やすくなりましたね。

これでGooglecalendarから1日のイベントは取得できましたね。
(逆に言えばfor文を使えば何日分もイベントは取得できますね)

7日分のイベント情報を取得するコードは以下のようになります。

function getCalendar(){
  //もしgooglecalendarが変更になった場合、以下のCALENDAR_IDを変更(以下は2021年現在のもの)
  const CALENDAR_ID="  ";

  //calendarIDを元にcalendar情報を取得
  var my_calendar=CalendarApp.getCalendarById(CALENDAR_ID);
  //イベント情報を取得する日を本日に指定
  var start_date=new Date();
  
  //1日のイベントを取得し${SHOW_PERIODS}回繰り返してtextを作成する
  for(var i=0;i<SHOW_PERIODS;i++){
    //今日一日のイベントを配列で取得
    my_events=my_calendar.getEventsForDay(start_date);
    my_events.forEach(function(my_event){//配列の各項目に対して以下の処理を行う
      var title=null; //イベント名
      var mtg_info;   //イベント情報全体
      var mtg_url;    //イベントのURL
      var my_event_description;  //イベントの備考欄
      var event_start_time;       //予定開始時間
      var event_end_time;         //予定終了時間

     //MTGのURLがLocationプロパティに入っているので今回はgetLocation()メソッドを使う
      mtg_url=my_event.getLocation();

      //イベント説明の取得
      my_event_description=my_event.getDescription();

      //イベントの開始・終了時刻を取得、それをフォーマット化する。(Utilities.formatDateをコメントアウトしてLogger.logすれば意味が分かる)
      event_start_time=my_event.getStartTime();
      event_start_time=Utilities.formatDate(event_start_time,"Asia/Tokyo","HH時mm分");
      event_end_time=my_event.getEndTime();
      event_end_time=Utilities.formatDate(event_end_time,"Asia/Tokyo","HH時mm分");      

      title=my_event.getTitle().toString();
      text+="・${month}月${date}日[${day}]の予定\n".replace("${month}",(start_date.getMonth()+1).toString()).replace("${date}",start_date.getDate(). toString()).replace("${day}",day_array[start_date.getDay()].toString());
      text+="`"+title+"`"+"\n";
      text+=" 時間:"+event_start_time+"~"+event_end_time+"\n";
      text+=" MTG場所:"+mtg_url+"\n";

      if(mtg_url!=""){//URLが存在する場合
        text+=" MTG場所:"+mtg_url+"\n";
      }

      if(my_event_description!=""){//説明がある場合
        text+=" [補足事項]\n"
        text+=" "+my_event_description+"\n\n";

        //正規表現をもちいてHTMLタグを消去
        text=text.replace(/(<([^>]+)>)/gi,'');
        text=text.replace(/&nbsp;/gi,"");
        text+="----------------------------------------------------------------------------------------------\n";         
      }else{//説明がない場合
        text+=" [補足事項]\n 無し\n\n"
        text+="----------------------------------------------------------------------------------------------\n";      
      }
    })
    //日付を進める
  start_date.setDate(start_date.getDate() + 1);
  }
      //予定がないとき
    if(text==""){
      text+="この1週間は学部・修士生に関係するイベントはありません";
    }
    //送信するtextの内容を確認する時用の関数
    Logger.log(text);
  return text;
}

結果はこのようになります。
f:id:WMMCsaiteihen:20211202183856p:plain

正規表現などの部分は自分で勉強しておいてください...

5.Google apps Scriptでのトリガー

5-1.Google apps Scriptでトリガーを作製する
トリガーというのは、これまで書いたコードを実行開始する何かしらのきっかけのことを指します。
コードは以下のようになります。
なお実行させたい関数の名前を「ScriptApp.newTrigger」関数に入れておけばオッケーです。

/****************************************************************
 * setTrigger()
 * 引数:なし
 * 返り値:なし
 *
 *[処理の説明]
 *・次の関数実行の時間トリガーを設定する(次の日の0:00) 
 ****************************************************************/
function setTrigger() {
 var setTime = new Date();
  setTime.setDate(setTime.getDate() + 1)
  setTime.setHours(0);
  setTime.setMinutes(00); 
  ScriptApp.newTrigger('実行させたい関数の名前').timeBased().at(setTime).create();  
}

実際に実行し、
以下のように画像の左の時計のマークをクリックすると
トリガーが作製されていることが確認できます。
f:id:WMMCsaiteihen:20211202144706p:plain


5-2.Google apps Scriptでトリガーを削除してみる

上節と同様にトリガーを消すことも可能です。

/*****************************************************
 * deltrigger()
 * 引数:なし 
 * 返り値:なし
 * 
 *[処理の説明]
 *・関数実施トリガーをいったんすべて消す
 *****************************************************/
function deltrigger() {
  const triggers = ScriptApp.getProjectTriggers();
  for(const trigger of triggers){
    if(trigger.getHandlerFunction() == "関数の名前"){
      ScriptApp.deleteTrigger(trigger);
    }
  }
}

6.これまでのすべてを組み合わせると...?

//**グローバル変数の定義***************************************//
//本文の変数text.これに本文を追加していく
var text="";
//曜日の配列
var day_array = new Array('日', '月', '火', '水', '木', '金', '土');

//以下のマクロ(的なの)を変えることでcalendarのイベント情報取得期間を決定している(以下では7日)
const SHOW_PERIODS=7;
//***********************************************************/

/****************************************************************
 * setTrigger()
 * 引数:なし
 * 返り値:なし
 *
 *[処理の説明]
 *・次の関数実行の時間トリガーを設定する(次の日の0:00) 
 ****************************************************************/
function setTrigger() {
 var setTime = new Date();
  setTime.setDate(setTime.getDate() + 1)
  setTime.setHours(0);
  setTime.setMinutes(00); 
  ScriptApp.newTrigger('notifySlack').timeBased().at(setTime).create();  
}

/*****************************************************
 * deltrigger()
 * 引数:なし 
 * 返り値:なし
 * 
 *[処理の説明]
 *・関数実施トリガーをいったんすべて消す
 *****************************************************/
function deltrigger() {
  const triggers = ScriptApp.getProjectTriggers();
  for(const trigger of triggers){
    if(trigger.getHandlerFunction() == "notifySlack"){
      ScriptApp.deleteTrigger(trigger);
    }
  }
}

/******************************************************************
 * getCalendar()
 * 引数:なし 
 * 返り値:Slack通知内容本文
 * 
 *[処理の説明]
 *・GoogleCalendarより1週間分の予定を取得して本文作成
*******************************************************************/
function getCalendar(){
  //以下にCALENDAR_IDを入力
  const CALENDAR_ID= "              ";

  //calendarIDを元にcalendar情報を取得
  var my_calendar=CalendarApp.getCalendarById(CALENDAR_ID);
  //イベント情報を取得する日を本日に指定
  var start_date=new Date();
  
  //1日のイベントを取得し${SHOW_PERIODS}回繰り返してtextを作成する
  for(var i=0;i<SHOW_PERIODS;i++){
    //今日一日のイベントを配列で取得
    my_events=my_calendar.getEventsForDay(start_date);
    my_events.forEach(function(my_event){//配列の各項目に対して以下の処理を行う
      var title=null; //イベント名
      var mtg_info;   //イベント情報全体
      var mtg_url;    //イベントのURL
      var my_event_description;  //イベントの備考欄
      var event_start_time;       //予定開始時間
      var event_end_time;         //予定終了時間

     //MTGのURLがLocationプロパティに入っているので今回はgetLocation()メソッドを使う
      mtg_url=my_event.getLocation();

      //イベント説明の取得
      my_event_description=my_event.getDescription();

      //イベントの開始・終了時刻を取得、それをフォーマット化する。(Utilities.formatDateをコメントアウトしてLogger.logすれば意味が分かる)
      event_start_time=my_event.getStartTime();
      event_start_time=Utilities.formatDate(event_start_time,"Asia/Tokyo","HH時mm分");
      event_end_time=my_event.getEndTime();
      event_end_time=Utilities.formatDate(event_end_time,"Asia/Tokyo","HH時mm分");      

      title=my_event.getTitle().toString();
      text+="・${month}月${date}日[${day}]の予定\n".replace("${month}",(start_date.getMonth()+1).toString()).replace("${date}",start_date.getDate(). toString()).replace("${day}",day_array[start_date.getDay()].toString());
      text+="`"+title+"`"+"\n";
      text+=" 時間:"+event_start_time+"~"+event_end_time+"\n";

      if(mtg_url!=""){//URLが存在する場合
        text+=" MTG場所:"+mtg_url+"\n";
      }

      if(my_event_description!=""){//説明がある場合
        text+=" [補足事項]\n"
        text+=" "+my_event_description+"\n\n";

        //正規表現をもちいてHTMLタグを消去
        text=text.replace(/(<([^>]+)>)/gi,'');
        text=text.replace(/&nbsp;/gi,"");
        text+="----------------------------------------------------------------------------------------------\n";         
      }else{//説明がない場合
        text+=" [補足事項]\n 無し\n\n"
        text+="----------------------------------------------------------------------------------------------\n";      
      }
    })
    //日付を進める
  start_date.setDate(start_date.getDate() + 1);
  }
      //予定がないとき
    if(text==""){
      text+="この1週間は学部・修士生に関係するイベントはありません";
    }
    //送信するtextの内容を確認する時用の関数
    Logger.log(text);
  return text;
}

/******************************************************************
 * notifySlack()
 * 引数:なし 
 * 返り値:なし
 * 
 *[処理の説明]
 *・getCalendar()関数より帰ってきた本文をそのまま通知する
*******************************************************************/
function notifySlack() {
  deltrigger();
  setTrigger();
  var text=getCalendar();

  var body={
    "username":"Calendar",
    "text":"<!channel>\n\n"+text,
    //自分のSlack時
    "channel":"#random"//好きなチャンネル名に変えてください
  }

  var request={
    "method":"POST",
    "contentType":"application/json",
    "payload":JSON.stringify(body)
  } 

  //自分用のslackのID
  var url="   ";
  
  UrlFetchApp.fetch(url,request);

}

f:id:WMMCsaiteihen:20211202184633p:plain
結果としてSlackにカレンダー情報を1週間分取得して通知してくれるシステムが完成しました。


さて、明日はxfa君の
GMSなんていらん!?」
です。
すまん単語がまずわからん…から何書いてくれるのかわからんけどとりあえずお楽しみに!