複数 SNS に一括投稿する Netlify Functions コード例を上げる

かつて使っていたモノを短くまとめた。

Cross Post

Netlify Functions で動かしていたソースコードの簡略版。

に投稿を一括送信する。

の3箇所から受信するための方法も記載。

コードそのままでは動かないので、適宜修正すること。

$ npm run dev
# npx netlify-lambda serve src だと chokidar でエラーが出るので npx は使えない

# ブックマークレットからの GET リクエストを再現する : 日本語は encodeURIComponent 後のモノを渡す
$ curl -X GET "http://localhost:9000/.netlify/functions/cross-post?credential=CREDENTIAL&text=$(node -p "encodeURIComponent('テスト')")"

# IFTTT Webhook からの POST リクエストを再現する
$ curl -X POST --data '{"credential":"FROM-IFTTT","text":"テスト"}' 'http://localhost:9000/.netlify/functions/cross-post'

# Slack Incoming Webhook からの POST リクエストを再現する
$ curl -X POST --data-urlencode 'token=TOKEN' --data-urlencode 'text=テスト' 'http://localhost:9000/.netlify/functions/cross-post'
{
  "name": "cross-post",
  "private": true,
  "scripts": {
    "dev": "netlify-lambda serve src",
    "build": "netlify-lambda build src"
  },
  "devDependencies": {
    "netlify-lambda": "1.6.3"
  }
}
[build]
  command   = "npm run build"
  functions = "dist"
// axios を使わず Node.js 組み込みの http・https で送信する場合
// ====================================================================================================

const http  = require('http');
const https = require('https');

/**
 * リクエストする
 * 
 * @param {string} url URL
 * @param {object} options オプション
 * @return {Promise<*>} Promise
 */
function request(url, options) {
  return new Promise((resolve, reject) => {
    options = options || {};
    const body = options.body || null;
    
    if(options.body) {
      delete options.body;
    }
    
    const agent = url.startsWith('https:') ? https : http;
    
    const req = agent.request(url, options, (res) => {
      res.setEncoding('utf8');
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      })
        .on('end', () => {
          resolve(data);
        });
    })
      .on('error', (error) => {
        reject(error);
      })
      .on('timeout', () => {
        req.abort();
        reject('Request Timeout');
      });
    
    req.setTimeout(8000);
    
    if(body) {
      req.write(body);
    }
    req.end();
  });
}

/**
 * Do Well (オレオレマイクロブログ) に POST する
 *
 * @param text 投稿文字列
 * @return {Promise} 通信結果
 */
function postDoWell(text) {
  return request('http://example.com/index.php', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
    },
    body: new URLSearchParams({
      credential: 'CREDENTIAL',
      text      : text
    }).toString()
  });
}

/**
 * Misskey に POST する
 *
 * @param text 投稿文字列
 * @return {Promise} 通信結果
 */
function postMisskey(text) {
  return request('https://misskey.io/api/notes/create', {
    method: 'POST',
    body: JSON.stringify({
      i   : 'MISSKEY_API_KEY',
      text: text
    })
  });
}

/**
 * Mastodon に POST する
 *
 * @param text 投稿文字列
 * @return {Promise} 通信結果
 */
function postMastodon(text) {
  return request('https://mstdn.jp/api/v1/statuses', {
    method: 'POST',
    headers: {
      'Accept'      : 'application/json, text/plain, */*',
      'Content-Type': 'application/json;charset=utf-8',
      'User-Agent'  : 'my-post-api'  // 必須
    },
    body: JSON.stringify({
      access_token: 'MASTODON_ACCESS_TOKEN',
      status      : text,
      visibility  : 'public'
    })
  });
}


// axios を使う例
// ====================================================================================================

const axios = require('axios');

/**
 * Do Well (オレオレマイクロブログ) に POST する
 *
 * @param text 投稿文字列
 * @return {Promise} 通信結果
 */
function postDoWell(text) {
  const params = new URLSearchParams();
  params.append('credential', 'CREDENTIAL');
  params.append('text'      , text);
  return axios.post('http://example.com/index.php', params);
}

/**
 * Misskey に POST する
 *
 * @param text 投稿文字列
 * @return {Promise} 通信結果
 */
function postMisskey(text) {
  return axios.post('https://misskey.io/api/notes/create', {
    timeout: 8000,
    i      : 'MISSKEY_API_KEY',
    text   : text
  });
}

/**
 * Mastodon に POST する
 *
 * @param text 投稿文字列
 * @return {Promise} 通信結果
 */
function postMastodon(text) {
  return axios.post('https://mstdn.jp/api/v1/statuses', {
    timeout     : 8000,
    access_token: 'MASTODON_ACCESS_TOKEN',
    status      : text,
    visibility  : 'public'
  });
}


// Netlify Functions を想定したエントリポイント
// ====================================================================================================

/**
 * エントリポイント
 *
 * @param event イベント
 * @param _context 未使用
 * @param callback レスポンスを行うコールバック
 * @return statusCode と body を含む連想配列
 */
exports.handler = (event, _context, callback) => {
  // レスポンスの雛形 (通常時は result・エラー時は error プロパティにメッセージを入れる)
  const response = {
    statusCode: 400,
    headers: { 'Content-Type': 'application/json' }
  };
  
  // ブックマークレットから本エントリポイントを GET で呼び出した場合
  const credential = event.queryStringParameters.credential;
  const text       = event.queryStringParameters.text;
  
  // IFTTT の Webhook から本エントリポイントを POST で呼び出した場合
  const params     = JSON.parse(event.body);
  const credential = params.credential;  // IFTTT Webhook から送られてきたモノと判別するためのパラメータを用意しておく
  const text       = params.text;
  
  // Slack Incoming Webhook から本エントリポイントを POST で呼び出した場合
  const params = [...new URLSearchParams(event.body)].reduce((acc, pair) => ({...acc, [pair[0]]: pair[1]}), {});
  const token = params.token;  // Slack Incoming Webhook が送ってくるトークン
  const text  = params.text;
  
  // 適宜パラメータの正当性チェック、例外ハンドリングなどを行うこと
  
  return Promise.all([postDoWell(text), postMisskey(text), postMastodon(text)])
    .then((results) => {
      response.statusCode = 200;
      response.body = JSON.stringify({ result: 'Success' });
      return callback(null, response);
    })
    .catch((error) => {
      response.body = JSON.stringify({ error: 'Failed' });
      return callback(null, response);
    });
};

そのまんまでは動かないので、適宜調整して欲しい。


ブックマークレットとして呼び出せばブラウジング中に一括投稿できるし、Slack や IFTTT と連携するようにしておけば色んなところから一括投稿できるという仕組み〜。

↑ この記事の元ネタ。


拙作の Neo's PHP Micro Blog、Misskey、Mastodon に一括投稿するための Netlify Functions。AWS Lambda でも多分動かせると思う。

axios を使った送信処理部分を、httphttps モジュールで代替するコードは、この Function で実装したモノだった。


詳しい説明はもうしない。よしなにドウゾ。