調べて、学ぶ

検索したり実験したことを覚えとくブログ

googleAPIを駆使するLINEbotサービスを作った #3

前回の続き

2.分解した内容をgoogleカレンダーに登録する

gMailのテキスト取得がうまくいったので、取得した文章をgoogleカレンダーのパラメータとするよう分解していく。

メール例文

予約通知です

施設名: aaaaaaaaa

スタート: 2020年 7月 1日, 水曜日

エンド: 2020年 7月 5日, 日曜日

予約ID: nnnnnnnnnn


受信するメールは定型文(以下)なので、「:」ごとに分解していけば良い。

こんな感じ。(ソースから引用&若干編集)

//メールの文章を分解してパラメータに格納する
//引数 pText:メールのテキスト内容

exports.sliceText = function(pText){
    let textlines = pText.replace(/\r/g,'') //改行ごとに配列化
    let params;
    let cnt;
    let YMD;
    let dt;
    let job = {'dtFrom':'','dtTo':'','youbi':''}; 
    let reserveID;

    //改行ごとにメール文章を
    for(cnt in textlines){
    
      params = textlines[cnt].split(':')
      switch(params[0]){
        case '施設名':
          room = params[1].replace(/ /g,'');
          break;
        case 'スタート':
          dt = params[1].replace('/ /g','').replace('年','-').replace('月','-').replace('日','');// y-m-d, ◯曜日
          YMD = dt.split(',')[0].split('-');
          stay.dtFrom = new Date(YMD[0], Number(YMD[1]) -1 , YMD[2] ,12,00,00);
          break;
          
        case 'エンド':
          dt = params[1].replace('/ /g','').replace('年','-').replace('月','-').replace('日','');// y-m-d, ◯曜日
          YMD = dt.split(',')[0].split('-');
          
          stay.dtTo = new Date(YMD[0], Number(YMD[1]) -1 , YMD[2] ,10,00,00);
          
          //清掃予定(時間は任意で固定)
          jpb.youbi = params[1].split(',')[1].replace(/ /g,'').substr(0,1);
          job.dtFrom = new Date(YMD[0], Number(YMD[1]) -1 , YMD[2] ,10,00,00);
          job.dtTo = new Date(YMD[0], Number(YMD[1]) -1 , YMD[2] ,11,00,00);
          
          
          break;


        case '予約ID':
          console.log(params[1])
          reserveID = params[1].split('ref')[0];
          break;
      }
    }

    //googleカレンダー用のパラメータを返す
    return {'room':room,'job':clean,'reserveID':beds24ID};
};

googleカレンダーについて。 先のsliceText()をよしなに加工して、以下のinsertEents()の引数として渡すとイベントが作成できる。(認証周りはgMailと同じ)

//◆◆GCal予定書き込み◆◆
//◆◆insertEvents()に使用するパラメータ
/* {
  start:{dateTime:'',timeZone:'Asia/Tokyo'}
  ,end:{dateTime:'',timeZone:'Asia/Tokyo'} 
  ,summary:'カレンダーのタイトル'
  ,description:'カレンダーのメモ'
  ,location :'カレンダーの場所'
} */
exports.insertEvents = function(pGcalParam){
  return new Promise((resolve)=>{
    // Authorize a client with credentials, then call the Google Calendar API.
    resolve(authorize(JSON.parse(credDat)))
  }).then((res)=>{
    const oauth = res

    return new Promise((resolve)=>{
      const calendar = google.calendar({version: 'v3', auth:oauth});
      calendar.events.insert({
        calendarId: 'primary',
        resource:pGcalParam,
      }, (err, res) => {
        if (err) return console.log('The API returned an error: ' + err);
        
        //console.log(res.data.id);
        resolve(res);
      });
    })
  })
};

(後々で必要になる)カレンダー内容の更新について。

このLINEbotは「仕事が来たぞ」というお知らせについて「やります」と返事した時にカレンダーの更新(担当者名を書き換える)を行うので、googleカレンダーの更新が必須になる。 APIのリファレンスをぱっと見た感じだとupdate()を使えばいいじゃないの? と思いがちですが、部分的な編集の場合はpatch()を使うらしい。 渡すパラーメータも上のinsertEvents()で使っているものと同じなので、上記の「calendar.events.insert」の部分を「calendar.events.patch」と書き換えるだけでOK。

※一意のイベントに絞り込まなければならないので、カレンダーのIDは必須。 処理のフローとしては、

1.予定の一覧(ID)を取得

2.1をループし、LINEbotから与えられたカレンダーと同じIDが見つかったら内容更新

となる

(ソース抜粋)
//現在の予定一覧を取得(gCalendar.listEvents())し、対象のイベントが未登録であればreplyユーザ名で更新する。
gCalendar.listEvents().then((ev)=>{
    if(ev.length){
        ev.map((ev)=>{
        
        if(ev.id == [LINEbotで募集しているID]){
            //カレンダーのサマリー(担当者を格納している)内容を確認
            if(ev.summary == '未登録'){
            
                //登録
                const gCalParam = {
                    summary: 【LINEのユーザ名】
                    ,id:e.id
                }
            
                entryEvents(gCalParam).then((res)=>{
                    //略
                });

            }else{
                //誰かが登録済み
                let echo='';

                if(ev.summary == 【LINEのユーザ名】){
                    //自分が予約済み
                    //略
                }else{
                    //自分以外が予約済み
                    //略
                }
            }
          }
      });

  }else{
      console.log('no events')
  }
});

function entryEvents(gCalParam){
  return new Promise((resolve)=>{
    // Authorize a client with credentials, then call the Google Calendar API.
    resolve(authorize(JSON.parse(credDat)))
  }).then((res)=>{
    const oauth = res

    return new Promise((resolve)=>{
      const calendar = google.calendar({version: 'v3', auth:oauth});
      
      //patch()で更新。eventIdにカレンダーのIDを指定するのがキモ
      calendar.events.patch({
        calendarId: 'primary',
        eventId:pGcalParam.id,  
        resource:{summary:pGcalParam.summary}
      },(err,res)=>{
        if (err) return console.log('The API returned an error: ' + err);
        
        //console.log(res.data.id);
        resolve(res);      
      })
    })
  })
}

これで「メールを受信」して「カレンダーに登録する(更新する)」ことができた。

#4.LINEの配信・メールの処理へ続く ...続かせるのやめました。

(追記)2020/11/18 気力が持たないし、これからやることもそんなに面白くないのでここで打ち切ることにします。

googleAPIを駆使するLINEbotサービスを作った #2

前回の続き。

LINEbotの問いかけの返事はpostbackで取得できることがわかった。

なので、これからはメインの処理に入る

1.gMailに届くメールを取得して文章を分解する

2.分解した内容をgoogleカレンダーに登録する

3.分解した内容をLINEbotのユーザ(お友達登録した人)にブロードキャスト

4.取得したgMailを「処理済み」トレイに移動

1.gMailに届くメールを取得して文章を分解する

googleAPIのことは知っていたので、「node gmail」でググるとほぼコピペで済む。

今回利用したのはこちら。

Google公式ライブラリを利用してNode.jsからGmailの送受信をしてみよう

…って、あれ、この方は#1のときにbotの手ほどきを書いてくれていた方だ。二度もお世話になっているとは…感謝しまくりですわ。

ここでハマったのが本家のチュートリアル(https://developers.google.com/gmail/api/quickstart/nodejs)で、この内容をソースに貼り付けてメール取得&取得内容の変数を分解しようとすると、うまく行くのに、この内容を別ファイルにして保存・呼び出しするとなぜだか変数がundefinedになっている。

//うまくいかない処理(イメージ)
const mailUtil = require([外部ファイルにしたメール取得処理])

const maillist = mailUtil.getMailList(); //gmailのメール一覧を取得
console.log(maillist.getMailText(maillist[0].id))  //前の処理で得られたメールのIDを元にテキスト取得

//maillist[0].idはなぜかundefinedになる

しばらく「なんで?」と考えた結果気づいたのは、node.jsの1番の特徴「ノンブロッキングIO」。時間のかかる処理(上述のgetMailList()の部分)が終わる前に、次の処理へ行くことを最優先する”非同期”と言われる仕組み。こいつのおかげで、gMailへアクセスしている処理が返って来る前にメイン処理は次へ次へと進んでいた。1番の特徴でありながら、そいつに足をすくわれていた(--;

さて…このサンプルをどうしたら「メールを取得後に処理」できるのか。

 1.ウェイトを挟む

  => ウェイトしてまだ処理済みになってなかったら意味がない  

 2.イベント化してメールを取得したらイベント発火

  => わりと現実味あり。

  調べてみると「イベントエミッター(EventEmitter)」てのがあるみたいだけど、書かれている日付が軒並み「古い」。なんかもっといい方法がある気がする…

 3.”非同期”を”同期”する

 =>これが欲しい。「node 非同期 同期」で調べるとasync/await 、promise()の話が出て来るのでしばらく読んだ結果、「できることは同じ。async/awaitはpromiseの糖衣構文(簡単にしたもの)」とのこと。どうせなら源流に近いほうをやっておきたいので、promise()を使用したら…おぉ、できた。

//別ファイルにした処理(mailUtil.js)
//(要点抜粋)
//◆◆ メールIDの一覧を取得 ◆◆
exports.getMailList=function (){
  return new Promise((resolve)=>{
    resolve(authorize(JSON.parse(credDat))) //チュートリアルにある認証処理の終了待ち(終了したら認証情報が返る)
  }).then((res)=>{   //取得した認証情報を変数resとして利用
    const oauth = res;

    return new Promise((resolve)=>{
      //自身のメールボックスから「受信トレイにある未読メール」を一覧取得
      gmail.users.messages.list({userId:'me',auth:oauth,'labelIds':LabelID_remove},(err,res)=>{
        if(err) return console.error('Error getting message list', err);
        resolve(res.data.messages)
      })
    })
  })
}

exports.getMaiilText = function(pMailid){
  return new Promise((resolve)=>{
    resolve(authorize(JSON.parse(credDat)))
  }).then((res)=>{
    const oauth = res

    return new Promise((resolve)=>{
      gmail.users.messages.get({userId:'me',id:pMailid,auth:oauth},(err,res)=>{
        if(err) console.log('The API returned an error: ' + err)
        
        let ret={'Subject':'','Text':''};
        ret.Subject = res.data.payload.headers.filter(function(elm){return elm.name=='Subject'})[0].value;

        if(res.data.payload.parts[0].body.size == 0){
          ret.Text = 'The mail size 0 byte';
          resolve(ret)

        }else{
          //受け取ったメールテキストはbase64でエンコードされているので、デコードする。
          const b64str = Buffer.from(res.data.payload.parts[0].body.data,'base64');
          ret.Text = b64str.toString('utf-8'); 
          //console.log(res)
          resolve(ret)
        }
      })
    })
  })
}
//前述の処理の改善
const mailUtil = require(./mailUtil.js)

mailUtil.getMailList().then((list)=>{
     //受信トレイのメールIDを取得
     mailUtil.getMailText(list.id).then((mailtext)=>{   
       console.log(mailtext);
     }
})

これで「IDをもとにメールを読む」ができる。

なかなか厄介。

#3へ続く

googleAPIを駆使するLINEbotサービスを作った #1

【概要】困っているトモダチのためにLINEbotを作った。

こんな動きをします。

gmailで届くスケジュールを読み取ってgoogleカレンダーに登録
googleカレンダーに登録した内容をLINEbotから(botを友達登録した人に)お知らせする
・LINEbotからお知らせを受けたユーザは、LINEbotへの返答でgoogleカレンダーを更新する

➡︎メールで届く”担当者不在のredmineチケット”をLINEbot登録者に横流しして、担当者を募るイメージでOKです。

【イメージ】

f:id:docoka_dareka:20200628224101p:plain
botイメージ

【技術的な部分】

今回の仕掛けはユーザの代わりにgoogleAPI(webサービス)を叩くLINEbot(webアプリ)なので、とにかくHTTPを話せる言語が必要。 筆者の手持ちの武器は

  • VB(6.0)とVBAとvbsは呼吸するように書く:今回においては全く使えない
  • VC++(6.0)はソース見ればなんとなく書ける:〃
  • php(4.0)はソース見ればVCよりは書ける:使えなくもないけど辛い
  • html(4.0),css(2.0)はたまにリファレンス見る程度:今回においては不要
  • js(ES5なら)たまにリファレンス見る程度:今回における初めての光明

→年齢が伺われますねぇ

ということで、幸い勉強中だったnode.jsでゴリゴリすることに。


LINEbotのチュートリアルは、やってみた系の話がたくさん見つかります。 雰囲気がわかりやすかったのはここ

Qiita:1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest

これに従うだけでnode.jsによるLINEbot(ユーザの問いかけに”反応”する)が作れるようになります…が、botから問いかけて「ユーザの応答を処理する」方法が書かれてないため、一方的に話す独りよがりなbot(Lv1)になってます。

f:id:docoka_dareka:20200629000504p:plain
botレベル1(返事を受け取れない)
ここでしばらく時間を使ってしまいましたが、調べ続けたら答え発見。

JS初心者がAWS Lambdaで実装するLINE Bot〜「オウム返し」の一歩先〜

なるほど。イベントのtypeが「postback」なものを受け取れば良いのか…

#2へ続く

macからGoogle Homeになんか喋らせたい

【やりたいこと】タイトルのとおり。
これをやるためにはnode.jsが要るので、そもそものところから勉強中(やっては休んで忘れてまたやり直して...)。

基礎的な部分はなんとなくわかったけど、目的達成するためにはインターネットによるカンニング必須。node.jsで喋らせるためにはgoogle-home-notifierが必要で、それをインストールするためにはpythonが必要とのこと。道のり長すぎだわ💦

 

【環境構築】

python環境設定

qiita.com

【実技】

google-home-notifierインストール&おしゃべり設定。

qiita.com

これで実行したら「get key failed from google」のエラー。

どうも、key.jsにキーが設定されてないことが原因らしい。

さらにググると、どうやらgoogle-tts-api (Text-To-Speech)を更新しなくてはいけないらしい。

qiita.comこれの対処1で無事に話し出した。(あとでやり直したら対処2もOKでした)

 

Qiita&Qiitaに投稿してくれた皆様ありがとー!

 

検索ワード:google home, node.js, mac

iPadpro

とある筋から設定を依頼されたiPadpro。

それ自体はすぐ終わったのですが、パソコンにつなごうとして、おや~…と。

自分で使わないと気にもしなかったんですが、本体のコネクタ&ケーブルがUSB-typeCになったのですね。

パソコンにつなげられないじゃないか(typeCの口がないのです)

予定外のお買い物(C-A変換ケーブル)&引き渡しの延長決定~⤵️

office2013のプロセスが終わらない

【困りごと】

ExcelVBAでWordを起動・RTF文書を読んでテキスト形式で保存・終了(Quit)しようとしたら、終了にやたら時間がかかる。デバッグしてみると、Quitの行から次の行に移動するのに分単位。

【前提】

ActiveDirectory環境&オフライン環境(インターネットには出られない)
office2013・office2010のPCが混在。

 

【調べたこと】 

  • 手動でWordを起動・終了すると、画面は消えるけどプロセスは残ったまま。
    かなり待っていると、忘れた頃にプロセスが消える。
  • office2013だから? と思ってたらPCごとに再現したりしなかったり。
  • ググると出てくるのは「Normal.dotを削除すべし」。でも、Normal.dotは肥大化している訳ではない(18KB程度)し、言われた通りに削除して再作成しても現象変わらず。

  •  さらに検索ワードを変えて調べると、以下のブログを見つける。

    Officeアプリケーションのプロセスがなかなか消えない - 前回よりは成長したブログ

    内容は読んでもらうとして、これも欲しい情報ではなかった。(現象解決せず)

  • 悩みながらExcelの画面を見つめているとちらつく、画面右上にあるアカウント名。これ、何のためにあるんだろ…ググると、サインインしている必要はなさそうなので、サインアウトすると、QuitでWordのプロセスが消えた。(お?)
  • サインアウトで解決する=ネットワークのタイムアウト待ち を疑う。
    再度サインインした状態と見比べようとするが、サインインできない。
    なぜならここは「オフライン環境だから」
    レジストリを削除するとサインイン状態になった。
  • サインイン状態でネットワークモニタを起動させながらWordを起動・終了すると接続先を何度も切り替えながら外部に出ようとし(サインアウト情報を送りたい?)、最終的にタイムアウト・プロセス終了 となる。

【オチ】

office2013(おそらくそれ以降)でサインイン状態だから起きている現象。
起動したWordが終了する際にインターネットに出てサインアウト情報送ろうとしたけどできないからリトライを繰り返して遅延する。

【対応】

手動でサインアウト&今後サインインできなくして解決。

サインイン禁止については、以下の「レジストリ設定を使用してユーザーのログオン時に表示する ID の種類を決定する」の欄を参照。(私は4を選びました)

Office 2013 における ID、認証、および承認の概要 | Microsoft Docs

台数多いからグループポリシーで配布。

 

これがわかるのに3日使ったわ…