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でグルグル回すことにしてる。