datsukan blog
🐦

ブログ記事公開の一定時間後にTwitterへ自動でつぶやく仕組みを作った

この記事は投稿してから1年以上が経過しています。

つくったもの

ブログ記事を新たに執筆してCMSに投稿すると、ホスティング先への反映を待ってからTwitterへ記事の公開について自動的にツイートする仕組みを作りました。
せっかくなので内容を記事として公開してみました。

対象としている読者

ある程度Webの開発に慣れている方、AWSの操作に慣れている方を想定して記事を記述しています。
それなりには詳細に記述しているので、それ以外の方でもある程度参考になると思います。

記事の投稿からツイートまでの流れ

  1. ブログの記事を書く
  2. 記事の情報を管理しているCMSへアップロードする
  3. ホスティングしているサービスなどへ追加記事の反映を行う
  4. Twitterで記事の公開について自動でツイートする

記事の投稿からツイートまでの流れ

仕組みの概要

ホスティングサービスなどでビルドが進んでいる間、CMSへのアップロードに連動してAWS上の処理が動きます。
API Gatewayで用意したREST APIにてリクエストを受け付け、Step Functionsにてホスティング用のビルド時間を待機します。
待機時間が過ぎると、リクエスト時に受け取った記事情報を使ってLambdaの処理でTwitterへの投稿が行われます。

ブログ記事の公開をTwitterへ連携するシステムの構成図

Step Functionsを採用している理由

ホスティング先への反映が瞬時に完了する場合、API GatewayとLambdaだけを使って同期処理でTwitterへツイートすることも可能です。
しかし私のブログではGatsbyを使ったSSGを採用しているため、数分かけてブログのビルドを行う必要があります。
API GatewayやLambdaを単純に使うとSleepを使用した際にタイムアウトするため、Step Functionsを使って非同期処理にしています。

作成手順

流れ

  1. Twitterへの投稿を行うための認証情報の生成を行う
  2. Twitterへの投稿を行うLambda関数を作成する
  3. Step FunctionsのWorkflowを作成する
  4. API GatewayでREST APIを作成する
  5. CMSで新規記事公開時のWebhookを設定する

Twitterへの投稿を行うための認証情報の生成を行う

開発者アカウントの作成

投稿したいTwitterアカウントでログインした状態でTwitter開発者プラットフォームのDeveloper Portalにアクセスしてください。
表示される指示に従って開発者アカウントを作成します。

項目内容
Twitter Account初期状態で入力済み
Email初期状態で入力済み
What country are you based in?Japan
What's your use case?Build customized solutions in-house
Will you make Twitter content or derived information available to a government entity or a government affiliated entity?No

Twitter DevelopersのSingUpで入力する内容

情報の入力をして進むとポリシーへの同意を求められるので確認の上同意します。

この際Twitterアカウントに電話番号を設定していないとエラーになるので、設定していない場合は先にしておきます。

ポリシーに同意するとメールアドレス宛に認証メールが送信されるので、メールのリンクから認証を行います。

Appの作成と認証情報の生成

認証メールを開くとAppの作成を促されるので、Appの名前を入力します。
Blog published notificationとしておきますが、任意で設定してください。

Appの作成

Appを作成するとAPIを使うための各種認証情報が生成されるので、後で使えるようにメモしておきます。

AppのAPI用の各種認証情報

ダッシュボードから先程作成したPROJECT APPApp settings(歯車アイコン)を選択して設定画面を開きます。
設定画面でKeys and tokensタブを選択します。
Authentication Tokensという項目のAccess Token and Secretで、Generateボタンをクリックします。
表示された情報は後で使えるようにメモしておきます。

投稿権限を設定する

  1. 設定画面のSettingsタブを選択する
  2. User authentication not set upSetupボタンをクリックする
  3. App permissionsRead and writeにする
  4. Type of AppWeb App, Automated App or Botにする
  5. App infoCallback URI / Redirect URLにブログのURLを末尾スラッシュ無しで入力する(例:https://blog.datsukan.me
  6. App infoWebsite URLにブログのURLを末尾スラッシュ有りで入力する(例:https://blog.datsukan.me/
  7. Saveボタンをクリックする
  8. Changing permissions might affect your AppのダイアログでYesをクリックする

ここまででTwitterの設定は完了しました。

Twitterへの投稿を行うLambda関数を作成する

Goで処理を記述する

Lambda関数として動かす処理を作成します。
今回は言語にGoを採用します。
Twitter APIの操作にはmichimani/gotwiを採用しました。

main.go
package main

import (
	"context"
	"flag"
	"fmt"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/joho/godotenv"
	"github.com/michimani/gotwi"
	"github.com/michimani/gotwi/tweet/managetweet"
	"github.com/michimani/gotwi/tweet/managetweet/types"
)

// Request は、リクエスト情報の構造体。
type Request struct {
	Token string `json:"token"`
	Slug  string `json:"slug"`
	Title string `json:"title"`
}

func main() {
	t := flag.Bool("local", false, "ローカル実行か否か")
	slug := flag.String("slug", "", "ローカル実行用の記事Slug")
	title := flag.String("title", "", "ローカル実行用の記事Title")
	flag.Parse()

	isLocal, err := isLocal(t, slug, title)
	if err != nil {
		fmt.Println(err.Error())
		return
	}

	if isLocal {
		fmt.Println("local")
		r := Request{
			Slug:  *slug,
			Title: *title,
		}
		localController(r)
		return
	}

	fmt.Println("production")
	lambda.Start(controller)
}

// isLocal は、ローカル環境の実行であるかを判定する。
func isLocal(t *bool, slug *string, title *string) (bool, error) {
	if !*t {
		return false, nil
	}

	if *slug == "" {
		fmt.Println("no exec")
		return false, fmt.Errorf("ローカル実行だがSlug指定が無いので処理不可能")
	}

	if *title == "" {
		fmt.Println("no exec")
		return false, fmt.Errorf("ローカル実行だがTitle指定が無いので処理不可能")
	}

	return true, nil
}

// localController は、ローカル環境での実行処理を行う。
func localController(r Request) {
	if err := godotenv.Load(); err != nil {
		fmt.Println(err)
		return
	}

	if err := useCase(r); err != nil {
		fmt.Println(err.Error())
	}

	fmt.Println("tweetしました。")
}

// controller は、API Gateway / AWS Step Functions / AWS Lambda 上での実行処理を行う。
func controller(r Request) error {
	if r.Token != os.Getenv("API_TOKEN") {
		return fmt.Errorf("unauthorized access")
	}

	if r.Slug == "" {
		return fmt.Errorf("slug is empty")
	}
	if r.Title == "" {
		return fmt.Errorf("title is empty")
	}

	if err := useCase(r); err != nil {
		return err
	}

	return nil
}

// useCase は、アプリケーションのIFに依存しないメインの処理を行う。
func useCase(r Request) error {
	in := &gotwi.NewClientInput{
		AuthenticationMethod: gotwi.AuthenMethodOAuth1UserContext,
		OAuthToken:           os.Getenv("GOTWI_ACCESS_TOKEN"),
		OAuthTokenSecret:     os.Getenv("GOTWI_ACCESS_TOKEN_SECRET"),
	}

	c, err := gotwi.NewClient(in)
	if err != nil {
		return err
	}

	t := fmt.Sprintf("新しいブログ記事を投稿しました🐣\n\n「%s」\n%s%s", r.Title, os.Getenv("BLOG_URL"), r.Slug)
	if _, err := tweet(c, t); err != nil {
		return err
	}

	return nil
}

// tweet は、指定されたテキストをツイートする。
func tweet(c *gotwi.Client, text string) (string, error) {
	p := &types.CreateInput{
		Text: gotwi.String(text),
	}

	res, err := managetweet.Create(context.Background(), c, p)
	if err != nil {
		return "", err
	}

	return gotwi.StringValue(res.Data.ID), nil
}
  • Request構造体に記述している通り、{"token": "xxxx", "slug": "xxxx", "title": "xxxx"}というJsonで記事情報を受け取る想定
    • CMSのWebhook → API Gateway → Step Functions → Lambda とデータを受け渡す際にこの形式になるようにする
  • 動作確認用にローカル環境でも実行できるように制御を用意している
  • 新しいブログ記事を投稿しました🐣...となっている箇所は投稿メッセージを組み立てている
    • 必要なら任意の内容に変更して使用する

実装のサンプル↓

ローカル環境で動作確認する

main.goと同じ階層に.envファイルを作成して、下記の通り環境変数を設定します。
なお、外部に漏洩するとまずい情報なので、.gitignore.envを設定することを推奨します。

.env
GOTWI_API_KEY=[Twitter開発者プラットフォームでAppを作成した際に生成された`API Key`]
GOTWI_API_KEY_SECRET=[Twitter開発者プラットフォームでAppを作成した際に生成された`API Key Secret`]
GOTWI_ACCESS_TOKEN=[Twitter開発者プラットフォームの`Access Token and Secret`で、`Generate`した際の`Access Token`]
GOTWI_ACCESS_TOKEN_SECRET=[Twitter開発者プラットフォームの`Access Token and Secret`で、`Generate`した際の`Access Token Secret`]

BLOG_URL=[自分のブログのFQDN(例:`https://blog.datsukan.me/`)]
API_TOKEN=[認証用のトークンを独自に生成して設定する]

main.goがあるディレクトリでCLIからgo run main.go -local -slug=xxxx -title=hogeを実行します。
slugtitleの値は任意の値で置き換えてください。
うまく行けば自身のTwitterアカウントでツイートが行われているはずです。
ここでは記事のURLがhttps://[ドメイン]/[記事のSlug]の形式であることを前提にしているので、異なる場合は環境変数のBLOG_URLの値、もしくはメッセージ部分を都合に合わせて書き換えてください。

AWS上にLambda関数を作成する

  1. AWSの管理コンソールへログインする
  2. Lambdaの関数のページへアクセスする
  3. 関数の作成ボタンをクリックする
  4. 関数名を入力する(例:blog-published-tweet
  5. ランタイムをGo 1.xにする
  6. ほかは変えずに関数の作成ボタンをクリックする
  7. 作成された関数のページから各種設定を行う
    1. ランタイム設定を編集して、ハンドラblog-published-tweetにする(任意の名称で良いが、Goの実行ファイル名に合わせる)
    2. 設定環境変数を編集して、「ローカルで動作確認する」の際に.envに設定した内容を登録する
    3. 処理を登録する(blog-published-tweetは任意の実行ファイル名でも可)
      1. Goで処理を記述するで作成した処理をgo build -o blog-published-tweet main.goでビルドする
      2. 生成されたblog-published-tweetをZip圧縮する
      3. 管理コンソールの関数のページでコードソースアップロード元を選択して、.zip ファイルから先程圧縮したファイルをアップロードして保存する

Lambda関数をテスト実行する

管理コンソールの関数のページで、テストタブを開いてイベント JSONを下記の値にしてテストボタンをクリックしてください。

{
  "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "slug": "xxxx",
  "title": "hoge"
}
  • sulgtitleのvalueは任意の値に置き換えてください
  • tokenのvalueは環境変数のAPI_TOKENに設定した値にしてください

ローカルでの動作確認と同じようにTwitterへツイートが行われていれば成功です。

Step FunctionsのWorkflowを作成する

  1. AWSの管理コンソールでStep Functionsのページへアクセスする
  2. ステートマシンの作成ボタンをクリックする
  3. ワークフローを視覚的に設計を選択する
  4. タイプ標準を選択する
  5. 次へボタンをクリックする
  6. Workflow Studioが表示されるので、workflowを作成していく
    1. 左上の検索欄でChoiceと検索して、一番上に出てきたアイテムをworkflow上のStartの下にドラッグ&ドロップする
    2. 右側の設定欄にあるChoice RulesRule #1の編集ボタンをクリックする
    3. 展開された中のAdd conditionsボタンをクリックする
    4. 下記の通り入力して条件を保存するボタンをクリックする
      項目内容
      Not指定なし
      Variable$.revision
      Operatoris equal to
      Value (Type)Number constant
      Value1
    5. 左上の検索欄でPassと検索して、出てきたアイテムをworkflow上のChoiceDefaultの分岐にドラッグ&ドロップする
    6. 右側の設定欄にある次の状態最後に移動を選択する
    7. 左上の検索欄でWaitと検索して、出てきたアイテムをworkflow上のChoice$.revision == 1の分岐にドラッグ&ドロップする
    8. 右側の設定欄にあるの設定値をブログのビルド時間より長くする(例:ビルドに4~6分かかるので、600secondsに設定)
    9. 左上の検索欄でLambda Invokeと検索して、出てきた中からAWS Lambda Invokeのアイテムをworkflow上のWaitの下にドラッグ&ドロップする
    10. 右側の設定欄にあるAPI パラメータFunction nameから、先程作成したLambda関数を選択する(手順の記述通りならblog-published-tweet
  7. 右上の次へボタンをクリックする
  8. 生成されたコードを確認のページで次へボタンをクリックする
  9. ステートマシン名を入力する(例:blog-published-tweet
  10. ステートマシンの作成ボタンをクリックする

Workflowの完成イメージ

API GatewayでREST APIを作成する

実行ロールの作成

  1. AWSの管理コンソールでIAMのページへアクセスする
  2. 左のメニューのアクセス管理からロールを選択する
  3. 右上のロールを作成ボタンをクリックする
  4. 信頼されたエンティティタイプAWSのサービスを選択する(デフォルト)
  5. ユースケース他の AWS のサービスのユースケースからAPI Gatewayを選択する
  6. 次へボタンをクリックする
  7. 許可を追加のページで次へボタンをクリックする
  8. ロール名を入力する(例:blog-publish-tweet-apigateway-stepfunctions
  9. ロールを作成ボタンをクリックする
  10. 作成したロールの詳細ページを開く
  11. 許可ポリシー許可を追加ポリシーをアタッチを選択する
  12. AWSStepFunctionsFullAccessを検索してアタッチする

APIの作成

  1. AWSの管理コンソールでAPI Gatewayのページへアクセスする
  2. 右上のAPIを作成ボタンをクリックする
  3. REST API構築ボタンをクリックする
  4. プロトコルを選択するRESTを選択する(デフォルト)
  5. 新しい API の作成新しい APIを選択する(デフォルト)
  6. 名前と説明API 名を入力する(例:blog-published-tweet
  7. APIの作成ボタンをクリックする

エンドポイントの作成

  1. APIのページでリソースアクションからメソッドの作成を選択する
  2. 表示された選択欄でPOSTを選択する
  3. 統合タイプAWS サービスにする
  4. AWS リージョンap-northeast-1にする
  5. AWS サービスStep Functionsにする
  6. HTTP メソッドPOSTにする
  7. アクションの種類アクション名の使用にする(デフォルト)
  8. アクションStartExecutionを入力する
  9. 実行ロールに前段で作成したロールのARNを入力する
  10. コンテンツの処理パススルーにする(デフォルト)
  11. 保存ボタンをクリックする
  12. 作成したメソッドのページから、統合リクエストのリンクをクリックする
  13. マッピングテンプレートのアコーディオンを展開する
  14. リクエスト本文のパススルーテンプレートが定義されていない場合 (推奨)にする
  15. マッピングテンプレートの追加をクリックする
  16. Content-Typeapplication/jsonを入力する
  17. 作成(チェックマークの)ボタンをクリックする
  18. 下部に表示された入力欄に下記のJSONを記述する
    {
        "input": "$util.escapeJavaScript($input.json('$'))",
        "stateMachineArn": "[前段で作成したStep FunctionsのステートマシンのARN]"
    }
    
  19. 保存ボタンをクリックする
  20. リソースアクションからAPIのデプロイを選択する
  21. デプロイされるステージ[新しいステージ]にする
  22. ステージ名を入力する(例:v1
  23. デプロイボタンをクリックする
  24. デプロイ後のステージのページに表示されているURL の呼び出しのURLをメモしておく

WebAPIの動作確認

エンドポイントの作成でデプロイ時にメモしたURLを使って下記のようにリクエストを行ってください。

POST [メモしたURL]
Content-Type: application/json

{
    "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "slug": "xxxx",
    "title": "hoge",
    "revision": 1
}
  • curl、Postman、VC Codeの拡張機能:Rest Clientなど、好きなクライアントで実行してください
  • sulgtitleのvalueは任意の値に置き換えてください
  • tokenのvalueは環境変数のAPI_TOKENに設定した値にしてください

Twitterへツイートが行われていれば成功です。

CMSで新規記事公開時のWebhookを設定する

今回はCMSにContentfulを使っているケースを記載します。

  1. Contentfulへログインする
  2. 上部メニューからSettingsを選択し、さらにWebhooksを選択する
  3. 右上のAdd Webhookをクリックする
  4. 下記の通り入力する
    項目内容
    Name任意の名称
    URLPOST & 前段のAPI Gatewayのデプロイで生成されたURL
    ActiveON
    TriggersSelect specific triggering events
    Content EventsEntryPublishだけ選択
    Other API Events選択なし
    FiltersContent Type IDのequalsで記事のContent Modelのnameを入力する(例:article
    Content typeapplication/json
    PayloadCustomize the webhook payload(※JSONは後述)
  5. 右上のSaveボタンをクリックする

Payloadに設定するJSON

{
  "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "slug": "{ /payload/fields/slug/ja }",
  "title": "{ /payload/fields/title/ja }",
  "revision": "{ /payload/sys/revision }"
}
  • tokenのvalueは環境変数のAPI_TOKENに設定した値にしてください
  • 記事のContent Modelでslugtitleを定義している前提です
  • Localeの設定でja部分が変わります
    デフォルトだとenになります

あとは実際にContentful上の記事をPublishすると、時間差でTwitterへつぶやかれるはずです。
他のCMSでここまで細かく設定できない場合は、大雑把にWebhookで通知させて、必要な通知だけ今回作ったWebAPIをコールさせるように中間でフィルタリングする処理が必要かもしれません。

おわり

設定内容は多いですが、作りたかった形になったので良かったです。
こういったワークフローをWeb上で簡単に作成できるサービスはいくつかあるのですが、融通が効かなったり課金が必要だったりします。
安く運用したい場合や最適化したい場合は今回のようなやり方はアリだと思います。

コメント
 
URLをコピーしました
Profile picture
datsukan

24歳。埼玉県在住。東京都のSaaS企業でバックエンドエンジニアとして勤務しています。

© 2022 datsukan