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 を作り込んでいく。以下の例ではパラメータの存在チェックなどを省いているので、実運用のためにはもう少し手を入れること。

// 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}` };
    });
};

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

環境変数は、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 側の設定の説明は色々省く。

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

// 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);
    });
};

こんな感じ。

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

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

以上

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

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