Corredor

ウェブ、プログラミングの勉強メモ。

Netlify Functions を使って複数の SNS にマルチポストする Function を作った

AWS Lambda とほぼ同等の機能を無料で利用できる、Netlify Functions。

今回はコレを使って、ブックマークレット形式で呼び出せる Function と、Slack の Slash Command として呼び出せる Function を2つ作ってみた。

どちらも、「オレオレマイクロブログ」「Mastodon」「Misskey」など複数の SNS に同じ投稿を行える、マルチポスト機能を実現した Function として作った。

Function を作るにあたっての準備

前回の記事で紹介した Netlify Functions の登録が済んでいていることを前提とする。

今回は POST 時のクエリストリングのパースに querystring、SNS への POST 送信を行うために axios という npm パッケージを使うので、これらをインストールしておく。

$ npm install --save querystring axios

ブックマークレットとして呼び出せるマルチポスト Function

まずはブックマークレットとして呼び出して使う Function を用意してみる。

ブックマークレット側から仕様を決める

ブックマークレットとしては、以下のように任意のテキストを GET パラメータで送信することにする。同時に、credential パラメータを用意することで、簡易的なパスワード認証をかけることにした。

// 現在見ているページのタイトルと URL をマルチポストするブックマークレット
javascript:(d => {
  if(location.href.includes('netlify.app')) {
    return;
  }
  
  open('https://EXAMPLE.netlify.app/.netlify/functions/my-bookmarklet?credential=MY-PASSWORD&text=' + encodeURIComponent(d.title + ' ' + d.URL), '');
})(document);

手前の if 文は、window.open() 後の画面でうっかり多重実行しないようにするためのガード句。実際はコレを1行にして使用する。

ということで、Function 側は credential パラメータと text パラメータを受け取って処理できれば良いことになる。

Function を作る

上述のリクエスト仕様に合わせて、Function を作り込んでいく。以下の例ではパラメータの存在チェックなどを省いているので、実運用のためにはもう少し手を入れること。

  • src/my-bookmarklet.js
// POST 送信を行うために axios を使用する
const axios = require('axios');

/** オレオレマイクロブログに投稿する */
function postMicroBlog(text) {
  // オレオレマイクロブログは JSON ではなく x-www-form-urlencoded 形式で送信する必要があるため、以下のように書く
  const params = new URLSearchParams();
  params.append('api_key', process.env.MICRO_BLOG_API_KEY);  // 環境変数からトークンを読み取るようにしておく
  params.append('text'   , text);
  
  return axios.post('https://example.com/post.php', params)
    .then((result) => {
      console.log('Micro Blog : Success : ', result);
      return { success: 'Micro Blog' };  // 成功したサービス名を返す
    })
    .catch((error) => {
      console.error('Micro Blog : Error : ', error);
      return { failed: 'Micro Blog' };  // 失敗したサービス名を返す
    });
}

/** Misskey に POST する */
function postMisskey(text) {
  return axios.post('https://misskey.io/api/notes/create', {
    i   : process.env.MISSKEY_API_KEY,
    text: text
  })
    .then((result) => {
      console.log('Misskey : Success : ', result);
      return { success: 'Misskey' };
    })
    .catch((error) => {
      console.error('Misskey : Error : ', error);
      return { failed: 'Misskey' };
    });
}

/** Mastodon に POST する */
function postMastodon(text) {
  return axios.post('https://mstdn.jp/api/v1/statuses', {
    access_token: process.env.MASTODON_API_KEY,
    status      : text,
    visibility  : 'public'
  })
    .then((result) => {
      console.log('Mastodon : Success : ', result);
      return { success: 'Mastodon' };
    })
    .catch((error) => {
      console.error('Mastodon : Error : ', error);
      return { failed: 'Mastodon' };
    });
}

/** エントリポイント */
exports.handler = async (event, context, callback) => {
  // クエリ文字列から2つのパラメータを取得する
  const credential = event.queryStringParameters.credential;
  const text       = event.queryStringParameters.text;
  
  // パラメータの存在チェックをするとしたらこんな感じで書く
  if(credential === undefined) {
    return { statusCode: 400, body: 'Credential Is Null' };
  }
  
  // クレデンシャルのチェック (正しくなければ一括送信を行わない)
  if(credential !== process.env.CREDENTIAL) {
    return { statusCode: 400, body: 'Invalid Credential' };
  }
  
  // Promise.all を使って一括送信する
  return Promise.all([ postMicroBlog(text), postMisskey(text), postMastodon(text) ])
    .then((results) => {
      // 投稿に成功したサービス、失敗したサービスの情報をまとめてレスポンス文字列に渡す
      const success = results.filter(r => r.success !== undefined).map(r => r.success).join(', ');
      const failed  = results.filter(r => r.failed  !== undefined).map(r => r.failed ).join(', ');
      console.log(`Success : [${success}]  Failed : [${failed}]`);
      return { statusCode: 200, body: (success ? `Success : [${success}]` : '') + (success && failed ? '  ' : '') + (failed ? `Failed : [${failed}]` : '') };
    })
    .catch((error) => {
      console.error('Error : ', error);
      return { statusCode: 400, body: `Failed To Post : ${error}` };
    });
};

少々長くなったが、こんな風に実装してみた。

  • event.queryStringParameters からクエリ文字列を取得する
    • encodeURIComponent() でパーセント・エンコードした文字列もデコードされて届くので、特に処理せずとも良い
  • SNS への POST は axios を使って普通に…
    • async も使えるので非同期処理のハンドリングは思いどおりに行える
  • レスポンスは連想配列で statusCodebody を渡せば良い
  • API トークンなどは process.env で環境変数を注入するようにしておく

環境変数は、Netlify 管理画面から「Settings」→「Build & deploy」→「Environment」と移動し、「Environment variables」欄にて設定できるので、ココに各種トークンを入れておく。

コレで、現在見ているページのタイトルと URL を複数の SNS にマルチポストするコードができた。ブックマークレットを起動すると Netlify Functions が処理を行い、

Success : [Micro Blog, Misskey, Mastodon]

というように、投稿が成功したサービス名が列挙される。もしも投稿に失敗したサービスがあった時は、

Success : [Micro Blog, Misskey]  Failed : [Mastodon]

というようなレスポンスになるようにしてある。

Slack の Slash Command からマルチポストする

続いて、Slack の Slash Command として作る Webhook。以前、Google Apps Script (GAS) で同様のモノを作ったことがあるので、Slash Command 側の設定の説明は色々省く。

neos21.hatenablog.com

neos21.hatenablog.com

neos21.hatenablog.com

Slack とのやり取りにおいては、POST メソッドが使われること、それでいてクエリ文字列のパースが必要なこと、そしてレスポンスの仕方を工夫しないと上手く Slack 側に応答が返せないことで詰まったので、そこを重点的に紹介する。

  • src/my-slack.js
// POST パラメータをパースするために使用する
const querystring = require('querystring');
// POST 送信を行うために axios を使用する
const axios = require('axios');

// 以下の投稿メソッドはブックマークレット版と同じなので省略する
// --------------------------------------------------

/** オレオレマイクロブログに投稿する */
function postMicroBlog(text) { }

/** Misskey に POST する */
function postMisskey(text) { }

/** Mastodon に POST する */
function postMastodon(text) { }

// --------------------------------------------------


/** エントリポイント */
exports.handler = async (event, context, callback) => {
  // レスポンスの雛形を作っておく
  const response = {
    statusCode: 400,
    headers: { 'Content-Type': 'application/json' }
  };
  
  // POST メソッドでのリクエストでなければ中止する
  if(event.httpMethod !== 'POST') {
    response.body = JSON.stringify({ text: 'Error : Method Not Allowed' });
    return callback(null, response);
  }
  
  // パラメータを event.body から取得し、パースする
  const params = querystring.parse(event.body);
  const token = params.token;  // Slack Webhook のトークン文字列
  const text  = params.text;   // Slash Command で送られてきた投稿文字列
  
  // Slack のトークンチェック
  if(token !== process.env.SLACK_APP_TOKEN) {
    response.body = JSON.stringify({ text: 'Error : Invalid Slack App Token' });
    return callback(null, response);
  }
  // あとは適宜 text 変数の Null チェックなども行っておく
  
  // Promise.all を使って一括送信する
  return Promise.all([ postMicroBlog(text), postMisskey(text), postMastodon(text) ])
    .then((results) => {
      // 投稿に成功したサービス、失敗したサービスの情報をまとめてレスポンス文字列に渡す
      const success = results.filter(r => r.success !== undefined).map(r => r.success).join(', ');
      const failed  = results.filter(r => r.failed  !== undefined).map(r => r.failed ).join(', ');
      console.log(`Success : [${success}]  Failed : [${failed}]`);
      
      // 成功時のレスポンスを組み立ててレスポンス (返答) する
      response.statusCode = 200;
      response.body = JSON.stringify({ text: (success ? `Success : [${success}]` : '') + (success && failed ? '  ' : '') + (failed ? `Failed : [${failed}]` : '') });
      return callback(null, response);
    })
    .catch((error) => {
      console.error('Error : ', error);
      response.body = JSON.stringify({ text: `Error : Failed To Post : ${error}` });
      return callback(null, response);
    });
};
  • POST メソッドかどうかを判定するには event.httpMethod を見る
  • POST 時のクエリパラメータは event.body に入っている。querystring という npm パッケージにパースしてもらう
  • ブックマークレットの場合は credential という自前のリクエストパラメータで簡易認証を入れたが、Slash Command の場合は Slack App Token が飛んでくるので、コレを見る
    • チェックに使用するトークン情報は process.env.SLACK_APP_TOKEN と環境変数で参照しているので、Netlify の設定画面で環境変数を設定しておこう
  • レスポンス時は callback() 関数を使用しないと、正しく Slack 上での返信として扱われない
    • response.body に設定した JSON 文字列内の text プロパティが、Slack 上で応答文として表示されるメッセージテキストとなる

こんな感じ。

あとはコレをデプロイして、

  • https://EXAMPLE.netlify.app/.netlify/functions/my-slack

といたエンドポイント URL を取得したら、Slash Command の送信先に設定してやれば良い。

以上

ホントに AWS Lambda と同じ感覚で利用できるし、メインで操作するのは GitHub などのリポジトリサービスへの Push だけで、めちゃくちゃお手軽。

環境変数の注入など、セキュアな作りにすることもちゃんとできるし、デプロイも Git Push を契機に素早く行われて、とても快適だ。Netlify Functions、コレからも使っていこうと思う。

AWS Lambda実践ガイド

AWS Lambda実践ガイド