3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-18

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

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

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

#通知の準備

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

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

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

image.png

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

image.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

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

ソースはこんな感じ。

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に、通知したいメッセージと、さきほどのパーソナルアクセストークンを指定します。

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

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 の配列に示した時間の時だけ通知するようにしました。

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

以上

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?