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 も使えるので非同期処理のハンドリングは思いどおりに行える
- レスポンスは連想配列で
statusCode
とbody
を渡せば良い - 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 側の設定の説明は色々省く。
- Google Apps Script を使って Slack のスラッシュコマンドを作る
- Google Apps Script を使って Slack から Twitter 投稿を行うスラッシュコマンドを作る
- GAS を使って Slack コマンドが受け取った文字列を別のサーバに POST 送信する
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、コレからも使っていこうと思う。