Qiitaの自分の投稿にLGTMが付いたら通知してもらう


自分が投稿した記事に、どなたかがLGTM(いいね)してくれると、やっぱりうれしくて、書いたかいがあったなあと、元気をもらえます。

そこで、30分ごとに、LGTM(いいね)数とフォロワー数をウォッチして、増えていたら、LINEに通知と、自宅にあるGoogleHomeスマートスピーカにしゃべってもらおうと思います。
コロナの影響でずっと在宅勤務なので、ちょっとしたアクセントにもなります。

ちなみに、GoogleHomeスマートスピーカは、これから立ち上げるcron実行PCと同じネットワークにある必要があります。

#通知の準備

LINE通知には、LINE Notifyを使いました。
そのためには、パーソナルアクセストークンが必要です。

以下にアクセスして、アクセストークンの発行(開発者向け)をします。
　https://notify-bot.line.me/my/

グループに発行してもよいですし、1:1でLINE Notifyから通知を受け取る でもよいです。
そうすると、43文字程度のパーソナルアクセストークンが発行されます。あとで、使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/d6bba911-e4f5-8c3c-0b01-6baa737bf352.png)

Google Homeスマートスピーカからしゃべってもらうためには、スマートスピーカのIPアドレスが必要です。
スマートスピーカと連携済みのAndroidのGoogle Homeアプリから、デバイス設定を選ぶと、一番下の情報 というところに、IPアドレスがありますのでメモっておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7f69d4f3-440e-a6c9-edd9-649151685d8d.png)

#現在のLGTM(いいね)数とフォロワー数の取得

Qiita APIを見てみたのですが、記事ごとのLGTM(いいね)数は取得できますが、合計数をとれるようなAPIは見当たりませんでした。
そこで、QiitaのWebページからスクレーピングします。

スクレーピングには、npmのcheerioを使いました。
また、Webページの取得には、node-fetchを使いました。

> npm install cheerio
> npm install node-fetch

class=” UserCounterList__UserCounterItem-sc-******” のような感じのHTMLエレメントがあるので、その近辺を探しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/46bb22b0-6101-ec2f-4533-1cdcb634c860.png)

(2020/10/10 修正)
と思ったら、取得したHTMLのmeta情報に、投稿数・LGTM数・フォロワー数がありました。なのでそこのスクレイピングに変更しました。

ソースはこんな感じ。

```js:index.js
const cheerio = require('cheerio');
const fetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');

async function get_qiita_state(userid){
	return do_get('https://qiita.com/' + userid, {})
	.then((text) =>{
	  const $ = cheerio.load(text);
/*
	  var state = {};
	  var root = $("[class^='UserCounterList__UserCounterItem-sc-']");
	  root.each((i, elem) =>{
	    var label = $(elem).children("[class^='UserCounterList__UserCounterItemLabel-sc-']").text();
	    var num = parseInt($(elem).children("[class^='UserCounterList__UserCounterItemCount-sc-']").text());
	    state[label] = num;
	  });
*/
		var description = $("meta[name='description']").attr("content");
		var description_list = description.split(' ');
		
		var state = {
			Items: parseInt(description_list[1]),
			Contributions: parseInt(description_list[3]),
			Followers: parseInt(description_list[5]),
		};
		
	  // Posts, Contributions, Followers
	  console.log(state);
	  
	  return state;
	});
}

function do_get(url, qs) {
  var params = new URLSearchParams(qs);
  var url2 = new URL(url);
  url2.search = params;

  return fetch(url2.toString(), {
      method: 'GET',
    })
    .then((response) => {
      if (!response.ok)
        throw 'status is not 200';
      return response.text();
    });
}
```

関数get_qiita_state()の呼び出しにより、

```
　{ Posts : 投稿数, Contributions : LGTM数, Followers : フォロワー数 }
```

が返ってきます。

#LINE通知する

関数line_notifyに、通知したいメッセージと、さきほどのパーソナルアクセストークンを指定します。

```js:index.js
const fetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');
const Headers = fetch.Headers;

function line_notify(message, token){
  var params = {
      message: message
  };
  return do_post_urlencoded_token('https://notify-api.line.me/api/notify', params, token);
}

function do_post_urlencoded_token(url, params, token) {
  const headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token });
  var body = new URLSearchParams(params);

  return fetch(new URL(url).toString(), {
      method: 'POST',
      body: body,
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw 'status is not 200';
      return response.json();
    })
}
```

#GoogleHomeスマートスピーカにしゃべらせる

関数homeSpeechに、メッセージと、スマートスピーカのIPアドレスを指定すると、数秒後にスマートスピーカがしゃべります。

以下のnpmモジュールを使います。
・castv2-client
・google-tts-api

```js:index.js
const Client = require('castv2-client').Client;
const MediaReceiver = require('castv2-client').DefaultMediaReceiver;
const googletts = require('google-tts-api');

function homeSpeech(text, host) {
  return googletts(text, 'ja-JP', 1)
    .then(function(url) {
      return playUrl(url, host);
    });
}

function playUrl(url, host) {
  return new Promise((resolve, reject) => {
    var client = new Client();
    client.connect(host, () => {
      client.launch(MediaReceiver, (err, player) => {
        if( err ){
          console.log('Error: %s', err.message);
          client.close();
          return reject(err);
        }

        var media = {
          contentId: url,
          contentType: 'audio/mp3',
          streamType: 'BUFFERED'
        };
        player.load(media, { autoplay: true }, (err, status) =>{
          client.close();
          resolve('Device notified');
        });
      });
    });

    client.on('error', (err) =>{
      console.log('Error: %s', err.message);
      client.close();
      reject(err);
    });
  })
}
```

# 本体

以上の関数を呼び出すメイン部です。
いくつか、ちょっと特殊処理を入れています。

LGTM数とフォロワー数は、以前のチェック時からの増加をみて、LINEとスマートスピーカから通知をしています。以前のチェック時の数は、ファイルに保存しています。
ファイル名として、QIITA_STATE_FILE を指定し、読み出しと更新用の関数をstate_read、state_updateを用意しました。

また、夜中に通知されても困るので、現在時刻を見て、NOTIFY_RANGE_HOURS の配列に示した時間の時だけ通知するようにしました。

```js:index.js
const fs = require('fs');

const QIITA_USERID = process.env.QIITA_USERID || '【チェックしたいQiitaユーザ名】';
const GOOGLE_DEVICE_ADDRESS = process.env.DEVICE_ADDRESS || '【GoogleHomeスマートスピーカwのIPアドレス】';
const QIITA_STATE_FILE = process.env.QIITA_STATE_FILE || './data/qiita_state.json';
const LINE_PERSONAL_ACCESS_TOKEN = process.env.LINE_PERSONAL_ACCESS_TOKEN || '【LINE Notifyのパーソナルアクセストークン】';
const NOTIFY_RANGE_HOURS = process.env.NOTIFY_RANGE || [ 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 0, 1 ]

get_qiita_state(QIITA_USERID)
.then(async (state) =>{
  var prev_state = state_read();

  var date = new Date();
  var hour = date.getHours();

  if( NOTIFY_RANGE_HOURS.includes(hour) ){
    state_update(state);
    
    var message = "";
    if( state.Contributions > prev_state.Contributions  )
      message += 'いいねが' + (state.Contributions - prev_state.Contributions ) + '件増えたよ。';
    if( state.Followers > prev_state.Followers  ){
      if( message != "" )
        message += "\n";
      message += 'フォロワーが' + (state.Followers - prev_state.Followers ) + '人増えたよ。';
    }
  
    if( message != "" ){
      await homeSpeech(message, GOOGLE_DEVICE_ADDRESS);
      await line_notify("\n" + message, LINE_PERSONAL_ACCESS_TOKEN);
    }else{
      console.log('no change');
    }
  }
})
.catch(error =>{
  console.log(error);
});

function state_read(){
  try{
    return JSON.parse(fs.readFileSync(QIITA_STATE_FILE, 'utf8'));
  }catch(error){
    return {};
  }
}

function state_update(state){
  fs.writeFileSync(QIITA_STATE_FILE, JSON.stringify(state), 'utf8');
}
```

ということで、以下を環境に合わせて変更してください。

・QIITA_USERID：チェックしたいQiitaユーザ名
・GOOGLE_DEVICE_ADDRESS：GoogleHomeスマートスピーカwのIPアドレス
・QIITA_STATE_FILE：現在のLGTM数の保存先ファイル名
・LINE_PERSONAL_ACCESS_TOKEN：LINE Notifyのパーソナルアクセストークン
・NOTIFY_RANGE_HOURS：通知されてもよい時間(時)の配列

#cron化

シェルスクリプトを作成します。
以下例です。

> vi index.sh
> chmod +x index.sh

```
#!/bin/sh

cd /home/XXXXX/projects/node/cron_lgtm
/home/XXXXX/.nvm/versions/node/v8.12.0/bin/node index.js
```

あとはcronに登録するだけです。以下は30分ごとにチェックする場合です。

> crontab -e

0,30 * * * * /home/XXXXX/projects/node/cron_lgtm/index.sh


以上