ぼっちしてたのでQiitaで似たような思考を持つ人を探す方法を考えてみた

# 独りぼっちは、寂しいもんな・・・
最近絶賛ぼっち満喫してます。
最近はぼっち過ぎて、ぼっち生活を充実するぼっち充というワードがあるようです。
ちなみに別ワードにソロ充というものがあるらしいのですが、
こちらは友達がいるけど一人行動する人たちのことらしいです。
（~~友達いるのに単独行動するとかぼっちの敵~~）

決して・・・寂しくなんてないんだからね！

# Qiitaで仲間（似たような思考の人）を探す
~~そもそもQiitaで探すなという話ですが~~
そんなわけで[Qiita API](https://qiita.com/api/v2/docs)なるものがあるようなので
Qiita投稿記事から自分の投稿と似たような投稿をする人を
フォローするという~~はた迷惑な~~便利な機能を作ってみました。
次のような流れでフォロー対象を探します。

1. 自分の投稿内容からタグ抽出
2. ユーザを探す
3. 投稿なしのユーザはスキップ
4. ユーザの投稿を取得
5. 自分と同じタグをつけてる投稿があるユーザならフォロー
6. １〜５を１時間おきに１０００リクエスト上限超えないようにcronで呼び出し

# ソースコード
NodeJSで実装しています。
cronを使っているのでcron,timeモジュールは必要です。

```package.json
{
  "name": "qiita",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cron": "^1.1.0",
    "time": "^0.11.4"
  }
}
```

ソース本体です。クーロンにより、毎時（x時０分０秒）に定期実行されます。
Qiitaトークンはご自分のQiitaアカウントのものに差し替えてください。

```qiitaFollower.js
var http = require('http');
var querystring = require('querystring');

// qiita認証トークン
var token = '<Qiitaトークン>';

// search
var pageStart = 0;
// 検索ページ数上限
var pageSearchMax = 50;

var isJSON = function(arg) {
    arg = (typeof arg === "function") ? arg() : arg;
    if (typeof arg  !== "string") {
        return false;
    }
    try {
        arg = (!JSON) ? eval("(" + arg + ")") : JSON.parse(arg);
        return true;
    } catch (e) {
        return false;
    }
};

function qiitaAPI(api,method,callback){
    

    var options = {
        hostname: 'qiita.com',
        port: 80,
        path: api,
        method: method,
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + token
        }
    };

    
    setTimeout(function(){
        var req = http.request(options, function (res) {

            var body = '';
            res.setEncoding('utf8');

            res.on('data', function (chunk) {
                body += chunk;
            });

            res.on('end', function () {
                callback(null,body);
            });

        }).on('error', function (e) {
            console.log(e);
            callback(e,null);
        });

        //req.write('');
        req.end();
    }, 500);    // Qiitaサーバに負荷かけすぎない

}


// 自分の投稿を取得
function myposts() {
    var deferred = Promise.defer();

    var path = '/api/v2/authenticated_user/items';

    path += '?' + querystring.stringify({
        'page': 1,
        'per_page': 100,
    });

    
    // 非同期関数 
    qiitaAPI(path,'GET', function(err, data){
       if (err) {
         return deferred.reject(err); // .catch
       }
       deferred.resolve(data); // then
    });
    
    return deferred.promise; 
}



// ユーザ取得
function users(page) {
    var deferred = Promise.defer();

    var path = '/api/v2/users';

    path += '?' + querystring.stringify({
        'page': page,
        'per_page': 100,
    });
    
    // 非同期関数 
    qiitaAPI(path,'GET', function(err, data){
       if (err) {
         return deferred.reject(err); // .catch
       }
       deferred.resolve(data); // then
    });
    
    return deferred.promise; 
}



// ユーザの投稿を取得
function userpost(id) {
    var deferred = Promise.defer();

    var path = '/api/v2/users/' + id + '/items';

    path += '?' + querystring.stringify({
        'page': 1,
        'per_page': 100,
    });

    qiitaAPI(path,'GET',function(err,data){
        if(err !== null){
            return deferred.reject(err); 
        }
        deferred.resolve(data); 
    });
    
    return deferred.promise; 
}

// Qiitaユーザをフォロー
function follow(id) {
    var deferred = Promise.defer();

    var path = '/api/v2/users/' + id + '/following';
    console.log("path:",path);

    // 非同期関数 
    qiitaAPI(path,'PUT', function(err, data){
        if (err) {
            return deferred.reject(err); // .catch
        }
        deferred.resolve(data); // then
    });

    return deferred.promise; 
}

function followQiitaUsers(){
    // 自分の投稿のタグ
    var mytags = {};
    // ユーザ
    var findusers = [];
    // フォローユーザ
    var followUsers = [];

    // 自分の投稿取得
    myposts()
    .then(function (data) {


        // 自分の投稿記事のタグを取得
        var datas = JSON.parse(data);
        
        // 1時間1000リクエスト上限超えた
        if(datas.type === 'rate_limit_exceeded'){
            throw new Error(datas.message);
        }
        
        for(var i=0;i<datas.length;++i){
            var tags = datas[i].tags;
            for(var j=0;j<tags.length;++j){
                mytags[tags[j].name] = tags[j].name;
            }

        }   
        console.log("mytags:",mytags);


        var pages = [];
        for(var i=pageStart;i<pageStart+pageSearchMax;++i){
            pages.push(i);
        }

        // 他のユーザ情報取得
        return pages.reduce(function (promise, param) {
            return promise.then(function () {
                return users(param).then(function(data){
                    var datas = JSON.parse(data);
                    // 1時間1000リクエスト上限超えた
                    if(datas.type === 'rate_limit_exceeded'){
                        throw new Error(datas.message);
                    }
                    console.log("user page",i,":",datas);

                    Array.prototype.push.apply(findusers,datas);
                });
            });
        }, Promise.resolve());

    })
    .then(function(){


        var users = [];
        for(var i=0;i<findusers.length;++i){
            var id = findusers[i].id;
            var items_count = findusers[i].items_count;
            // プロフィール画像
            var profile_image_url =  findusers[i].profile_image_url;
            // ホームページ
            var website_url = findusers[i].website_url;
            // 投稿がある人限定
            if(items_count > 0){
                users.push({id:id,image:profile_image_url,homepage:website_url});
            }
        }
        console.log("users:",users);


        // ユーザ投稿情報取得
        return users.reduce(function (promise, param) {
            return promise.then(function () {
                return userpost(param.id).then(function(data){
                    var datas = JSON.parse(data);
                    // 1時間1000リクエスト上限超えた
                    if(datas.type === 'rate_limit_exceeded'){
                        throw new Error(datas.message);
                    }                    
                    
                    console.log("user id:",param.id);
                    var usertags = [];
                    // ユーザの投稿内容取得
                    for(var i=0;i<datas.length;++i){
                        var tags = datas[i].tags;
                        for(var j=0;j<tags.length;++j){
                            usertags[tags[j].name] = tags[j].name;
                        }
                        console.log("title:",datas[i].title);
                    }   
                    console.log("usertags:",usertags);

                    // 自分と同じ投稿タグを持っているか？
                    for(var mytag in mytags){
                        // 持っていたらフォロー対象
                        if(usertags[mytag]){
                            followUsers.push(param);
                            break;
                        }
                    }                            

                    return Promise.resolve();
                }).catch(function(err){
                    console.log("err:",err);
                    return Promise.reject(err);
                });
            });
        }, Promise.resolve());


    })
    .then(function(){
        console.log("followUsers:",followUsers);

        return followUsers.reduce(function (promise, param) {
            return promise.then(function () {
                return follow(param.id).then(function(data){
                    try{
                        if(isJSON(data)){
                            var datas = JSON.parse(data);
                            // 1時間1000リクエスト上限超えた
                            if(datas.type === 'rate_limit_exceeded'){
                                throw new Error(datas.message);
                            }
                            if(datas.type === 'forbidden'){
                                console.log("すでにフォロー済み:",param.id);   
                            }
                        }
                    }catch(err){
                    }
                })
                .catch(function(err){
                    console.log("err:",err);
                    return Promise.reject(err);
                });
            });
        }, Promise.resolve());
    })
    .then(function(){

        console.log("------end-----");
        pageStart += pageSearchMax;// 成功したら次の検索時は新しいユーザを探す
        console.log("pageStart:",pageStart);
    })   
    .catch(function(err){
        console.log("------err-----");
        console.log("err:",err);
    });
}



/////////////////// cronによる定期実行 //////////////////////

var cronJob = require('cron').CronJob;

// 毎時間実行
var cronTime = "0 0 * * * *";

// 一度だけ実行したい場合、Dateオブジェクトで指定も可能
// var cronTime = new Date();

var job = new cronJob({
    //実行したい日時 or crontab書式
    cronTime: cronTime,

    //指定時に実行したい関数
    onTick: function() {
        console.log('---------------- Cron Call --------------------');
        followQiitaUsers();
    },

    //ジョブの完了または停止時に実行する関数 
    onComplete: function() {
        console.log('onComplete!');
    },

    // コンストラクタを終する前にジョブを開始するかどうか
    start: false,

    //タイムゾーン
    timeZone: "Asia/Tokyo"
});

//ジョブ開始
job.start();
//ジョブ停止
//job.stop();

// テスト
//followQiitaUsers();

```

package.jsonとqiitaFollower.jsを同フォルダに入れて
下記コマンドで実行できまする（まぁ、NodeJSやってる人だったらわかりますわね・・・）

```
$npm install
$node qiitaFollower.js
```

サーバに設置しておくと勝手にフォローしてくれて幸せになれるかも
app.jsとかでrequireするだけで設置できます。

```app.js
require("./qiitaFollower");
```

# 改良案？
投稿内容も取得できるので、
特定の投稿内容に反応してフォロー対象と見なしてもいいかもしれません。
他にもフォローユーザが何かしら投稿したら賑やかしにいいねしたり~~自動コメント(やめれ)したり~~とか

# あれぇ・・・？
/api/v2/usersという全ユーザ取得のAPIがあるのですが
page=100
per_page=100
くらいでBad Requestとなり、エラーとなりました。
（ということは１万人くらいしか取得できないということに・・・）

# 余談
この記事見てフォロー返し頂ければ、泣いて喜びます。
関係ないですが土日は特に用がなくとも秋葉原とかでブラブラしてたりします。
~~秋葉原で僕と握手~~