One Liner Enabled 2

09/21

Index

if Write Spreadsheet then Tweet

Googleスプレッドシートに簡単につぶやきを入力して
それをHexoで公開ってのを前回紹介した。

実際につかってみて不満に思うことがあったので
それを改善していくことにする。

Makes It Better Than The Former

不満はまずIFTTT。
レシピ次第なのかもしれないけど
GAで長い文章がまず認識されない。
これはちょっと残念だった。

次に、日付。
Pixel 3からSpreadsheetアプリを使って
編集するときに投稿日時を入れるのが面倒。
また、嘘の投稿もできてしまう。

これらの不満のうち、IFTTTはもう諦めた。
ただ投稿日時はなんとかしたい。

What I thought about

そこで、投稿日時をごにょごにょして
Spreadsheet(以下、SS)に書き直すことをしてみる。
SSとの仲介役はone-linerというスクリプト。
one-linerと連携してHexoに渡すスクリプトがhexo-generate。
one-linerはHexo投稿用のmdファイルを生成して
後続のhexo-generate起動のトリガとなるファイルを
Google Drive(以下、GD)に保存する。
hexo-generateはGDのトリガファイルの有無をチェック、
有ったらsourceディレクトリにコピーしてhexo gを実行する。

簡単なシーケンスがこちら。

sequenceDiagram
  participant pc1 as PC/Smartphone
  participant SS as Google Spreadsheet
  participant prog1 as one-liner
  participant GD as Google Drive
  participant prog2 as hexo-generate
  participant prog3 as Hexo

  pc1 ->> SS : Edit
  loop Execute every 2minutes
    prog1 ->>+ SS : Get tweet data
    SS -->>- prog1 : Return tweet data
    alt updated?
      Note over prog1 : Continue
    else Nothing changed?
      Note over prog1 : Quit
    end
    prog1 ->> prog1 : Generate tweet data
    prog1 ->>+ GD : Create md file
    GD -->>- prog1 : Created
    prog1 ->>+ GD : Create trigger file
    GD -->>- prog1 : Created
    prog1 ->>+ SS : Update spreadsheet
    SS -->>- prog1 : Updated
  end
  loop Execute every 2minutes
    prog2 ->>+ GD : Check trigger file exists
    GD -->>- prog2 : Return result
    alt Exist?
      Note over prog2 : Continue
    else No file Exists
      Note over prog2 : Quit
    end
    prog2 ->>+ GD : Get md file
    GD -->>- prog2 : Return md file
    prog2 ->>+ prog3 : Kick hexo g command
    prog3 ->> prog3 : Execute hexo g
    prog3 -->>- prog2 : Generated
  end

SSのフォーマットは以下の通り。

Post Date Passcode Post Content
2019-5-25 00:55:00 1234 で これはテストです。

なんで投稿内容に「で 」が入っているかというと
IFTTT経由で投稿するとなぜか「で 」が入るから。
投稿日時のところに「1」を入れたら正確な日付を
入れて書き直すようにしたい。

リアルタイムでチェックして全セルを書き直してしまうので
何行もつぶやきたいときはちょっと注意が必要かも。

Fix one-liner

前回までは、SSのセルからデータを
取得するだけだったが、今回は投稿日付を
整形して書き戻してやることをしてみる。

あ、あと、前回はセルの値を取得するだけだったが
今回は書き込みを行うのでscopeを変更する必要がある。
https://www.googleapis.com/auth/spreadsheets.readonly
としてたところを、
https://www.googleapis.com/auth/spreadsheets
とした。

あとはトークンを一度削除して、認証しなおせばよい。

/**
 *
 * index.js
 *
 */
//constants
require('date-utils');
const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');


const postpass='1234';
const sep1=',';
const sep2='at';
const datesep='-'
const timesep=' ';
const replacedatespec='{{ date }}';
const linefeed="\n";
const mdh2='## ';

const BaseDir = '[HEXO DIRECTORY]';
const gdriveDir = '[GOOGLEDRIVE DIRECTORY]';
const templateMdFile = BaseDir + '/source/_drafts/[TEMPLATE MD FILE]';
const productionMdFile = BaseDir + '/source/_posts/[MD FILE]';
const hexoGenerateFile = gdriveDir + '[TRIGGER FILEPATH]';

const spreadSheetId='[SPREADSHHET ID]';
const spreadSheetRange='シート1!A1:C'; //for me

//const SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
const SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];
const TOKEN_PATH = 'token.json';

/**
 *
 * main
 *
 */
// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
    if (err) return console.log('Error loading client secret file:', err);
    // Authorize a client with credentials, then call the Google Sheets API.
    authorize(JSON.parse(content), updateTweet);
});


/**
 *
 * functions
 *
 */

// Optimize post contents
function setDescFormat(str){
    try {
        var desc=str.substring(2);
    } catch (err) {
        console.log(err.name + ': ' + err.message);
        desc="";
    }
    desc=desc.replace(/([^a-z0-9A-Z]) ([^a-z0-9A-Z])/g, "$1 $2");
    return desc;
}

// Validate password
function setPassFormat(str){
    let isValid=false;
    let validatestr=('0000'+str).slice(-4);
    //console.log(validatestr);
    if(validatestr==postpass){
        isValid=true;
    }
    return isValid;
}

// Optimize date format
function setDateFormat(dt){
    let postdate;
    if(dt=='1'||dt==1||dt==''){
        postdate=new Date();
    }else if(dt.indexOf(' at ')>-1){
        //date format is 'May 26, 2019 at 05:30PM'
        let year=dt.substring(dt.indexOf(', ') + 2, dt.indexOf(' at '));
        let month=dt.substring(0, 3);
        let day=dt.substring(4, dt.indexOf(','));
        let hour=dt.substring(dt.indexOf(' at ') + 4, dt.indexOf(' at ') + 6);
        let minute=dt.substring(dt.indexOf('AM') - 2, dt.indexOf('AM'));
        if(minute==''){
            minute=dt.substring(dt.indexOf('PM') - 2, dt.indexOf('PM'));
            if(hour=='12'){
            }else{
                hour=hour*1+12;
            }
        }else{
            if(hour==12){
                hour='00';
            }
        }
        let second='00';
        let dateformat=month+' '+day+",  "+year+" "+hour+":"+minute+":"+second+" GMT+09:00";
        postdate=new Date(dateformat);
    }else{
        postdate=new Date();
        let year=dt.substring(dt.indexOf("'") + 1, dt.indexOf('-'));
        let datebuf=dt.substring(dt.indexOf('-') + 1, dt.indexOf(' '));
        let month=datebuf.substring(0, datebuf.indexOf('-'));
        let day=datebuf.substring(datebuf.indexOf('-') + 1);
        let hour=dt.substring(dt.indexOf(' ') + 1, dt.indexOf(':'));
        let minute=dt.substring(dt.indexOf(':') + 1, dt.indexOf(':') + 3);
        let second=dt.substring(dt.indexOf(':') + 4, dt.indexOf(':') + 6);
        let dateformat=month+' '+day+",  "+year+" "+hour+":"+minute+":"+second+" GMT+09:00";
        postdate=new Date(dateformat);
    }
    return postdate;
}

// is filespec exist?
function checkFileExist(filespec){
    let isExist = false;
    try{
        fs.statSync(filespec);
        isExist = true;
    }catch(err){
        isExist = false;
    }
    return isExist;
}

/**
 * Create an OAuth2 client with the given credentials, and then execute the
 * given callback function.
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback to call with the authorized client.
 */
function authorize(credentials, callback) {
    const {client_secret, client_id, redirect_uris} = credentials.installed;
    const oAuth2Client = new google.auth.OAuth2(
        client_id, client_secret, redirect_uris[0]);

    // Check if we have previously stored a token.
    fs.readFile(TOKEN_PATH, (err, token) => {
        if (err) return getNewToken(oAuth2Client, callback);
        oAuth2Client.setCredentials(JSON.parse(token));
        callback(oAuth2Client);
    });
}

/**
 * Get and store new token after prompting for user authorization, and then
 * execute the given callback with the authorized OAuth2 client.
 * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
 * @param {getEventsCallback} callback The callback for the authorized client.
 */
function getNewToken(oAuth2Client, callback) {
    const authUrl = oAuth2Client.generateAuthUrl({
        access_type: 'offline',
        scope: SCOPES,
    });
    console.log('Authorize this app by visiting this url:', authUrl);
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });
    rl.question('Enter the code from that page here: ', (code) => {
        rl.close();
        oAuth2Client.getToken(code, (err, token) => {
            if (err) return console.error('Error while trying to retrieve access token', err);
            oAuth2Client.setCredentials(token);
            // Store the token to disk for later program executions
            fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
                if (err) return console.error(err);
                console.log('Token stored to', TOKEN_PATH);
            });
            callback(oAuth2Client);
        });
    });
}

// Generate tweet data
function updateTweet(auth) {
    const sheets = google.sheets({version: 'v4', auth});
    sheets.spreadsheets.values.get({
        spreadsheetId: spreadSheetId,
        range: spreadSheetRange,
    }, (err, res) => {
        if (err) return console.log('The API returned an error: ' + err);
        const rows = res.data.values;
        let tweetData={date:[], valid:[], description:[], pass: [], rawdata: [], rawdate: [] };
        if (rows.length) {
            rows.map((row) => {
                //console.log(`${row[0]}, ${row[1]}, ${row[2]}`);
                let datebuf=setDateFormat(row[0]);
                let nowdate=new Date();
                if(row[0]<=nowdate||datebuf<=nowdate){
                    tweetData.date.unshift(datebuf.toLocaleString('ja-JP', {timezone: 'JST' }));
                    tweetData.valid.unshift(setPassFormat(row[1]));
                    tweetData.description.unshift(setDescFormat(row[2]));
                    tweetData.rawdata.unshift(row[2]);
                    tweetData.rawdate.unshift(row[0]);
                    tweetData.pass.unshift(row[1]);
                }
            });
            if(tweetData.date.length>0){
                doPost(tweetData, auth);
            }
        } else {
            console.log('No data found.');
        }
    });
}

// Update tweet data set to spreadsheet
function doUpdate(tweetData, auth){
    const sheets = google.sheets({version: 'v4', auth});
    let values = [];
    let resource = {};
    let idx=0
    for(let i=tweetData.rawdata.length-1;i>=0;i--){
        values[idx] = [
            tweetData.date[i],
            ('0000'+tweetData.pass[i]+'').slice(-4),
            tweetData.rawdata[i]
        ]
        idx++;
    }
    resource = {values};
    sheets.spreadsheets.values.update({
        spreadsheetId: spreadSheetId,
        range: spreadSheetRange,
        valueInputOption: 'RAW',
        resource,
    }, (err, res) => {
        if (err) return console.log('The API returned an error: ' + err);
        console.log('updated');
    });
}

// Generate template file and create trigger file
function doPost(tweetData, auth){
    if(checkFileExist(templateMdFile)){
        fs.readFile(templateMdFile, (err, databuf)=>{
            let postbuf='';
            for(let i=0;i<20;i++){
                if(tweetData.valid[i]){
                    databuf+=linefeed
                        +mdh2
                        +tweetData.date[i]
                        +linefeed
                        +linefeed
                        +tweetData.description[i]
                        +linefeed;
                }
            }
            databuf=databuf.replace(replacedatespec, tweetData.date[0]);
            fs.readFile(productionMdFile, (err, postbuf)=>{
                if(postbuf==databuf){
                    console.log('nothing was updated.');
                }else{
                    fs.writeFile(productionMdFile, databuf, ()=>{
                        console.log('production file['+productionMdFile+'] was generated.');
                        fs.writeFile(hexoGenerateFile, '', ()=>{
                            console.log('hexo trigger file was generated.');
                        });
                    });
                    doUpdate(tweetData, auth);
                }
            });
        });
    }else{
        console.log('template file['+templateMdFile+'] was not found.');
        return false;
    }
}

で、Google DriveとのやりとりはAPIを使ってない。
ここもAPIでできたらかっこいいな。
ここではサーバにディレクトリとしてマウントできる
google-drive-ocamlfuseを使っている。
なので普通のファイル操作で実装している。

あと、forever使ってサービスとして動かすこともしていない。
cronでグルグル回すことにしてる。


コメント: