パルが更新されたら通知するLINE BotをJavaでつくる。

パルの更新をチェックするのが不毛なので、パルが更新されたら通知するLINE botを作りました。Feedlyとかだと見逃す。

使用した環境・ライブラリ

Javaを採用した理由は、持ってるマシン(Windows 10 2台(内1台はほぼすっぴん)、Raspberry Pi 3(raspbian stretch)、CentOS7)全部で動くようにしたかったからです。DockerはWindowsマシンの片割れで正常に動作せず、Windows Subsystem for Linuxはすっぴんのほうでダウンロードに失敗して環境構築がめんどくさくなってしまい、全員JDKなら入ってるからこれでいいやと思いました。環境ごとにOracleJDKかOpenJDKかがバラバラなのですが、大したことはしないので問題ないと判断しました。

全環境でコマンド一発で動くようにしたいので、依存するクラスをすべて内包した実行可能jarを作ることにします。

実際のコード

java

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import com.linecorp.bot.client.LineMessagingClient;
import com.linecorp.bot.model.PushMessage;
import com.linecorp.bot.model.message.TextMessage;

import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndFeed;
import com.rometools.rome.io.SyndFeedInput;

public class RSSReaderBot {

    private static final String CHANNEL_ACCCESS_TOKEN = "TOKEN";
    private static final LineMessagingClient CLIENT = LineMessagingClient.builder(CHANNEL_ACCCESS_TOKEN).build();
    private static final String URL = "http://negineesan.hatenablog.com/rss"; // パルのFeed
    private static final SyndFeedInput INPUT = new SyndFeedInput();
    private static final String CHANNEL_ID = "CHANNEL_ID"; // 通知を流したいチャンネルのID

    public static void main(String[] args) throws MalformedURLException, IOException {

        long lastUpdateDate = 0;

        while (true) {
            // java.net.UnknownHostExceptionが発生するのはopenConnection時ではなくStream取得時なのと、
            // サンプルコードがごちゃごちゃする関係上try文には入れてない
            HttpURLConnection urlConnection = (HttpURLConnection) new URL(URL).openConnection();
            urlConnection.setConnectTimeout(10 * 1000);
            urlConnection.setReadTimeout(10 * 1000);

            try (InputStream stream = urlConnection.getInputStream();
                    InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {

                SyndFeed feed = INPUT.build(reader);
                SyndEntry entry = feed.getEntries().get(0); // 最新のエントリだけ取得
                long publishedDate = entry.getPublishedDate().getTime();
                if (publishedDate > lastUpdateDate) { // 「現在時刻」-「更新日時」 < 「更新間隔」 でもいい
                    CLIENT.pushMessage(new PushMessage(CHANNEL_ID,
                            new TextMessage("パルが更新されました" + "\n" + entry.getTitle() + "\n" + entry.getLink())));
                    lastUpdateDate = publishedDate;
                }

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                urlConnection.disconnect();
                try {
                    Thread.sleep(10 * 60 * 1000); // 10分
                } catch (InterruptedException ignore) {
                }
            }
        }
    }
}

今話題の無限ループで10分毎に更新をチェックします。alertじゃないから大丈夫なんじゃないですかね…(震えながら)。httpclientは不要です。

build.gradle

plugins {
    id 'java'
}

version = '0.1-SNAPSHOT'

dependencies {
    // LINE BOT SDK
    implementation 'com.linecorp.bot:line-bot-api-client:2.4.0'
    // ROME
    implementation 'com.rometools:rome:1.12.0'
    implementation 'org.slf4j:slf4j-simple:1.7.25'
    // test
    testImplementation 'junit:junit:4.12'
}

repositories {
    mavenCentral()
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

jar {
    manifest {
        attributes 'Main-Class':'biz.retrorocket.hai.RSSReaderBot'
    }
    from configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}

Gradle 3.4あたりからcompileじゃなくてimplementation使わないといけないらしいので以下を参考にimplementationで書きました。

LINE SDKとROMEがslf4j-apiに依存していて、実行時にSLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder"でWARNが出てくるので、slf4j-simpleも一緒に指定しています。

あと、ソースコードをUTF-8で書いている場合、build.gradleのオプションでエンコードを指定するのを忘れるとこうなります。

久々に力強い文字化けを見た。
久々に力強い文字化けを見た。

追記(2019/03/29)

ROMEのサンプルの写経だとStreamを閉じてなかったり、例外処理されなくてループで動かすには厳しかったので修正しました。HttpURLConnectionに関しては一応disconnectしました。
参考:HttpURLConnection#disconnectとKeep-Aliveと - CLOVER?