AWS Lambda + Java で Twitter に画像付きツイートを定期的に投稿する

## 概要
- AWS Lambda + Java 8 で画像付きツイートを定期的に投稿するサンプルコードを書く
- 用意した背景画像に現在日時を日本語で描画する
- Twitter への投稿は Twitter4J ライブラリを使用する
- 定期的な実行は Amazon CloudWatch Events を使用する

## 画像への日本語描画には Google Noto Fonts を使用
- AWS Lambda + Java 上では日本語が使えるフォントが用意されていない
  - 参考: [AWS Lambda \+ Java で日本語 \(ひらがな・カタカナ・漢字\) を使えるフォントがあるか調べる \- Qiita](https://qiita.com/niwasawa/items/3c88364841fb43771eb3)
- Google Noto Fonts の Noto Sans CJK JP Regular フォントを組み込んで使用する
  - フォントファイル NotoSansCJKjp-Regular.otf のサイズは約16.4MB
  - 参考: [Noto CJK – Google Noto Fonts](https://www.google.com/get/noto/help/cjk/)

## ソースコード

### ファイル一覧

- pom.xml : Maven 用ビルド設定ファイル
- ImageTweet.java : メインの処理を実行する Java クラス
- NotoSansCJKjp-Regular.otf : 日本語の文字を含むフォントファイル
- fukidashi.png : Twitter に投稿する画像の背景画像ファイル

```
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── ImageTweet.java
        └── resources
            ├── font
            │   └── NotoSansCJKjp-Regular.otf
            └── image
                └── fukidashi.png
```

### ImageTweet.java

```java
package com.example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import twitter4j.Status;
import twitter4j.StatusUpdate;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.conf.ConfigurationBuilder;

import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

public class ImageTweet implements RequestHandler<Map<String, Object>, Map<String, Object>> {

  /**
   * スタンドアローンの Java アプリケーションとして実行した場合のエントリポイントです。
   *
   * @param args 先頭の要素[0]にツイートする文字列を持つ配列
   */
  public static void main(String[] args) {
    Map<String, Object> output = new ImageTweet().invoke(new HashMap<String, Object>());
    System.out.println(output);
  }

  /**
   * AWS Labmda 関数を実行した場合のエントリポイント (ハンドラーメソッド) です。
   *
   * @param input   入力データ
   * @param context AWS Lambda Context オブジェクト
   * @return 出力データ
   */
  @Override
  public Map<String, Object> handleRequest(Map<String, Object> input, Context context) {
    return invoke(input);
  }

  /**
   * ツイートします。
   *
   * @param input ツイートするテキスト
   */
  public Map<String, Object> invoke(Map<String, Object> input) {
    Map output = new HashMap<String, Object>();
    try {
      // ツイートするテキストを組み立てる
      String text = LocalDateTime.now(ZoneId.of("JST", ZoneId.SHORT_IDS))
        .format(DateTimeFormatter.ofPattern("y年M月d日 H時m分s秒"));

      // 画像ファイルのパス
      String imagePath = "image/fukidashi.png";
      // 画像ファイル名
      String imageFileName = new File(imagePath).getName();
      // 画像フォーマット名
      String imageFormatName = "png";
      // 画像ファイルを読み込む
      BufferedImage image = getImageFromResource(imagePath);

      // フォントファイルを読み込む
      String fontPath = "font/NotoSansCJKjp-Regular.otf";
      Font font = getFont(fontPath);

      // 画像を加工する (テキストを描画)
      Graphics2D g = image.createGraphics();
      g.setColor(Color.BLUE);
      g.setFont(font.deriveFont(30.0f));
      g.drawString(text, image.getWidth() / 4, image.getHeight() / 2);
      g.dispose();
      // 画像のデータを取得
      InputStream imageBody = createInputStream(image, imageFormatName);

      // 環境変数を取得する
      Map<String, String> env = System.getenv();

      // ツイートする
      TwitterFactory tf = getTwitterFactory(env);
      Status status = tweet(tf, text, imageFileName, imageBody);

      // 出力データを構築
      output.put("result", status);
    } catch (Exception e) {
      output.put("error", e);
    }

    return output;
  }

  /**
   * フォントファイルを読み込みます。
   *
   * @param path フォントファイルのパス
   * @return フォントオブジェクト
   * @throws FontFormatException 必要なフォントデータが無い場合
   * @throws IOException         何らかのエラーが発生した場合
   */
  private Font getFont(String path) throws FontFormatException, IOException {
    ClassLoader cl = getClass().getClassLoader();
    try (InputStream input = cl.getResourceAsStream(path)) {
      // Java の Font オブジェクトのデフォルトサイズは12
      return Font.createFont(Font.TRUETYPE_FONT, input).deriveFont(12.0f);
    }
  }

  /**
   * 画像ファイルを読み込みます。
   *
   * @param path 画像ファイルのパス
   * @return 画像オブジェクト
   * @throws IOException 何らかのエラーが発生した場合
   */
  private BufferedImage getImageFromResource(String path) throws IOException {
    ClassLoader cl = getClass().getClassLoader();
    URL url = cl.getResource(path);
    return ImageIO.read(url);
  }

  /**
   * 画像データのバイト列を取得します。
   *
   * @param image      画像オブジェクト
   * @param formatName 画像フォーマット名
   * @return 画像データのバイト列
   * @throws IOException 何らかのエラーが発生した場合
   */
  private InputStream createInputStream(BufferedImage image, String formatName) throws IOException {
    ByteArrayOutputStream output = new ByteArrayOutputStream();
    ImageIO.write(image, formatName, output);
    return new ByteArrayInputStream(output.toByteArray());
  }

  /**
   * TwitterFactory オブジェクトを取得します。
   *
   * @param env 環境変数
   * @return TwitterFactory オブジェクト
   */
  private TwitterFactory getTwitterFactory(Map<String, String> env) {
    ConfigurationBuilder cb = new ConfigurationBuilder();
    // Twitter 認証情報を環境変数から取得する
    cb.setDebugEnabled(true)
      .setOAuthConsumerKey(env.get("TWITTER_CONSUMER_KEY"))
      .setOAuthConsumerSecret(env.get("TWITTER_CONSUMER_SECRET"))
      .setOAuthAccessToken(env.get("TWITTER_ACCESS_TOKEN"))
      .setOAuthAccessTokenSecret(env.get("TWITTER_ACCESS_TOKEN_SECRET"));
    return new TwitterFactory(cb.build());
  }

  /**
   * 画像付きツイートを投稿します。
   *
   * @param text          ツイートするテキスト
   * @param imageFileName ツイートに含める画像のファイル名
   * @param imageBody     ツイートに含める画像のデータ
   * @return ツイートした結果の Status オブジェクト
   * @throws TwitterException 何らかのエラーが発生した場合
   */
  private Status tweet(TwitterFactory tf, String text, String imageFileName, InputStream imageBody) throws TwitterException {
    Twitter twitter = tf.getInstance();
    StatusUpdate status = new StatusUpdate(text);
    status.setMedia(imageFileName, imageBody);
    return twitter.updateStatus(status);
  }
}
```

### pom.xml

Maven でビルドするための設定ファイル。

- AWS Lambda でサポートされている Java のバージョン 1.8 に合わせる
- AWS Lambda 用ライブラリ aws-lambda-java-core を導入する
- ライブラリも含めた JAR ファイルを生成するため maven-shade-plugin を導入する

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>image-tweet</artifactId>
  <packaging>jar</packaging>
  <version>1.0</version>
  <name>image-tweet</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-lambda-java-core -->
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-core</artifactId>
      <version>1.2.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.twitter4j/twitter4j-core -->
    <dependency>
      <groupId>org.twitter4j</groupId>
      <artifactId>twitter4j-core</artifactId>
      <version>4.0.7</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <!-- https://maven.apache.org/plugins/maven-shade-plugin/ -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.1</version>
        <configuration>
          <createDependencyReducedPom>false</createDependencyReducedPom>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>
```

### 背景画像

今回のプログラムでは、ツイートする画像の背景としてフキダシ画像を用意している。

![image-tweet-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/b885cb55-e610-152b-0626-5e37ed958b70.png)

### JAR ファイルの生成

Maven でビルドして JAR ファイルを生成する。

```
$ mvn package
```

target ディレクトリに image-tweet-1.0.jar という JAR ファイルが生成される。

## AWS Lambda 関数の作成

[Lambda Management Console](https://console.aws.amazon.com/lambda/) から Lambda 関数を作成する。

「基本的な情報」を入力する。
関数名: 任意の名前
ランタイム: Java 8
実行ロール: 基本的な Lambda アクセス権限で新しいロールを作成

![image-tweet-03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/7cb12953-c73a-8644-3bba-1e0e0445c3a7.jpeg)

「関数コード」を入力する。
コード エントリ タイプ: .zip または jar ファイルをアップロード
ランタイム: Java 8
ハンドラ: com.example.ImageTweet::handleRequest

「アップロード」をクリックして、生成しておいた JAR ファイル image-tweet-1.0.jar をアップロードする。

<img width="1107" alt="image-tweet-05.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/fcec7272-cc89-9fab-69e3-a2df97616ecb.jpeg">

環境変数を設定する。
今回のプログラムでは Twitter API をコールするためのクレデンシャル情報を環境変数から取得している。

<img width="822" alt="image-tweet-06.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/e2cd9b72-57db-010f-9541-101608d38b71.jpeg">

ページ右上の「テスト」をクリックして、新しいテストイベントを作成する。
イベントテンプレート: Hello World
イベント名: 任意の名前
入力データとなる JSON は任意のものに変更する (あるいはデフォルトのまま)。

<img width="809" alt="image-tweet-08.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/8b356530-5ce0-d5dc-07f9-494912bece89.jpeg">

ページ右上の「テスト」をクリックすると、テストを実行できる。結果が表示される。

<img width="1109" alt="image-tweet-09.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/514388b2-7867-fb42-59db-bb18fd45907e.jpeg">

ここまでで Twitter に画像付きツイートを投稿する機能が完成。

## Amazon CloudWatch Events を使用する

Lambda 関数の Designer 左側にある 「トリガーを追加」 をクリックする。

<img width="1107" alt="image-tweet-07.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/04d0e1b3-9417-947b-4feb-e8d59f510e21.jpeg">

CloudWatch Events を選択する。

<img width="822" alt="image-tweet-04.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/d2649151-e296-6e8f-5e80-c12218b92408.jpeg">

毎分実行するようにトリガーの内容を設定する。
ルール: 新規ルールの作成
ルール名: 任意の名前
ルールの説明: 任意の説明文
ルールタイプ: スケジュール式
スケジュール式: cron(* * * * ? *)

![image-tweet-02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/18749866-8cd5-ccb0-5d1e-803667fab60a.jpeg)

これで定期実行される設定が完了する。

## 実行結果

Twitter に画像付きツイートを定期的に投稿できるようになる。

![image-tweet-01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/13b8949b-4113-0d31-f2e8-1cc5dd02c015.jpeg)

## 参考資料

- [Java による Lambda 関数のビルド \- AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-programming-model.html)
- [Amazon CloudWatch Events とは \- Amazon CloudWatch Events](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html)
- [Rate または Cron を使用したスケジュール式 \- AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html)
- [ルールのスケジュール式 \- Amazon CloudWatch Events](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html)
- [Google Developers Japan: オープンソースの美しい Noto フォントファミリーに日本語、中国語、韓国語が加わりました。](https://developers-jp.googleblog.com/2014/07/noto.html)
- [Google Noto Fonts](https://www.google.com/get/noto/)
- [AWS Lambda \+ Java で Hello World \- Qiita](https://qiita.com/niwasawa/items/69bba64d7cd2d6277473)