よーぐるとのブログ

技術ネタを中心。私的なことを徒然と綴っていきます。

Spring Boot(Kotlin)でOAuthを使ってGoogle Calendar APIを叩く

Java(Kotlin)アプリケーションからGoogle CalendarAPIを叩きたい

Spring Bootアプリケーション(Kotlin製)のAPIからGoogle Calendar APIを叩こうとしています。

GoogleAPIを叩くにはOauthログインが必要で、公式ガイドにはQuick StartがあるにはあるのですがWebアプリ向けではなく、少し工夫が必要でした。

Quick Startに書かれていること

Google Calendar APIにはいくつかの言語でのQuickStartがあり、その中にはJavaもあります。今回はKotlinで書かれたアプリケーションを作っているので厳密には異なるのですが、Javaでの書き方がわかれば問題ありません。

developers.google.com

QuickStartにはOauthによる認可を行いカレンダーAPIを叩くまでの流れとサンプルコードが記載されていますが、ここで実装されているのはコマンドラインアプリーケーションであり、Callback用のローカルサーバを立てて認証を行っています。

public class CalendarQuickstart {
    private static final String APPLICATION_NAME = "Google Calendar API Java Quickstart";
    private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
    private static final String TOKENS_DIRECTORY_PATH = "tokens";

    // アクセスする権限のスコープ
    private static final List<String> SCOPES = Collections.singletonList(CalendarScopes.CALENDAR_READONLY);
    // 認証情報
    private static final String CREDENTIALS_FILE_PATH = "/credentials.json";

    // 認証情報を取得する処理
    private static Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) throws IOException {
        // Load client secrets.
        InputStream in = CalendarQuickstart.class.getResourceAsStream(CREDENTIALS_FILE_PATH);
        if (in == null) {
            throw new FileNotFoundException("Resource not found: " + CREDENTIALS_FILE_PATH);
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));

        // 認証に利用するオブジェクトを作成
        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
                .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIRECTORY_PATH)))
                .setAccessType("offline")
                .build();
        // ローカルサーバを8888ポートで立てる
        LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build();
        return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
    }

    // メイン関数
    public static void main(String... args) throws IOException, GeneralSecurityException {
        final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
        // カレンダーAPI呼び出し用のオブジェクトを作成。この時点でアクセストークンがないと認証が必要になる。
        Calendar service = new Calendar.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT))
                .setApplicationName(APPLICATION_NAME)
                .build();

        // ここから先はGoogle Calendar APIを叩く処理
        DateTime now = new DateTime(System.currentTimeMillis());
        Events events = service.events().list("primary")
                .setMaxResults(10)
                .setTimeMin(now)
                .setOrderBy("startTime")
                .setSingleEvents(true)
                .execute();
        List<Event> items = events.getItems();
        if (items.isEmpty()) {
            System.out.println("No upcoming events found.");
        } else {
            System.out.println("Upcoming events");
            for (Event event : items) {
                DateTime start = event.getStart().getDateTime();
                if (start == null) {
                    start = event.getStart().getDate();
                }
                System.out.printf("%s (%s)\n", event.getSummary(), start);
            }
        }
    }
}

ターミナルからキックするとGoogleの認証URLがログに出力され、URLに飛んだ先で認証を行うことでOauthによる認可が行われます。これでAPIを叩くには叩けるんですが、そのままWebアプリに流用するような作りにはなっていません。 QuickStartにも以下の通りしっかり書かれています。

The authorization flow in this example is designed for a command-line application. For information on how to perform authorization in a web application, see Using OAuth 2.0 for Web Server Applications.

Web Server向けの案内はこちらにあるようです。

developers.google.com

Java向けの案内はなく・・・

f:id:yoghurt1131:20190707231248p:plain
google-oauth-for-web-server

QuickStartのFurther Readingにはいくつか参考になりそうなリンクがあるんですが、

developers.google.com

f:id:yoghurt1131:20190707231516p:plain
api-client-library-java

ここにあるのもQuickStartにあるCommand-Line Application、AndroidGoogle App Engineのみ(というか画像が古い...。Google+って終了してなかったっけ?)

Web Applicationでの叩き方

ということでQuickStatとUsing OAuth 2.0 for Web Server ApplicationsのHTTP版、JavaライブラリのAPI Referenceを参考にしつつWeb Applicationで同じことをやる方法を探りました。

Oauthの流れ

知っている人は読み飛ばしてもらって良いですが、Oauthはざっくりと以下のような流れで認可を行います。

f:id:yoghurt1131:20190708000409p:plain:w500
GoogleカレンダーでのOauthによる認可

初回ユーザがアクセスしてきた場合、アプリーケーションはGoogleの認証画面へとリダイレクトします。

f:id:yoghurt1131:20190708001255p:plain:w300
Googleの認証画面

認証が終わるとあらかじめ設定してあるコールバックURLに飛ばされます。コールバック時にはパラメータとして認証コードがついてきます。

https://oauth2.example.com/auth?code=AUTHORIZATION_CODE

アプリケーションで認証コードを受け取り、それをGoogleのOauth APIへと渡してアクセストークンを取得します。

取得したアクセストークンを利用することでGoogle Calendar APIにアクセスすることができます。

実装

ということでGoogle Calendar APIの利用には以下の処理が必要になります。

  • 認証済みでないユーザが来た際にGoogleの認証ページにリダイレクトする
  • コールバックのリクエストを受け取ってアクセストークンを取得する

認証済みでないユーザが来た際にGoogleの認証ページにリダイレクトする

リダイレクトURLはhttps://accounts.google.com/o/oauth2/v2/auth を叩けば良いようですが、必要なクレデンシャル情報も含めいくつかパラメータが必要になってきます。 ライブラリのリファレンスを漁っているとAuthorizationCodeRequestUrlというそれっぽいクラスが見つかったのでそれを利用して生成することにしました。 https://developers.google.com/api-client-library/java/google-oauth-java-client/reference/1.19.0/com/google/api/client/auth/oauth2/AuthorizationCodeRequestUrl

f:id:yoghurt1131:20190708003142p:plain:w500

このクラスのbuildメソッドをKotlinで呼び出してあげます。

val SCOPES = Collections.singletonList(CalendarScopes.CALENDAR_READONLY) // アクセスするスコープを設定
val authUrl = "https://accounts.google.com/o/oauth2/auth" // 認証URL
val clientId = "xxxxxxx" // Client ID
val callbackUrl = "http://localhost:8080/callback" // コールバック用URL

val requestUrl = AuthorizationCodeRequestUrl(authUri, clientId)
    .setRedirectUri(callbackUrl)
    .setScopes(SCOPES)
    .build()
// リダイレクト
response.sendRedirect(requestUrl)

コールバックのリクエストを受け取ってアクセストークンを取得する

こちらはQuickStartでも利用されていたGoogleAuthorizationCodeFlowのリファレンスに飛んだらヒントがありました。

f:id:yoghurt1131:20190708005336p:plain

The web browser will then redirect to the redirect URL with a "code" query parameter which can then be used to request an access token using newTokenRequest(String).

newTokenRequest(String)を呼んであげれば良さそうです。

        val request = googleAuthorizationCodeFlow.newTokenRequest(code)
                .setRedirectUri(googleClientSecrets.details.redirectUris.first())
        val response = request.execute()
        googleCredential.setAccessToken(response.accessToken)
        return "This is endpoint for google oauth2 callback."

Spring Boot Applicationでの認証処理

最終的なコードは以下のようになりました。

Googleの認証画面へのリダイレクトをインターセプターで実装し、エンドポイントにアノテーションを付与することでControllerの指定したエンドポイントにアクセストークン無しで来た場合、Oauthによる認可が行われます。

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GoogleOAuth2  // ControllerにつけるAnnotation
// アクセストークンがない場合に自動でGoogleの認証ページにリダイレクトさせるInterceptor
class GoogleOAuth2Interceptor(
        private val googleClientSecrets: GoogleClientSecrets, // Oauthに利用するプロパティ
        private val googleCredential: GoogleCredential            // Calendar APIに利用するプロパティ
) : HandlerInterceptor {
    // 利用する権限:カレンダーのREADのみ
    private val SCOPES = Collections.singletonList(CalendarScopes.CALENDAR_READONLY)
    private val logger: Logger = LoggerFactory.getLogger(this::class.java)

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        logger.info("Start - GoogleOAuth2Interceptor $request, $response, $handler")
        val handerMethod = handler as HandlerMethod
        // アノテーションが付与されている場合、認証チェックを行う。
        val annotation = handerMethod.getMethodAnnotation(GoogleOAuth2::class.java)
        annotation?.let {
            logger.info("Check Google OAuth2....")
            // アクセストークンの有無を確認
            if(googleCredential.accessToken.isNullOrBlank()) {
                logger.info("No accessToken. Redirect Google Authentication Server")
                // アクセストークンがない場合は認証用URLを生成してリダイレクトする
                val requestUrl = AuthorizationCodeRequestUrl(googleClientSecrets.details.authUri, googleClientSecrets.details.clientId)
                        .setRedirectUri(googleClientSecrets.details.redirectUris.first())
                        .setScopes(SCOPES)
                        .build()
                 response.sendRedirect(requestUrl)
                return false
            }
            logger.info("AccessToken has found. Continue.")
        }
        return true
    }

    override fun postHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any, modelAndView: ModelAndView?) {
        logger.info("End - GoogleOAuth2Interceptor")
        super.postHandle(request, response, handler, modelAndView)
    }

}
@RestController
class CalendarController(
        private val googleCredential: GoogleCredential,
        private val googleClientSecrets: GoogleClientSecrets,
        private val googleAuthorizationCodeFlow: GoogleAuthorizationCodeFlow,
        private val calendarService: CalendarService) {

    private val logger = LoggerFactory.getLogger(javaClass)

    @GetMapping("/schedule/daily")
    @GoogleOAuth2  // 認証を行うエンドポイントにアノテーションを付与
    fun todaySchedule(): Schedule {
        val schedule = calendarService.getSchedule(1L)
        return schedule
    }

 // コールバック用エンドポイント
    @GetMapping("/callback")
    fun callback(code: String, scope: String): String {
        // 認証コードをもとにアクセストークンを取得
        val request = googleAuthorizationCodeFlow.newTokenRequest(code)
                .setRedirectUri(googleClientSecrets.details.redirectUris.first())
        val response = request.execute()
        googleCredential.setAccessToken(response.accessToken)
        return "This is endpoint for google oauth2 callback."

    }
}

すべてのコードはGithubにも掲載しています。

github.com

過労で腕を痛めた話と治した話 

突然ですが、腕を痛めました。。。 診断結果は疲労による上腕骨外側上顆炎と上腕骨内側上顆炎。文字からだとわかりずらいですが肘の周りです。通称テニス肘と言われるそうですね。

ソフトウェアエンジニアという仕事の性質上よくある話なのかもしれません。が、実際になってみると結構きつかったです。幸いなことに一ヶ月程度で良くなってきたので、ここでは腕を痛めるまでと痛めてから治すまでにやったことを書いていきます。

※ 前提として(そして会社の名誉のためにも)、いわゆる長時間労働が続いていたということはありませんでした。ごく普通に働いており、見た目上なんら問題のなさそうな生活をしていました。

発症一週間前

「なんか右腕がヒリヒリするなあ」というのがきっかけでした。普通にしている分には問題ないんだけど、仕事をしていると夕方〜夜にかけて肘の辺りに違和感が出てくる。なんとなくヒリヒリする。

特に心当たりもなかったのと、仕事ができないほどの痛みではなかったので、あまり気にせずいつもどおり仕事をし、いつもどおり生活をしていました。このくらいの痛みならそのうち消えるでしょ、という気分で。

そう、来週その痛みがさらに悪化するとも知らずに....(怖い話風)

翌週

腕に抱えている違和感が明確な痛みに変わり始めました。え、痛い。本当に痛い。 仕事でタイピングをしていると痛いし、Macbookトラックパッドをいじいじしているのも痛い。

肘の辺りだけ痛かったのが、手首のあたりも痛くなり、挙げ句の果てには指がつりそうになる始末。

当たり前のことなんですが腕が痛いというのはすごく不便で、仕事はもちろん私生活にも支障をきたしました。家でPC触ることができない、ゲームが遊べない、くらいならまだいいですが、何気ない動作が全て辛い。食器を洗うのがつらい、歯磨きがきつい。何もしてなくても痛いので何をする気にもなれない。利き腕だったので余計にそう感じたのかもしれません。

「これはまずい」と思ってお休みを取り、以前腱鞘炎になった知り合いが勧めてくれた手の専門外科へと行きました。

診断結果

お医者さんには「ジョウワンコツガイソクジョウカエン、ジョウワンコツナイソクジョウカエン。疲労だね。」と言われました。耳で聞いたときには何を言っているか全く変換できなかったのですが、漢字に直すと上腕骨外側上顆炎と上腕骨内側上顆炎。最初にも書きましたがいわゆるテニス肘だそうです。別にテニスなんてしないのになーと調べてたら「スマホ肘」「PC肘」という呼び方も見つけました。ある意味現代病の一種で、普段の生活で疲労が溜まって肘周りの筋肉が炎症を起こしている状態とのこと。

f:id:yoghurt1131:20190331222630j:plain:w500
痛めた箇所。僕の場合は外側だけでなく内側も痛めていた。

https://www.saiseikai.or.jp/medical/disease/lateral_humeral_epicondylitis/

肘の周りは手首や指につながる筋肉が集まっているため、ここを痛めるといろんな箇所に影響が出るのだとか。実際悪化したあとは手首や指、前腕全体がきしむような痛みを覚えました。インフルエンザにかかると関節が痛くなりますが、あの感覚に近いです。

治すためにやったこと

治すためにいくつかのことを行いました。

この中のどれが一番効いたとかは正直わからないです。全部やってみた結果良くなったのかもしれません。

もちろん、個人の経験ですのでやったことが完全に他の人に合うとは思いません。あくまで参考程度に。

1. 働き方を変える

腕を痛めたあと上長に相談する機会があったので、正直に話をして新規の開発などコーディングが増える作業を減らしてもらいました。 負荷をかけすぎない、ということに重点をおいて業務調整をしてもらい、主にコードレビューや運用周りのタスクをメインにこなすようになりました。

また、弊社はフレックス制度があるため通常の勤務より早めに上がることもできました。朝一番は問題ないけど、6〜7時間程度で腕の限界が来るので、チームメンバーと相談して仕事を調整しつつ、短めの勤務とさせてもらっていました。

2. 治療、リハビリ、ストレッチをする

病院からはロキソニン湿布を処方されてたのでそれを毎日上腕部分に貼っていました。

病院に勧められて超音波治療も行っていました。1回5〜10分程度で診療とは別で行ってもらえるので待つ必要がなかったのも嬉しかったです。週2〜3の頻度で通いました。

また、テニス肘に効きそうなストレッチも行いました。医者からはストレッチをするのは問題ないと言われていたので[*1]、「テニス肘 ストレッチ」で調べて出てきたものを試していました。

www.youtube.com 実際にやってみるとたしかに肘周りの筋肉が伸びている感じがする...

3. キーボードを変える

働き方とは別でもう一個、働く環境を変えました。具体的にはMacbookのキーボードが辛かったので自宅からキーボードを持ってきました。 Kinesis Freestyle 2 という左右分離式のキーボードです。

f:id:yoghurt1131:20190331220700j:plain:w500
kinesis-freestyle2
https://kinesis-ergo.com/shop/freestyle2-for-pc-us/

3ヶ月ほど前に肩こりの改善を目的に自宅用に購入し使っていたのですが、会社に持ってきて使うようになりました。左右が別れているので腕のポジションが自然なままタイピングができるのと、サポートキットによって手の角度を水平ではなく少し傾けた状態で使えるので、通常のノートPCをタイピングするより疲れにくいのが圧倒的なメリットです。

会社のPCゲーマーな同僚には「こんなキーボードで絶対にゲームしたくない」と酷評されましたが、コーディングをする分にはこちらの方が良いと思います。長時間デスクで作業をしたあとの腕から肩周りの疲れが違います。(だんだん宣伝ぽくなってきましたが)おすすめです。

追記: 効かなかったもの - サポーター

診断を受けたときに「どうすればいいでしょう?」と聞いたら「仕事をしないわけにもいかないだろうし、サポーターを付けるとかがいいかなあ」とお医者さんにぼやかれたんで肘周りのサポーターを購入しました。

が、これは僕にとってはあまり良くなかったと思います。たしかに仕事中の痛みは軽減できるんですが、痛みが軽減している分無理に働くことができてしまい、仕事が終わってサポーターを外したあとにすごく痛むということが何度かありました。ドーピングのような感じで自分には良くないなと思ったんで、何回か使ったあとはサポーター無しで生活し、痛くなってきたらきちんと休む、という方向に倒しました。

まとめ: 良くなってきた(気がする)

上記のような取り組みを並行して進めた結果、二週間程度で日常生活が送れるほど腕の痛みはやわらぎ、一ヶ月経った現在痛みはほとんどなくなりました。もちろん長時間コーディングをするなど負荷をかけるとやや違和感を感じる〜ヒリヒリする程度にはなってしまいますが、一番痛かったときと比較すると大きな改善です。

もともと身体が硬かったり医者からストレートネックだと言われたりと、疲労が溜まりやすい & 身体に出やすい体質なので、これからも無理をせず気をつけて働きたい(働きたくない)と思います。


*1:腱鞘炎など症状によっては「ストレッチをすることで悪化する」ケースもあるそうなので医者とよく相談をしたほうがいいです

Clean Architectureを読んだ話

「Clean Architecture 達人に学ぶソフトウェアの構造と設計 」を読んだので感想をば。

Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)

Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)

どういう本か

名前通りアーキテクチャに関する本ではあるが、この本の特徴は「良いアーキテクチャとはこういうものだ」というトップダウンな説明ではなく、「意識するべき原則は常に同じであり、それをどのように適用していくか」というボトムアップ方式でコアとなる考え方を丁寧に伝えているところだと思います。

どんな種類のシステムでもソフトウェアアーキテクチャのルールは同じ。ソフトウェアアーキテクチャのルールとは、プログラムの構成要素をどのように組み立てるかのルールである。構成要素は普遍的で変わらないのだから、それらを組み立てるルールもまた、普遍的で変わらないのである。

序文にこう書かれているように、本書前半ではSOLID原則に始まり、様々なソフトウェア設計の原則を丁寧に説明していきます。これらの原則は後半の本題であるコンポーネントアーキテクチャの話の中で具体例を交えながら何度も出てくるため、読者は基礎と応用の結びつき方が自然と理解でき、「なぜ」がすっと頭に入ってきます。

原則の説明→具体例の紹介→抽象化というサイクルが本書の中でうまく回されており、読んでいく中でエンジニアとして一段上の視野が拓けるようになると思います。

どういう人が読むとよいか

もちろんソフトウェアエンジニアリングに関わるいろんな人に読んでもらいたいですが、個人的にはプログラマとしてそれなりの経験があり、設計も含めて一からサービス開発にチャレンジしたことのある人が自身の経験と照らし合わせて読むと大きな気付きが得られるのではと思います。もちろんそれだけではなく、SOLID原則やデザインパターンアーキテクチャの原則に関する基本的な知識はあるものの、実践でうまく活かせないというモヤモヤを抱えている人にとっても非常にためになるような本だと思います。

また、ビジネスロジックの扱いなどドメイン駆動開発(DDD)の考え方について理解するにも良い本でした。いわゆるDDD本はそれはそれで素晴らしいのですが、挫折してしまった人、理想だと割り切って読んでしまった人にとってこの本は理想と現実を繋いでくれることでしょう。

個人的に良かった点

僕がこの本を読んだタイミングは、中長期かけて取り組んでいた新サービスの立ち上げが終わったタイミングでした。自分の中で色んな葛藤や反省を抱えていた時期だったので、それをうまく言語化し次に活かしていくのに非常に刺さりました。

印象に残った文章

一番印象に残ったのは以下の文章です。

そもそもソフトウェアアーキテクトはプログラマである。(中略)ソフトウェアアーキテクトは最高のプログラマであり、継続してプログラミングの仕事を引き受けながら、生産性を最大化する設計にチームを導いていく。(15章アーキテクチャとは? より)

これは非常に大事で、プログラマだろうとアーキテクトだろうと忘れてはいけないことだと思います。そもそも良いアーキテクチャを作るのは自己満足のためではなく、プロダクトの保守性を向上させ、ビジネス価値を高めていくためです。プロダクトの保守、改善が容易というのは既存のコードに手を入れやすいことと同義で、具体的な実装の積み重ねが良いアーキテクチャを構成していきます。上記の文章はなんとなく自分が思い描いていたアーキテクト像と近かったのでとても響きました。

現実に立ち向かう方法を教えてくれる

冒頭にも書いたようにこの本では非常に多様な具体例をもって目指すべき道筋を示してくれます。また、全体を通して「プロジェクトの現状やステータスによって最適解は常に変わる」というスタンスで書かれているため理論の押し付け感がなく、自分たちの現場と照らし合わせて読みやすいというのも本書の特徴です。多くのエンジニアに自分が持っている武器と、その使い方を教えてくれる本だと思います。

おまけ

付録としてついている「アーキテクチャ考古学」は、OSもファイルシステムもなかった時代からエンジニアとして活動してきた筆者の自著伝です。こちらも読み物としてとても面白いのでぜひ読んでみてください。

2019年の抱負

今更ですが明けましておめでとうございます! 2018年の簡単な振り返りと2019年の抱負です。

2018年

2018年は社会人になって2年目の年になりました。去年から都内でエンジニアとして働いていますが、大きいプロジェクトにアサインされ設計からリリースまでに携われたのは非常に学びが多かったです。また、DevOpsやコンテナ技術に興味を持って、勉強したり遊んだりとたくさんの新しいものに触れた年でもありました。

一方で仕事に比重がよってしまいそれ以外のことが徐々に疎かになってしまっていったのを感じたので、今年はこうやって文章に残しておきたいと思います!

2019年

去年もおおよそ同じだったのですが、2019年大事にすることは健康、技術、英語の3つです。

健康は生活習慣を改善すること、運動を継続することの2点。生活を豊かにしたいです。去年前半に自分の睡眠の質が悪いことに気がついて生活習慣の見直しを測り、一時期非常に良い朝型生活が送れていたんですが気がついたら戻っちゃっていました。生活習慣を変えることで非常にコンディションがよくなる感覚はあったのでなんとか維持していきたいです。

技術についてはインプットとアウトプットの量を増やすことを目指します。技術は日々進歩しているので、最前線との距離感や自分が経験したことの概念化、言語化をしっかりとしていきます。

英語はリーディングとリスニングを重点的に取り組みます。ここ二年間で英語でニュースを読んだり技術カンファレンスの講演を聞いたりするのは抵抗なくなってきたので、それがよりスムーズに速くできるよう慣れていきたいと思います。

AlexaスキルとAWS Lambda, Slackを使って我が家の買いもの管理を便利にした話

やったこと

ライフハックです。Alexaに買うものを伝えるとSlackの買い物チャンネルに伝えたものが追加されるシステムを作りました。 仕組みとしてはシンプルで、Alexaに「〇〇を追加して」と伝えると、Alexaのスキル、AWS Lambdaの関数が呼び出されSlackのチャンネルに〇〇がポストされるというものです。

f:id:yoghurt1131:20181223150520p:plain

買い物管理

同居人と生活をするにあたって、買いもの、特に洗剤やシャンプーなど日用品類の買い物管理をどうしようかという話題がありました。「XXを買っておいて」「〇〇はもう買ったんだっけ?」というコミュニケーションは仕事などで疲れているときにはコストが高いですし、買い忘れが発生して喧嘩する、みたいなのも避けたい。なんとかいい感じに解決したい、という思いを二人共モヤモヤ抱えていました。

ちなみに「Alexaで全部注文しちゃえばいいじゃん」というのはなしで。そういう案もあったのですが、お互い家にいない時間が多くマンションに宅配ボックスもないためボツとなりました。あくまでどちらかが買い物をしなきゃいけないという状況に対する取り組みが今回の記事です。

この課題を解決すべく、まずはSlackに買い物チャンネルを作り必要なものはそこにポストして管理する、というルールを決めました。やることは簡単で

  1. 買わなきゃいけないものがあったらその名前を書いてポストする
  2. ポストされたものを買ったら「済」スタンプをつける

というものです。 ルールを決めたことで「誰が何をいつ買ったか」が明確になり、お互い暮らしていく中で非常にストレスのない買いもの生活が送れるようになりました。

f:id:yoghurt1131:20181224165411p:plain

Alexaを用いた買うもの投稿機能

上記のルールで買いものの管理は便利になったものの、別の課題が出てきました。 それは「買わなきゃいけないものは日常生活をしている中でふと現れる」ということです。食器洗い、洗濯、掃除といった家事に取り組んでいる最中に「あれ買わなきゃ」と思っても、やっている最中にスマホを取り出してSlackに投稿するのは面倒だし(そもそも家事の途中は手が空いていないことの方が多い)、一段落ついた頃には忘れてしまうことだってあります。

そう、「Slackに買うものを投稿する」の自体がそもそもハードルが高いのです。

そこで、Alexaが代わりにSlackへの投稿をしてくれる簡単なアプリケーションを作成することにしました。声で伝えるのであれば手が埋まっていてもできるし、思いついたその場で登録しやすいですよね。 今回はAWS Lambdaと簡単なAlexaスキルを作成してVoice UIでの買い物登録を実現させました。

システム構成

最初に載せた図の再掲ですが。

f:id:yoghurt1131:20181223150520p:plain

ユーザがスキルを呼び出し、Alexaに向かって話しかけます。その発話から必要な情報(今回は「買うもの」)が取り出され、AWS Lambdaに登録した関数が呼び出されます。関数の実態はSlack APIを叩くだけのNode JSスクリプトです。Alexaから受け取った情報をもとにSlackの特定チャンネルにボットで投稿を行います。

Alexaスキルの作成

AlexaスキルはAmazon Alexaの開発者登録を行い、Alexa Developer Consoleから簡単に作成することができます。 アプリではインテントと呼ばれる発話意図を登録することで、特定のアクションを受け付けられるようになります。インテントには発話のパターンが登録でき、各発話パターンの中に「スロット」と呼ばれる枠を用意することで、「{りんご}を追加して」「{みかん}を追加して」といった発話によって変化する単語を入れることが可能になります。

f:id:yoghurt1131:20181224235329p:plain:w250

今回は「買うもの」に当たる部分を{things}という形でスロットにしました。また、買うものを追加する時に言うであろういくつかの発話パターンを登録します。

f:id:yoghurt1131:20181223151010p:plain
alexadevconsole

AWS Lambdaへの関数登録

AWS Lambdaには上述したように、Alexaスキルから連携される情報を受け取りSlackに投稿する関数を登録します。デフォルトのインテントや、定義したインテントを受け取るHandlerを定義することで、Alexaから連携される発話に対する処理を記述できます。

下記に買いものの処理をする部分のコードを載せます。let things = this.event.request.intent.slots[SLOT_NAME].value;の箇所が、実際にAlexaスキルから連携されているスロットを受け取っているところです。

SDKのバージョンが古いため、最新バージョンだとこの書き方では動かないかもしれません。

// Slack Webhook URL
const WEBHOOK_URI = process.env.WEBHOOK_URI;
// Alexaから受け取るスロット
const SLOT_NAME = 'things';
const handlers = {
    ...
    // 買いものリスト追加処理
    'ToBuyIntent': function () {
      let func = this;
      // thingsスロット(=買うもの)を取得
      let things = this.event.request.intent.slots[SLOT_NAME].value;
      // Alexaの発話
      const speechOutput = things + 'ですね。わかりました。';
      func.response.speak(speechOutput);
      func.emit(':responseReady');

      // Slackに投稿するためのパラメータ
      let options = {
        method: 'POST',
        uri: WEBHOOK_URI,
        body: {
          text: things
        },
        json: true,
        headers: {
          'content-type': 'application/json',
        }
      };
      // Slackに投稿
      requestPromise(options).then(function(body) {
      }).catch(function(err) {
        console.log(err);
        func.response.speak('エラーが発生しました');
        func.emit(':responseReady');
      });
    },
    ...
};

exports.handler = function (event, context, callback) {
    const alexa = Alexa.handler(event, context, callback);
    alexa.APP_ID = APP_ID;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

苦戦した部分

あとはAlexaスキルのエンドポイントにAWS Lambdaの関数を登録し、Alexaで呼び出す際のスキル名を設定すれば完成となります。

が、ここで一つ罠がありました。 買い物リストなんていうありふれた機能はすでにAlexaにデフォルトスキルとして実装されているのです。

f:id:yoghurt1131:20181224164032p:plain:w200f:id:yoghurt1131:20181224164036p:plain:w200

最初、スキル名を「買い物リスト」としいたんですが、そうすると自作したスキルではなくデフォルトのスキルが呼び出されてしまうのです。これでは使えない。。。

スキル名は「アレクサ、<今日の星占い(スキル名)>を開いて双子座の運勢を占って」のようにAlexaへの指示の中に織り交ぜて使われます。そのため、発話にいれて違和感のあるスキル名や極端に長いスキル名などは設定したくありません。 デフォルトのスキルが呼び出されず、かつAlexaへの呼びかけとして不自然じゃないスキル名を検討した結果、以下のようになりました。

f:id:yoghurt1131:20181224164658p:plain

これだ!!

「Alexa、スラックを開いてゴミ袋を追加して」

f:id:yoghurt1131:20181224165147p:plain:w200

きっちりとSlackに投稿されています。これならまあ及第点でしょう。やったね。

まとめ

AlexaスキルとAWS Lambdaを使って買いもの管理を便利にしてみました。 やはり音声による入力というのは手入力よりはるかにストレスがないらしく、もはやスマホでSlackを開いて買うものを投稿するという場面はほとんど見なくなりました。こういったライフハックを積み重ねて生活をどんどん便利にしていきたいですね。

資料とか

GitHub - IshinFUKUOKA/waht-i-buy: Alexa skill's application posting message to slack channel

スライド - 20180711-alexa-yoghurt1131.pdf - Google ドライブ

Knife-ZeroによるRaspberry Pi3の構成管理

お題

Raspberry Piの構成管理をChefで行いたい。 細かい環境構築やセットアップなどをコード化したい。

環境

・OS: Mac OSX Yosemite ・Chef: 14.5. ・ラズパイ: Raspberry Pi 3 ・ラズパイOS: Raspbian GNU/Linux 9 (stretch)

Knife Zeroのインストール

Knife-Zeroのサイトに記載されている方法に従ってknife zeroコマンドをインストール Installation · Knife-Zero

以下のようにknife zerと打ってUsageが出てくればOK。

$ knife zero
FATAL: Cannot find subcommand for: 'zero'
Available zero subcommands: (for details, knife SUB-COMMAND --help)

** ZERO COMMANDS **
knife zero apply QUERY (options)
knife zero bootstrap [SSH_USER@]FQDN (options)
knife zero chef_client QUERY (options) | It's same as converge
knife zero converge QUERY (options)
knife zero diagnose # show configuration from file

Rasbbian用の設定ファイル用意

bootstrap時には-tオプションで実行したいテンプレートを指摘できる。 以下のサイトにRaspbian用のテンプレートがおいてあったのでそれを利用する。

GitHub - dayne/raspbian_bootstrap: chef bootstrap for raspberry pi

bootstrapの実行

$ knife zero bootstrap -t raspbian-jessie-gems.erb --ssh-user pi --sudo 192.168.11.15
(中略)
ERROR:  Error installing chef:
  ohai requires Ruby version >= 2.4.
Starting the first Chef Client run...
sh: 112: chef-client: not found

よくみると先程ダウンロードしたテンプレート内に以下の記述が。

RUBY_VER=2.3.3

2.4.0に書き換えてあげる

RUBY_VER=2.4.0

別のエラー OpenSSL::OpenSSLError: password must be at least 4 bytes

knife.rbに以下を書き加えてあげる

ssl_verify_mode  :verify_none

bootstrapまではなんとか成功した。

mavenプロジェクトでフォーマッターを使う

技術Tipsです。

概要としてはmavenを利用しているspring bootプロジェクトにおいてコードフォーマットを行う方法、及びそれをIDE(Eclipse, IntelliJ)に適用する方法になります。

背景

コードフォーマッターが必要になった経緯です。

最近は仕事でSpring Bootを使ってWebアプリの開発をしています。Javaの開発であればEclipseIntelliJなど優秀なIDEがあるため、コマンド一発でフォーマットしてくれます。便利ですよね。

なので、個別にフォーマッターを入れる必要は開発時点ではなかったのですが、CI/CD時にフォーマットチェックを入れたいという話になり、Githubなどに上げたあとの環境でフォーマットをかける必要が出てきました。

今回はmavenでプロジェクトを作っていたためmavenでのフォーマッターの導入の話、IDEと共通のフォーマッターを使用する方法などを書きます。

使用するフォーマッター

formatter-maven-pluginというのを利用します。が、公式らしきサイトのUsageを試していてもうまくいかず、少し自分で調べる必要がありました。

利用方法

公式サイトの方ではpom.xmlに書くpluginと、実行方法は以下のようになっていました

pom.xml

<project ...>
    ...
    <plugins>
      <plugin>
        <groupId>net.revelc.code.formatter</groupId>
        <artifactId>formatter-maven-plugin</artifactId>
        <version>2.0.2-SNAPSHOT</version>
      </plugin>
    </plugins>
    ...
</project>

実行方法

mvn java-formatter:format

しかしこの方法ではフォーマットはおろか、プラグインの実行すらされません。 というかそもそもmvn java-formatter:formatで実行しているjava-formatterプラグインのprefixから検索しているのにプラグインのartifactIdがformatter-maven-pluginだし。。。

色々調べたり試したりしたところ、下記のプラグイン指定と実行方法で行けました。

pom.xml

<project ...>
    ...
    <plugins>
      <plugin>
        <groupId>net.revelc.code.formatter</groupId>
        <artifactId>formatter-maven-plugin</artifactId>
        <version>2.0.1</version>
      </plugin>
    </plugins>
    ...
</project>

実行方法

mvn formatter:format -Dconfigfile=formatter.xml

formatter.xmlは下で説明しますが、フォーマット用の設定ファイルです。

フォーマットチェックとフォーマット

対象ファイル

確認のため、あえてフォーマットが崩れているファイルを用意しました。main関数を1行で記述しています。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MavenFormatterDemoApplication{
    public static void main(String[] args){ SpringApplication.run(MavenFormatterDemoApplication.class, args);}
}

設定ファイルの取得

上で述べた設定用のxmlファイルですが、今回はSpring bootで標準的に使われているらしいxmlファイルを落としてきて入れました。formatter-maven-pluginではeclipseのフォーマット用のxmlが設定ファイルとして使え、検索するといくつかヒットします。

github.com

設定の例

下記は今回使用した設定ファイルの一例です。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="12">
<profile kind="CodeFormatterProfile" name="Spring Boot Java Conventions" version="12">
...
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
...
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="90"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
...
</profile>
</profiles>

設定項目はたくさんありますが、<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="90"/>(改行する文字数)のように比較的理解しやすいものが多いです。

とはいえ直接編集するのは骨が折れるので、カスタマイズするなら後述するEclipseの設定を用いてxmlを生成するのがよいでしょう。

フォーマットチェックとフォーマット

設定したxmlファイルのフォーマットに従っているかどうかをチェックするには以下のコマンドを実行します。

mvn formatter:validate -Dconfigfile=formatter.xml

先程のようにフォーマットが崩れているファイルがプロジェクトに存在すると、mavenがエラーを吐きます。

> ~/maven-formatter-demo> mvn formatter:validate -Dconfigfile=formatter.xml
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building maven-formatter-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- formatter-maven-plugin:2.0.1:validate (default-cli) @ maven-formatter-demo ---
[INFO] Using 'UTF-8' encoding to format source files.
[INFO] Number of files to be formatted: 2
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.130 s
[INFO] Finished at: 2017-10-01T19:57:48+09:00
[INFO] Final Memory: 13M/81M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal net.revelc.code.formatter:formatter-maven-plugin:2.0.1:validate (default-cli) on project maven-formatter-demo: File '~/maven-formatter-demo/src/main/java/com/yoghurt1131/mavenformatterdemo/MavenFormatterDemoApplication.java' format doesn't match! -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

では、これをフォーマットしていきます。先程のmvnコマンドでvalidateとしていた箇所をformatに変えるだけです。

 mvn formatter:format -Dconfigfile=formatter.xml
> ~/mave-formatter-demo> mvn formatter:format -Dconfigfile=formatter.xml
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building mave-formatter-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- formatter-maven-plugin:2.0.1:format (default-cli) @ mave-formatter-demo ---
[INFO] Using 'UTF-8' encoding to format source files.
[INFO] Number of files to be formatted: 2
[INFO] Successfully formatted:          1 file(s)
[INFO] Fail to format:                  0 file(s)
[INFO] Skipped:                         1 file(s)
[INFO] Read only skipped:               0 file(s)
[INFO] Approximate time taken:          0s
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.129 s
[INFO] Finished at: 2017-10-01T20:00:00+09:00
[INFO] Final Memory: 14M/79M
[INFO] ------------------------------------------------------------------------

フォーマット成功ファイルの数、失敗したファイルの数、スキップされた(すでにフォーマットされていた)ファイルの数などがでます。

実際に確認してみると、きちんとフォーマットがされています。

フォーマット前

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MavenFormatterDemoApplication{
    public static void main(String[] args){ SpringApplication.run(MavenFormatterDemoApplication.class, args);}
}

フォーマット後

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MavenFormatterDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MavenFormatterDemoApplication.class, args);
    }
}

IDEでの利用

作成したフォーマッターを使えば、PaaS環境などのIDEが無い場所でもフォーマットチェックやフォーマットが行えるようになりました。とはいえ、IDEのフォーマッターとmavenで使用するフォーマッターが違ってしまうと、開発環境でかけたフォーマットがpush先で通らないといった事故が起きる可能性があります。

ここでは、作成したフォーマッターを各IDE(eclipse, IntelliJ)に読み込む方法を書きます。

eclipseでの利用

Eclipse->Preferences(設定)->Java->Code Style->Formatterに設定項目があります。

f:id:yoghurt1131:20171001202002p:plain:w500

ここでxmlファイルをimportすることができます。

また、Newから独自のフォーマット規約を作成することができます。これはxmlファイルとしてExportすることができるので、既存のがイマイチで自分でフォーマッターを作りたいという場合はこれを利用してオリジナルのフォーマッターを作成するのがいいかと思います。

IntelliJでの利用

IntelliJでも利用方法はほとんど同じです。

IntelliJ->Preferences->Editor->Code Style->JavaからImport Schemeをすることで設定ファイルを導入することができます。

注意点としては、IntelliJにはIntelliJ独自のコードフォーマット(IntelliJ IDE code style XML)があることです。

Eclipseとの互換性はあるため、Eclipseで作ったxmlファイルをIntelliJで利用することはできますが、逆はできません。開発時にIntelliJの設定でフォーマッターを作成した場合はそのままmavenで使用することができないのでご注意ください。

リンク・参考