Wordpress自動投稿をNode.jsで書いてみた

遂に自分の体重分のベンチプレスをクリアできた「僧職系エンジニア」です。
いや、ホント、どこ目指してるんですかね。。。

前回の投稿はちょっとやり過ぎた感があるので、今回は至ってまじめに書いていきます。

本当はkoaで何か作ってみたがやりたかったんですが、一身上の都合(時間ない)によりこちらになりました。

今回なにやるの?

ほとんどの方は、一度はWordPressを触ったことがあると思います。
ただ、管理画面からポチポチ更新するのが面倒になってくるんですよねー。
そうすると次はPluginを導入して、CSVインポートで一気に投稿するという手段を選択します。
エンジニア以外の方はそれで満足かも知れませんが、エンジニアはそれすら面倒です。
だからその面倒を解消しようぜ!ってのが今回の内容です。

※「いや、私は読者に対して情熱を注いだ投稿を毎回したい!」って人は対象外となります。

仮想シチュエーション

A子さんは猫の動画が大好きで、どうしても色んな人にこの猫の動画を見てもらいたい!と考えました。
大学進学のために上京したばかりで友達もいない中、独学でnode.jsを勉強し、
何とか猫の動画を世界中から集めてDBに保存しました。
持ち前の社交スキルの高さから、友達も増えてきたのですが、
毎日の猫ブログ投稿に時間が取られすぎてしまい、
友達に誘われても「ブログ投稿するから。。。」と断ることが多くなりました。
(このままだと折角の友達が!)と危機感を感じた時に、
どうもXML-RPCと言うものを使えば自動的に投稿出来ると気づいたのでした。

・・・こんな人いねーよ。

事前に必要なもの

  • node.js
  • wordpressを設定している環境

今回は既にDBに猫に関するコンテンツが格納されている前提で話を進めていきます。

実際に作っていく

作るにあたって何が必要かと考えた時に

  • フォルダ構成
  • Libraryのインストール
  • Builder
  • Template
  • フロー制御
  • Task

ぐらいが必要かなと考えたので、これを作っていきましょう!

フォルダ構成

├── builder
│   └── NyanContentBuilder.js  // コンテンツ生成用
└── task
│   └── NyanUploadTask.js // Uploadタスク
└── template
│   └── NyanTemplate.ect // テンプレート
└── uploader
│   └── NyanUploader.js // アップローダー
└── node-wordpress
└── node_modules
└── package.json

Libraryのインストール

package.jsonを作って、使用するライブラリを定義しましょう。 フロー制御にasync リクエストにrequest テンプレートにect を使用します。

{
  "name": "nyan_collection",
  "version": "0.1.0",
  "devDependencies": {
    "request": "^2.34.0",
    "ect": "^0.5.9",
    "async": "^0.2.10",
  }
}

 

$ npm install

今回使うwordpress-xml-rpcのラッパーnode-wordpressはnpmに対応していないので
git cloneでプロジェクトのルートディレクトリに保存してください。

Builder

投稿名、投稿内容は結構変わったりするので、それ専用のクラスを作って管理しましょう。

// NyanContentBuilder.js
var ECT = require('ect');
var renderer = ECT({ root : __dirname + '/../template' });

/**
 * コンストラクタ.
 * @param template
 */
var NyanContentBuilder = function(template){
    this.templete = template;
};

/**
 * コンテンツの作成.
 * @param content object
 * @return String
 */
NyanContentBuilder.prototype.createContent = function(content) {
    var title = "可愛いニャンコ " + content.title;
    var htmlData = {title:title, video_url:content.video_url};
    // ここに関しては次の「template」の項目で説明します。
    var html = renderer.render(this.templete, htmlData);
    return html;
};

/**
 * ポスト名の作成.
 * @param content
 */
NyanContentBuilder.prototype.createPostTitle = function(content) {
    return "今日の可愛いニャンコ " + content.title;
};

module.exports = NyanContentBuilder;

Template

投稿内容は外部に出しておきましょう。 レイアウト等を変更したい場合は、このファイルだけを変更すればおっけーです。

// NyanTemplate.ect</pre>
<div><span><%- @title %></span>
<span>今日の可愛いニャンコだよ〜♪</span>
<video width="320" height="240" src="<%- @video_url %>" controls="controls"></video></div>
<pre>

これでテンプレートは完成しました。
実際にどうやってバインドするかは、先ほどのソースの中で出てきた

// バインドする値を生成.
var htmlData = {title:title, video_url:content.video_url};
// テンプレート名, バインドする値を渡してレンダリング実施
var html = renderer.render(this.templete, htmlData);

の2行で出来ます。

フロー制御

WordPressxml-rpcの仕様上、タグとカテゴリーを追加する際には既にそれが存在しないとエラーになります。
なので投稿する際にタグのチェック、カテゴリーのチェック、投稿のフローを制御する必要があります。
普通に書くとcallbackワロタwになるのでasyncを使用して、我慢出来るレベルまで落とし込みましょう。

// WordPressUploader.js
var wordpress = require('../node-wordpress/lib/wordpress');
var async = require('async');
var request = require('request');
var NyanContentBuilder = require('../builder/NyanContentBuilder');

/**
 コンストラクタ.
 @param param
 */
var WordPressUploader = function(param) {
    this.host    = param.host;
    this.account = param.account;
    this.password = param.password;
    this.callback = param.callback;
};

/**
 アップロードの実行.
 */
WordPressUploader.prototpye.upload = function() {
    // wordpressのxml-rpcラッパーのインスタンス生成.
    var wpc = wordpress.createClient({
        url:this.host,
        username:this.account,
        password:this.password
    });

    // 第2パラメータで色んなもの渡してます。
    // postStateは'draft'を設定すると「下書き」、'publish'は「公開」になります。
    // postDateは公開日になります。未来にすると予約投稿になります。
    _upload(wpc, {
        builder:new NyanContentBuilder('NyanTemplate.ect'),
        postState:'publish',
        postDate:new Date(),
        callback:this.callback});
};

/**
 アップロード処理.
 @param wpc
 @param params
 */
var _upload = function(wpc, params) {
// 今回は静的なコンテンツにしていますが
// ここにDBからデータを取得する処理を追加すれば、そのデータを元に投稿出来ます。
    var datas = [
        {name:"ウズベキスタンの黒猫", "tags":["黒猫", "ウズベキスタン"],categories:["おすすめ"], video_url:"http://xxxx/uz/000001.mp4"},
        {name:"フィリピンの路地裏で見つけたブチ猫", "tags":["ブチ猫", "フィリピン"], categories:["おすすめ2"],video_url:"http://xxxx/uz/000002.mp4"},
        {name:"ロシアのメインストリートで見つけたオッドアイ","tags":["ロシア", "オッドアイ"], categories:["おすすめ3"], video_url:"http://xxxx/uz/000003.mp4"}
    ];
    _uploadFlow(wpc, params, datas);
};

/**
 * タクソノミーのチェック
 *
 * 指定した項目がある場合はそのIDを返し
 * 無い場合はタクソノミーを生成する.
 *
 */
var _checkTerms = function(wpc, taxonomy, datas, callback, results) {
    if(results === undefined) {
        results = {};
    }
    if(!results.hasOwnProperty(taxonomy)){
        results[taxonomy] = [];
    }
    var searchValue = datas.shift();

    // すべての処理が終了した
    if(!searchValue) {
        callback(null, null, results);
        return;
    }

    wpc.getTerms(texonomy,{search:searchValue},function(err, value) {
        if(err) {
            callback(null, "Error:Check Texonomy", null);
            return;
        }
        if(value.length) {
            results[taxonomy].push(value[0].termId);
            // 次の処理.
            checkTagData(wpc, taxonomy, datas, callback, results);
            return;
        }
        wpc.newTerm({
            taxonomy:taxonomy,
            name:searchValue
        },function(err, value){
            if(err) {
                callback(null, "Error:Regist Texonomy", null);
                return;
            }
            if(value) {
                results[taxonomy].push(value);
                // 次の処理.
                checkTagData(wpc, taxonomy, datas, callback, results);
            }
        });
    });
};

/**
 * アップロードのフロー制御.
 */
var _uploadFlow = function(wpc, param, contents) {
    var content = contents.shift();
    if(!content) {
        param.callback(null, null);
        return;
    }
    async.waterfall([
        function(callback) {
            _checkTerms(wpc, "category", content.categories, callback, {});
        },
        function(err, results, callback) {
            // Categoryの登録に失敗
            if(err) {
                callback(null, 'Error:Regist Category');
                return;
            }
            _checkTerms(wpc, "post_tag", content.tags, callback, results);
        },
        function(err, results, callback) {
            // Tagの登録に失敗
            if(err) {
                callback(null, 'Error:Regist Tags');
                return;
            }
            // コンテンツのアップロードを開始
            uploadContent(wpc, param, content, results.category, results.post_tag, callback);
        }
    ], function(err, result) {
            if(err) {
                param.callback(null, null);
                return;
            }
            // アップロードを続ける.
            _uploadFlow(wpc, param, contents);
        }
    );
};

/**
 * コンテンツのアップロードを行う.
 * @param wpc
 * @param param
 * @param content
 */
var uploadContent = function(wpc, param, content, category, tag, callback) {
    var title = param.builder.createPostTitle(content);
    var htmlContent = param.builder.createContent(content);
    var terms = {};
    if(category.length) {
        terms.category = category;
    }
    if(tag.length) {
        terms.post_tag = tag;
    }
    // ここで実際に投稿処理が行われます。
    wpc.newPost({
            title: title,
            status: param.postState,
            content: htmlContent,
            name:title,
            date:param.postDate,
            terms:terms
        },
        function(err, value) {
            if(err) {
                callback(err, "Error:Failed Upload");
                return;
            }
            callback(null, "Info:Success Upload");
        });
};

module.exports = WordPressUploader;

このままではリクエストの間隔が早すぎて色んな所から怒られそうなので
実際にやる場合は適切にSleep等を設定してください。

Task

これでアップロードする手段が整ったので後はTaskを生成しておきましょう。
手動で更新するのが面倒な人はcronなどで定期的にこのTaskを呼び出せば
勝手に更新されていきます。

// NyanUploadTask.js
var wordpress = require('../uploader/WordPressUploader');
var async = require('async');

var UploadTask = function() {
    this.run = function () {
        async.waterfall([
            // 現在はひとつしか設定してないので、waterfall使わなくても問題無いですが
// 将来的に拡張することを想定してこのようにしています。
            function(callback) {
                new WordPressUploader({account:'Acount名', password:'Password', host:'http://hogehoge.com', callback:callback}).upload();
            }
        ],function(err, result) {
            console.log(result);
            console.log("upload finish!");
        });
    }
}
// 実行
new UploadTask().run();
$ node NyanUploadTask.js

これで設定等を間違えない限りは自動的にアップロードされます。

最後に

一からnodeでWordPress投稿機能を実装すると時間がかかりますが、
node-wordpressを使えば実装時間の短縮になります。
ただし、ヘルプが少ないので
何か調べたいときはソースを読んだほうが早いです。

今後、サイトを追加したい場合もBuilder, Template, Uploader, Taskの
変更で複数サイトも同時に管理できます。
今回はあくまでもアップロードするところにフォーカスを当てたのでコンテンツは静的になっていますが
コンテンツを取得するところにDBから取得する用に変更すればいいだけです。
後はDBに自動的に格納するクローラーを生成すれば「完全自動投稿」に昇華します。

使い道はあるのかと言われると微妙ですが、興味があったので今回のネタにしてみました。