※2019年5月19日追記: プログラムをリファクタリング&特定階層下の複数記事送信に対応したコードを追加
オープンソースの Wiki システム GROWI の API を利用して、特定のページを定期的にメール送信してみます。
環境
- Windows 10 Pro 1809 / Windows Subsystem for Linux (Bash on windows) Ubuntu 16.04.4 LTS
- たぶんMacでも一緒
- TypeScript: 3.4.5
- Serverless Framework: 1.33.2
-
GROWI: 3.4.6
- 予め growi-docker-compose を使って ConoHa VPS 上で起動しておいた GROWI を使います
また、AWSの下記サービスを利用します
- AWS Lambda: API取得とメール送信プログラムを実行します
- Amazon SES: メールを送信します
- Amazon CloudWatch: 今回は、Lambdaを定期的に実行するために利用します
GROWI の準備
- 「ユーザー設定」→「API設定」→「現在のAPI Token」の値をメモしておく
- 「管理」→「セキュリティ設定」→「ゲストユーザーのアクセス」を「閲覧のみ許可」にしておく
GROWI には現在APIのドキュメントは無いようですが、どのような物があるかはルーティングファイルから推測できます。今回用いる記事単体のAPIは /_api/pages.get?access_token=(アクセストークン)&path=(パス)
というURLになります。実際にアクセスしてみると、ページの情報がJSONで取得できることを確認しておきます。GETなのでブラウザで直接叩くだけで良いです。
AWS SES の利用準備
E メールアドレスの検証 - Amazon Simple Email Service に沿って、メールアドレスを認証しておきます。
Serverless Framework の利用準備
Serverless Framework をインストールしておきます。
$ npm install --global serverless
デプロイするために、 Serverless Framework - AWS Lambda Guide - Credentials に沿って認証情報を設定しておきます。必要に応じて awslabs/git-secrets もインストールしておきましょう。
Serverlessプロジェクトの作成
準備ができたら、 Serverless のコードを書いていきます。
$ serverless create --template aws-nodejs-typescript --path growi-to-mail
$ cd growi-to-mail
$ npm install --save-dev
$ npm install --save aws-sdk
Git 管理する場合は、お好みで下記も実行しておきます。
$ git init
$ curl https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore > .gitignore # お好みで
$ echo 'package-lock.json binary' >> .gitattributes # お好みで
$ git remote add origin https://github.com/USERNAME/REPOSITORY.git # GitHubに上げるなら
デプロイの設定
serverless.yml
にデプロイ用の設定を記述していきます。
service:
name: growi-to-mail
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs8.10
region: us-west-2
iamRoleStatements:
- Effect: 'Allow'
Action:
- ses:SendEmail
Resource: '*'
functions:
main:
handler: handler.main
events:
- schedule: cron(0/3 * * * ? *)
schedule
の設定にはcron記法が利用できます。詳細は Serverless Framework のドキュメントや Amazon CloudWatch のドキュメントを参照してください。
特定の単一ページをメール送信するプログラム
下記のようなプログラムで、GROWIの特定ページをメール送信できます。
import * as AWS from 'aws-sdk';
import * as https from 'https';
const aws_region = 'us-west-2';
const mail_to = ['受信用E-mailアドレス'];
const mail_from = '送信用E-mailアドレス';
const growi_url = 'GROWIのURL 例: https://demo.growi.org/';
const growi_access_token = 'メモしたアクセストークン';
const growi_pages_path = '送信したいパス';
export const main = async (event, context) => {
const ses = new AWS.SES({region: aws_region});
const page = await getGrowiPage(growi_pages_path);
return await ses.sendEmail({
Source: mailFrom,
Destination: { ToAddresses: mailTo },
Message: {
Subject: { Data: page['page']['path'] },
Body: { Text: { Data: page['page']['revision']['body'] } }
}
}).promise();
}
function getGrowiPage(growi_pages_path: string) {
return new Promise(function(resolve, reject) {
const url = `${growi_url}_api/pages.get?access_token=${growi_access_token}&path=${growi_pages_path}`;
https.get(url, (resp) => {
let data = '';
resp.on('data', (chunk) => { data += chunk; });
resp.on('end', () => {
resolve(JSON.parse(data));
});
});
});
}
デプロイ
$ serverless deploy
これで、上記設定なら3分に1度メールが飛んでくると思います。
また、デプロイしたlambdaは下記コマンドで即時実行できます。
$ serverless invoke --function main
片付け
下記コマンドで、デプロイしたlambda等を削除できます。
$ serverless remove
特定階層以下にあるすべてのページをメール送信するプログラム
/_api/pages.list
APIを使うことで、指定した階層以下にあるすべてのページのパスが取得できます。取得したパスを/_api/pages.get
で叩いていくことで、特定階層以下にあるすべてのページのメール送信が実現できます。
import * as AWS from 'aws-sdk';
import * as https from 'https';
const aws_region = 'us-west-2';
const mail_to = ['受信用E-mailアドレス'];
const mail_from = '送信用E-mailアドレス';
const growi_url = 'GROWIのURL 例: https://demo.growi.org/';
const growi_access_token = 'メモしたアクセストークン';
const growi_pages_path = '送信したいパス';
export const main = async (event, context) => {
const ses = new AWS.SES({region: aws_region});
const pageList = await getGrowiList(growi_pages_path);
let contents = [];
for (const pagePath of pageList['pages']) {
// TODO: 環境のキャパシティに応じて調整してください
const page = await getGrowiPage(pagePath['path']);
contents.push(page['page']['revision']);
}
const title = `${growi_pages_path}の一覧`;
const body = contents.reduce((accumulator, revision) => {
const time = new Date(revision['createdAt']);
// TODO: 記事のフォーマットは適当に変更してください
const article = `
--------------------------------
${revision['path']}
--------------------------------
${time.getFullYear()}年${time.getMonth()}月${time.getDate()}日 ${revision['author']['name']}
${revision['body']}
url: ${growi_url}${revision['path']}
`;
return `${accumulator}${article}`;
}, `${title}
================================================================
`);
return await ses.sendEmail({
Source: mailFrom,
Destination: { ToAddresses: mailTo },
Message: {
Subject: { Data: title },
Body: { Text: { Data: body } }
}
}).promise();
}
function getGrowiAPI(api: ('pages.get' | 'pages.list'), growi_pages_path) {
return new Promise(function(resolve, reject) {
const url = `${growi_url}_api/${api}?access_token=${growi_access_token}&path=${growi_pages_path}`;
http.get(url, (resp) => {
let data = '';
resp.on('data', (chunk) => { data += chunk; });
resp.on('end', () => {
resolve(JSON.parse(data));
});
});
});
}
function getGrowiPage(growi_pages_path: string) {
return getGrowiAPI('pages.get', growi_pages_path);
}
function getGrowiList(growi_pages_path: string) {
return getGrowiAPI('pages.list', growi_pages_path);
}
APIを多く叩くことになるので、環境に応じて負荷を下げるためにsleepを入れたり、逆にスペックに余裕がある環境なら並列実行したりして調整してください。
また記事の量が多い場合、AWS Lambdaのタイムアウト(現在は15分)に引っかかる可能性があります。その場合は最終的に、APIで取得した結果をS3に配置していくLambdaと、完成したS3からメールを送信するLambdaに分ける、といった事が必要になるのではないかと思います。