LGTM画像を驚くほど簡単に作れるWebサービスをScalaで作る

# はじめに

Scala, Play Framework, Akka Actor, Slick を利用し、LGTM画像を驚くほど簡単につくれるWebサービスを作った話です。

## LGTM とは

Looks Good To Me の略です。
GitHubのプルリクエストをレビューした時、「私は問題ないと思います」という意味で「LGTM」と書きこみます。

![LGTMコメント例](https://qiita-image-store.s3.amazonaws.com/0/29024/5fbea8ca-fbe1-ec24-8d9e-5d2ccf3341f9.png)

## LGTM画像とは

「LGTM」と4文字コメントするだけではそっけないので、Looks Good な画像を付けてコメントすることがあります。

![LGTM画像の例](https://qiita-image-store.s3.amazonaws.com/0/29024/373aad85-d1aa-96d7-026c-66b3587ae536.png)

こういった画像を探すのに便利な http://lgtm.in/ というサービスがあります。（現在は閉鎖？されてしまったようです）

しかし、LGTM.in はあくまでも単なる画像共有サービスです。
LGTM.inに自分の気に入った画像がないので作りたい！もうワンランクの上のLGTM画像が欲しい！
でも画像を用意して文字を重ねて...とかするのは面倒だ！というのが僕の思いでした。

そこで、「なんかいい感じのLGTM画像を驚くほど簡単につくれる」Webサービスを作りました。

# デモ

https://lgtmoon.dev/

## 使い方

1. 一番上の検索窓に、キーワードを入力します。
2. 画像検索が走るので、LGTM画像のベースにしたい画像をクリックします。
3. しばらく待つか、画面をリロードするとLGTM画像ができあがります。
4. 出来たLGTM画像をクリックすると画像のURLやGitHubのMarkdownがコピーできるので、プルリクのコメントなどに貼り付けます。

より高度な使い方

1. 一番上の検索窓に、LGTM画像のベースにしたい画像のURLを入力します。
2. しばらく待つか、画面をリロードするとLGTM画像ができあがります。
3. 以下同様です。

# 注意点

* ~~herokuの無料プランですので、動作が重かったり、容量がいっぱいになってLGTM画像が生成できなくなったりします。優しく扱ってください。~~ 有料プランにしました。
* どこかから怒られそうな画像は使わないようにしてください。
* ソースコードは公開しています。
  * https://github.com/yoshikyoto/lgtmoon

# 技術的な話

## アプリケーション構成

アプリケーション構成は以下の通りです。
フロントでの画像表示はvue.jsを使っています。採用理由は、Reactよりシンプルに書けて、個人開発でのメンテナンスがしやすいからです。
画像生成に ImageMagick を、DBアクセスには Scala のライブラリである Slick を使っています。DBにはherokuのPostgreSQLを利用しています。

![lgtmoon.png](https://qiita-image-store.s3.amazonaws.com/0/29024/80085114-7b75-0e03-c5ab-22b7f79a9345.png)

## 画像生成フロー

フロー図は以下の通りです。ベースとなる画像のダウンロードと、LGTM画像の生成には時間がかかるので、Actorを使って非同期に処理しています。画像生成APIが叩かれると、Actorに画像生成メッセージを送り、即座にAPIレスポンスを返します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29024/bc6df0a8-8b66-9234-edd6-38359b314860.png)


## vue.js（フロント）

フロントの画像表示部分にはvue.js ( https://jp.vuejs.org/ ) を使っています。vue.jsは、Reactより学習コストや実装コストが低く、小規模な個人開発向けです。時間がないのでなるべく手抜きをしています。

## Scala/Play（バックエンド）

バックエンドの言語はScalaです。ScalaのWebフレームワークであるPlay Frameworkを使っています。ScalaのWebフレームワークの中では圧倒的に人気で、ScalaでWebアプリを作る場合、普通はPlayを選択するのではないかな、と思います。

## Akka Actor

画像のダウンロードや加工には時間がかかります。画像生成APIが叩かれた際に、レスポンスはすぐに返し、時間のかかる処理は裏で進めたいです。その際に役に立つのが Akka Actor です。

今回のアプリケーションでは、「画像を生成しろ」というメッセージを送ると、Actorが裏で画像のダウンロードと加工を行ってくれます。

まず、「画像を生成しろ」メッセージを定義します。

```
case class ImageGenerateMessage(id: Long, url: String)
```

idは、DBの情報を更新するために必要な情報、urlはダウンロードすべきベース画像のURLです。

Actorは以下のようになっています。

```scala
class ImageActor extends Actor {
  val downloadDir = "/tmp"
  val convertedDir = "/tmp"
  val imageStorage = ImageStorage
  val imageRepository = ImageRepository
  val imageMagickService = ImageMagickService

  /** メッセージを受け取った時の処理 */
  override def receive: Receive = {
    case ImageGenerateMessage(id, url) => {
      val downloadPath = downloadDir + "/" + id
      val convertedPath = convertedDir + "/" + id
      // 画像のダウンロード
      imageStorage.download(url, downloadPath)
      // 画像の変換
      imageMagickService.convert(downloadPath, convertedPath)
      // convertされた画像をバイナリで取得してDBに入れる
      val bin = imageStorage.binary(convertedPath)
      imageRepository.updateStatus(id, imageRepository.AVAILABLE, bin)
    }
  }
}
```

`receive` で `ImageGenerateMessage` を受け取ったら画像のダウンロードと加工をします。
Actorはメッセージを受け取ったら処理、受け取ったら処理、ということをひたすらするだけです。

~~HerokuのPostgreSQLには、容量の制限がないため、DBに直接画像のバイナリを入れることで、ストレージを用意する手間を省いています。~~ Heroku の PostgreSQL に容量制限ができてしまったため（僕のせいかもしれない）、現在はオブジェクトストレージを契約し、そこに画像をアップロードするように変更しました。

Controllerでは、Messageを投げた後レスポンスを返します。( https://github.com/yoshikyoto/lgtmoon/blob/master/app/controllers/ImageGenerateController.scala )

```scala
/** 画像生成を行うコントローラ */
class ImageGenerateController extends BaseControllerTrait {
  /** 非同期で画像生成をするためのActor */
  val imageActor = Akka.system.actorOf(Props(new ImageActor()))

  /** postされたurlから画像生成をする */
  def withUrl = Action.async { request =>
    val jsonOpt = request.body.asJson
    jsonOpt match {
      case None => Future(NOT_JSON_RESPONSE)
      case Some(json) => {
          (json \ "url").asOpt[String] match {
            case None => Future(PARAMETER_KEYWORD_NOT_FOUND_RESPONSE)
            case Some(url)  => {
              // とりあえずURLだけ先に払い出して返す
              ImageRepository.create() map {
                case None => DATABASE_CONNECTION_ERROR_RESPONSE
                case Some(id) => {
                  // 加工後の画像のURLになる予定のやつ
                  val lgtmUrl = UrlBuilder.imageUrl(id.toString)
                  // Actorに投げる
                  imageActor ! ImageGenerateMessage(id, url)
                  // レスポンスを返す
                  Ok(JsonBuilder.imageUrl(lgtmUrl))
                }
              }
            }
          }
      }
    }
  }
}
```



## Slick

Slickは非同期にDBIOを行うことのできるScalaのライブラリです。
http://slick.lightbend.com/

Slickの特徴は以下の点です。

* 非同期処理
* 関数型チックなクエリ生成
* コード自動生成

### 非同期処理

Scalaの特徴は非同期処理なので、DBアクセスのライブラリも非同期処理に対応していてほしいです。Slickは非同期処理に対応しており、結果はFutureで帰ってくるため、重いDBIOも効率的に処理できます。

### 関数型チックなクエリ生成

Slickは、List操作のような形でDBのクエリを書くことができます。
https://github.com/yoshikyoto/lgtmoon/blob/master/app/domain/image/ImageRepository.scala

例えば、Selectのクエリを見てみましょう。

```scala
  /**
   * 画像を最新20件取得する
   * 
   * @param limit: Int 何件取得するか。デフォルト20
   * @return Future[Option[Seq[ImageRow]]]
   */
  def images(limit: Int = 20): Future[Option[Seq[ImageRow]]] = {
    val action = Image.filter(_.status === AVAILABLE)
      .sortBy(_.createdAt.desc)
      .take(limit)
      .result
    db.run(action).map {
      case images: Seq[ImageRow] => Some(images)
      case _ => None
    }.recover {
      case e => None
    }
  }
```

トップページに表示する画像20件を取得するクエリです。sortBy.take と、関数型の特徴であるList操作に近い形でクエリを書くことができます。

### コード自動生成

使い勝手にクセがあるのですが、Slickは、DBのテーブルからコードを自動生成する昨日があります。

今回はこのようなテーブルスキーマになっています。

```sql
create table Image (
       id            bigserial          primary key,
       content_type  varchar(32)        not null,
       created_at    timestamp          not null default current_timestamp,
       status        smallint           not null,
       bin           bytea              default null
);

create index image_created_at on image (created_at);
```

これをDBに流した状態で以下のようなコードを書き

```scala
object Codegen extends {
  def main(args: Array[String]): Unit = {
    val slickDriver = "slick.driver.PostgresDriver"
    val jdbcDriver = "org.postgresql.Driver"
    val url = "jdbc:postgresql://localhost/lgtmoon"
    val outputDir = "../app"
    val pkg = "repositories"
    val user = "postgres"
    val password = "postgres"
    slick.codegen.SourceCodeGenerator.main(
      Array(slickDriver, jdbcDriver, url, outputDir, pkg, user, password)
    )
  }
}
```

https://github.com/yoshikyoto/lgtmoon/tree/master/sql

このようにbuild.sbtを用意して `sbt run` すると

https://github.com/yoshikyoto/lgtmoon/blob/master/app/repositories/Tables.scala

このコードが自動生成されます。ここでは、Imageテーブルに対応するImageクラスと、テーブルの一つの要素に対応するImageRowクラスができます。これをもとに先ほどのようなクエリを書くことができます。


# さいごに

今回はLGTMを簡単に生成するアプリケーションを作成する話でした。

* http://lgtmoon.herokuapp.com/
* https://github.com/yoshikyoto/lgtmoon

今後つけたい機能

* 手元の画像をアップロードできるようにする
* 背景が白い画像や明るい画像、キャラクターなどの顔にLGTM画像がかぶってしまう時などへの対策
* 画像の検索やお気に入り的な機能