Goalist Developers Blog

teamLab Planets TOKYO DMM.com に行ってきた

f:id:watabe1028:20180806122230j:plain

こんちは。
今回もイベントレポートです。
前回の記事
またもや、ただの遊びです。

昨年の今頃、ヒカリエでのイベントレポを書きました。
1年が早いですね。
 

タイトル通りですが豊洲の方に行ってきました。
本当はBorderlessに行きたかったのですがチケットが取れず。。。
当日思いつきで豊洲に行ったらあまり待たずに入れました。
 
 

イベント概要

teamLab Planets TOKYO DMM.com
場所:東京都江東区豊洲6-1-16 teamLab Planets TOKYO
期間:2018/7/7 〜 2020年秋
時間:10 〜 25時
料金:2,400円〜(大人)
詳細:

planets.teamlab.art
 

はじめに

まず入り口でスマホを入れて首から下げれるケースが借りれます。
これで水に落とす心配はないです。

靴を脱いで素足になります。
不要な荷物をロッカーに預けます。
床が鏡張りになっているため女性はスカートはやめておいた方が良いです。
もしスカートの場合でも会場でハーフパンツの貸し出しもやっているので必ず借りましょう。
長ズボンの場合でも膝上までまくる必要があるので借りた方が良いかもです。
水深は40cmくらいと考えておけば良いと思います。
 
 

Waterfall of Light Particles at the Top of an Incline

https://planets.teamlab.art/tokyo/jp/ew/lightparticles/
f:id:watabe1028:20180806122322j:plain
(写り悪くてすんません)
はじめに足の洗浄?がてらに浅い水の中を歩きます。
坂の上に滝があります。
憑依する滝の小さい版みたいな感じですかね。
もうテンション上がります。
 
 

やわらかいブラックホール - あなたの身体は空間であり、空間は他者の身体である

https://planets.teamlab.art/tokyo/jp/ew/soft_black_hole/
ダメになるクッションが敷き詰められている感じ。
普通に歩けない人もいるし、押すなよ押すなよ、をやっている人もいます。
まだ序盤なのにうとうとしている人も。
こんな部屋がオフィスにあったら・・・
(写真なし)  
 
 

The Infinite Crystal Universe

https://planets.teamlab.art/tokyo/jp/ew/infinite_crystaluniverse/
f:id:watabe1028:20180806122430j:plain
やっと来れましたCrystal Universe!
前回のPlanetsの時はラボにいたのですが
納期前で社員公開の時に行けませんでした。
トテモキレイ!
みんなインスタに上げるために必死で撮影してましたw
 
 
 

Drawing on the Water Surface Created by the Dance of Koi and People

https://planets.teamlab.art/tokyo/jp/ew/koi_and_people/
f:id:watabe1028:20180806122524j:plain
f:id:watabe1028:20180806122549j:plain
結構深くて膝下ギリギリまでくる!
くっ足が短いか・・・
小さい子供たちが次々と水に飛び込んでいく・・・
次に行けないじゃん。。。
(服が濡れてると次に進めません、とアナウンスあり)
 
 
 

Expanding Three-dimensional Existence in Intentionally Transforming Space - Free Floating, 12 Colors

https://planets.teamlab.art/tokyo/jp/ew/transformingspace/
f:id:watabe1028:20180806122618j:plain
f:id:watabe1028:20180806122650j:plain
チームラボボールがたくさん!
背の高い人はガシガシ頭にぶつかるからご注意を!
これ、子供は楽しいだろうな。大人でもはしゃぐし。。。
 
 
 

Floating in the Falling Universe of Flowers

https://planets.teamlab.art/tokyo/jp/ew/fitfuof/
f:id:watabe1028:20180806122740j:plain
床に座ってみれます。
寝っころがっても見れる。
何種類もの花が咲いたり散ったり・・・
余裕で1時間くらいいれそう。
同じ景色が一切ないので本当にずっと見ていられる。。。
 
 
 

Cold Life

https://planets.teamlab.art/tokyo/jp/ew/coldlife/
見逃しました。
どこにあったんだろ?
分岐があったのか、見ていたのに気づかなかったのか、残念。
 
 
 

最後に

遅くまでやっているので仕事帰りでも十分寄れます。
普通に回って2時間くらい。
足を水に付けれるので涼めますw
(中は冷房も聞いてますし)
 
また当日券も買えるので思いつきでいけます。
チケットもそこそこ安い。
 
今はレストランもオープンしてるみたいです。
ステーキやロブスターが食べられるお店
ポテトやサンドイッチが食べられるお店みたいです。
 
 
仕事帰りアートを見てディナーとか良さげですね。
自分は終電あるから無理ですけどね!

デザインあ展 in Tokyo に行ってきた

f:id:watabe1028:20180803123133j:plain
こんちは。
今回はイベントレポートです。
もちろん、ただの遊びです。

突然ですが、みなさん「デザインあ」をご存知ですか?
www.nhk.or.jp

Eテレヘビーユーザーの自分は「ピタゴラスイッチ」と「デザインあ」が大好きです。
そんな「デザインあ展 in Tokyo」に行ってきました。
久しぶりのレポ記事です。
ネタバレ注意です。自己責任で読んでください。
 
 

GAIYO

デザインあ展 in Tokyo
場所:日本科学未来館
期間:2018/7/19 〜 2018/10/18
時間:10 〜 17時
料金:1,600円(大人)
詳細:
www.design-ah-exhibition.jp

平日の11時ごろ行ったのですが、忘れていました。
世は夏休みだと言うことを。
元気の良い少年達が縦横無尽にはしゃいでいました・・・
その辺はお気をつけて。

早速レポ!
全部載せたいところですが面倒なので一部だけ紹介します!
 
 

TAMAGO!

f:id:watabe1028:20180803123933j:plain
見ての通り、たまごです。焼いたり茹でたり。
デザイン的な発想よりも食玩技術に驚きです。
 
 

TSUMIKI!

f:id:watabe1028:20180803123958j:plain
食器の形をした積み木のようなもの。
もくもくと木を積み上げていた少年に
「崩した方が負けね」と勝負を挑み、完勝してその場をさりました。
しょうもない大人ですね。
 
 

NANIWO?

f:id:watabe1028:20180803124035j:plain
何をやっているでしょうか?
多分けん玉です。(本当に多分)
(この膝の効かせ具合!)

f:id:watabe1028:20180803124644j:plain
これは傘です。(これも多分)
なるほどねーってなります。
 
 

MOSYA!

f:id:watabe1028:20180803124725j:plain
これは街にあるフォントを模写していました。
フォント大好きなので(最近は筑紫A丸ゴシック推し)上がりました。
 
 

MO、MOJI?

f:id:watabe1028:20180803124806j:plain
おわかりいただけただろうか?

わかりにくいのでアップを。
f:id:watabe1028:20180803124832j:plain
まだわかりにくいですかね?

f:id:watabe1028:20180803124900j:plain
も、もみあげ!

これらは全てテキストにされていました。
これすごい!
実際に見て欲しい!
本当に全部テキストになってる!!
 
 

SAIGONI・・・

この記事では全体の5分の1も紹介出来ていません。
また、写真よりも実際に見た方が絶対面白いです。
値段も1,600円(大人)と高くないので是非見に行ってください。
だいたい2時間くらいで回れるのでお台場でショッピング前なんかにいいですね。
10月までやってます!

最後に説明が面倒だったので書くのをやめた写真をいくつかあげておきます。

f:id:watabe1028:20180803125018j:plain
f:id:watabe1028:20180803125050j:plain
f:id:watabe1028:20180803125122j:plain
f:id:watabe1028:20180803125147j:plain

DockerでMySQL8環境をつくるよ

ローカル環境に影響なしでMySQL8を試してみてエだろ…"お気軽"にヨ…

Docker for Macをインストール

www.docker.com

$ brew update
$ brew cask install docker

VirtualBoxのこととかは考えなくてよい

Docker for Macだと
VirtualBox上にDockerホストとなるマシンを自分で作成とかなんもしないでも
デフォルトでよしなにやってくれるのだそうで(HyperKitというのでLinux VMがつくられる、軽量)
これに明確に分かるまで激しく遠回りしてしまった
最初から公式ドキュメント読んでいればそれでよかったんだ
Docker Toolbox + VirtualBoxのブログ記事がおおくて…惑わされた…

公式のMySQLイメージを入手

https://hub.docker.com/_/mysql/

$ docker pull mysql:latest
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
mysql               latest              3da0f4853002        36 hours ago        445MB

ウム

MySQLイメージからコンテナの作成と起動

$ docker run --name mysql8.0.12 -e MYSQL_ROOT_PASSWORD=xxxxx -p 3307:3306 -d mysql:latest

ローカルのクライアント側からは3307ポートでアクセスしようと思う

起動中のプロセスリストを見てみる

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
7712194c222b        mysql:latest        "docker-entrypoint.s…"   7 seconds ago       Up 7 seconds        0.0.0.0:3307->3306/tcp   mysql8.0.12

ウム

このコンテナに接続して中身を確認してみる

$ docker exec -it 7712194c222b bash

root@7712194c222b:/# mysql --version
mysql  Ver 8.0.12 for Linux on x86_64 (MySQL Community Server - GPL)

ちゃんと8が入っている〜

ユーザ作成

root@7712194c222b:/# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.12 MySQL Community Server - GPL

mysql> USE mysql;

mysql> CREATE USER user1 IDENTIFIED BY 'yyyyy';

mysql> CREATE DATABASE db_name;

mysql> GRANT ALL PRIVILEGES ON db_name.* TO 'user1'@'%';

ローカルのMySQLクライアントからコンテナのDBに接続

$ mysql -u user1 -p --port 3307
Enter password: 
ERROR 1045 (28000): Access denied for user 'user1'@'localhost' (using password: YES)

できね〜

$ mysql -u user1 -p -h localhost --port 3307
Enter password: 
ERROR 1045 (28000): Access denied for user 'user1'@'localhost' (using password: YES)

できね〜

www.bunkei-programmer.net

ホストは127.0.0.1で指定でどうか

$ mysql -u user1 -p -h 127.0.0.1 --port 3307
Enter password: 
ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded: dlopen(/usr/local/Cellar/mysql/5.7.21/lib/plugin/caching_sha2_password.so, 2): image not found

ヌ"ア"ア"

qiita.com

パスワードの認証方法が変わっているのだね

外部からのパスワード認証で接続可能なユーザーを作成する

Dockerコンテナ内に入って

$ docker exec -it 7712194c222b bash

mysql_native_passwordで認証するユーザー作成

root@7712194c222b:/# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.12 MySQL Community Server - GPL

mysql> CREATE USER nativepassuser IDENTIFIED WITH mysql_native_password BY 'yyyyy';

mysql> SELECT user, host,  plugin, authentication_string FROM user;
+------------------+-----------+-----------------------+------------------------------------------------------------------------+
| user             | host      | plugin                | authentication_string                                                  |
+------------------+-----------+-----------------------+------------------------------------------------------------------------+
| nativepassuser   | %         | mysql_native_password | *D7D00ED2D87ECF8666F08E30C4A6EB61AB9982FC                              |
| user1            | %         | caching_sha2_password | $A$005$~j\'SF)I2)
oVUJMUxJKcEdZom3QAppyIBBX0vu5Rh/fr.g.fbw7YMe15 |
| mysql.infoschema | localhost | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| mysql.session    | localhost | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| mysql.sys        | localhost | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
/hN>ot             | localhost | caching_sha2_password | $A$005$NrwN
    &r(:NjHjf693xvJ9AQzt5F9J4D885VzpXnduNOLcJ.TcoYc4 |
+------------------+-----------+-----------------------+------------------------------------------------------------------------+
6 rows in set (0.00 sec)

再度ローカルのクライアントからコンテナのDBに接続を試す…

$ mysql -u nativepassuser -p -h 127.0.0.1 --port 3307
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 15
Server version: 8.0.12 MySQL Community Server - GPL

いけました!!!!ヴォエ!!!!
これでやりたい放題できますね

感想

お気軽ではなかったが勉強にはなった!ポジティブバカの言葉です

Let's { Dart } before we { Flutter }



The above program was written in Dart and it shouldn't come to you as a surprise if you are from Java or C programming background. (above lines are editable so go ahead and replace my name with yours and press play button ▶️)

So, what is Dart???
An overview of Dart on Ecma website reads as

Dart is a class-based, single-inheritance, pure object-oriented programming language. Dart is optionally typed and supports reified generics.

If that thing was steaming pile of mumbo-jumbo to you then allow me to explain in my subsequent blog named "Getting Started with Dart".

The greatest innovations are the ones we take for granted, like light bulbs, refrigeration, and penicillin.

However; in a world where the miraculous very quickly becomes common-place, how can a company, especially one as big as Google, maintain a spirit of innovation year after year? 🤔

While you are thinking about it... eureka!! there is already something new in the market and you gotta learn it, because...
Once a new technology rolls over you, if you're not part of the steamroller, you're part of the road.
With no surprise, Google this year came with the complete set of frameworks for all your needs, with its own programming language {Dart}.

1) Web-Applications (AngularDart)

AngularDart is a web app framework that focuses on productivity, performance, and stability.

2) Android and iOS Applications (Flutter)

Flutter is Google’s mobile app SDK for crafting high-quality native interfaces on iOS and Android in record time using only single code base.


So let's have a tour of these things together...

f:id:vivek081166:20180723142405p:plain


See you in the subsequent blog post with

"Getting Started with Dart"

till then happy learning 😊

VAPIDを用いたデスクトップ通知のAngular6とJavaによる実装

こんにちは。
新卒の増田です。

今回、クライアント(フロントエンド)にAngular6、サーバー(バックエンド)にPlayFramework(Java)を使ってデスクトップ通知を実装しようとした際、あまり参考になる資料がなかったのでここに残しておきます。

VAPIDやPush通知に関する細かい仕組みについては他のサイト(ここなど)を参照していただいて、で、実際に何を書けば良いのかにだけ焦点を当てて紹介していきます。

僕はエンジニア新生児なので、初めから書いていきます。
何もないところからデスクトップ通知ができるように。
もう本当に初めの方から。
ただ、sbtやAngular CLIなどのツールはこっそり入っているという前提です。

使っているもの

  • PlayFramework 2.6
  • Java 1.8
  • Eclipse 4.7
  • Angular6
  • TypeScript
  • WebStorm

ディレクトリとプロジェクトの作成

まず、適当なところにフォルダを作ります。名前も適当に。

$ mkdir notification
$ cd notification

そしてそこにクラアイアント(Angular)とサーバー(PlayFramework)、それぞれのプロジェクトを作成します。コマンドで。
まずはサーバー側。

notification$ sbt new playframework/play-java-seed.g8

name [play-java-seed]: backend
organization [com.example]: 
scala_version [2.12.6]: 
play_version [2.6.16]: 
sbt_version [1.1.6]: 

色々聞かれますが、とりあえず名前だけbackendと付けてあげてあとはEnterキーです。
もちろん名前もなくて良いです。お好みで。

次はクライアント側。

notification$ ng new frontend

こちらも名前はお好みで。
さて、これでとりあえずはディレクトリができました。いえい。

サーバー側の準備

サーバー側から触っていくことにします。
というわけで何も考えず

notification$ cd backend

そして今回はEclipseを使っていくので、project/plugins.sbtに

addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4")

を書き加え、ついでに今回必要なものは全てありがたいライブラリを使わせていただくので、build.sbtにあれこれ追記します。

libraryDependencies ++= Seq(
  filters,
  "org.bouncycastle" % "bcprov-jdk15on" % "1.54",
  "nl.martijndwars" % "web-push" % "3.1.0",
  "commons-io" % "commons-io" % "2.4"
)

はい。
ここで、plugins.sbtやbuild.sbtに追加したものをEclipseに適用するために

backend$ sbt eclipse

をしておきます。
次は、サーバーとクライアントで通信をするのですが、このままだとクライアントからサーバーにリクエストを送ってもCORSで弾かれてしまいます。CORSについては割愛します。僕もよく知りません。

とりあえず、application.confに

play.filters {
  ## CORS filter configuration
  # https://www.playframework.com/documentation/latest/CorsFilter
  # ~~~~~
  # CORS is a protocol that allows web applications to make requests from the browser
  # across different domains.
  # NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has
  # dependencies on CORS settings.
  enabled += "play.filters.cors.CORSFilter"
  cors {
    # Filter paths by a whitelist of path prefixes
    pathPrefixes = ["/"]

    # The allowed origins. If null, all origins are allowed.
    allowedOrigins = null

    # The allowed HTTP methods. If null, all methods are allowed
    allowedHttpMethods = null
    allowedHttpHeaders = null

    #preflightMaxAge = 3 days
  }
}

と書き加えた上で(コメント部分はもちろんなくても)、appの下にFilters.javaというクラスを作成します。Eclipseで普通にクラスを作成するとデフォルトパッケージの下に来ますが気にしません。
で、Filters.javaに

import javax.inject.Inject;

import play.filters.cors.CORSFilter;
import play.http.DefaultHttpFilters;
import play.mvc.EssentialFilter;

public class Filters extends DefaultHttpFilters {

    CORSFilter corsFilter;

    @Inject
    public Filters(CORSFilter corsFilter) {
        super(corsFilter);
        this.corsFilter = corsFilter;
       }

    public EssentialFilter[] filters() {
        return new EssentialFilter[] { corsFilter.asJava() };
    }
}

というのを貼り付けましょう。
PlayFrameworkのHPからサンプルをダウンロードすると初めからあるような気がするので、
そこからコピペしても良いと思います。これでCORSで弾かれなくなりました。幸せ。

ここまででサーバー側の設定はおしまいです。

クライアント側の準備

次はクライアント側の準備に移ります。
とりあえずfrontendディレクトリに移って、まずはこのプロジェクトをPWA化しましょう。
デスクトップ通知にはサービスワーカーが欠かせないらしいので、そのためです。
サービスワーカーが何であるのかについては世に遍在している説明を発見していただければ。

frontend$ ng add @angular/pwa —project frontend
frontend$ npm i @angular/service-worker --save -D

クライアント側の設定はこれでおしまいです。Angular CLIさんが必要な記述も全部してくれていますので。ちなみに ここなどを参考にしています。 参考ページではindex.htmlとmanifest.jsonを修正していますが、必要ありませんでした。

というわけで、これでサービスワーカーを使えるようになりました。

ここまでで必要な準備が終わったのでやっと本題に入れます。
デスクトップ通知の実装へ。

サーバー側でECDSAによる鍵ペアを生成

鍵ペアの生成には初めに準備しておいたライブラリを使わせていただきます。
ECDSAというのがどういうものなのかよく知りません。
知りませんが、それでも使えるというのが大事ということで。

鍵ペアですが、実は毎回作成する必要はなく、アルゴリズムに従って生成された鍵ペアさえあれば良いので、一度作ったものをそのままペタッ。

とりあえず一度生成すべく、Eclipseでapp下に何か適当なメインクラスを作成します。
本当はパッケージなど分けた方が良いのでしょうが、そういう細かいことは必要に迫られたときに。
例えば表示するだけならこのように、VapidKey.javaのようなクラスを作成します。

import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import nl.martijndwars.webpush.cli.commands.GenerateKeyCommand;
import nl.martijndwars.webpush.cli.handlers.GenerateKeyHandler;

public class VapidKey {

    public static void main(String[] args) {
        try {
            new VapidKey().generateKeys();
        } catch (Throwable th) {
            th.printStackTrace();
        }

    }

    public void generateKeys() throws Throwable {
        Security.addProvider(new BouncyCastleProvider());
        GenerateKeyHandler gh = new GenerateKeyHandler(new GenerateKeyCommand());
        gh.run();
    }
}

全部mainに入れれば良いじゃんと思いつつ。
ただ、何もせずにこれを実行すると、たぶん「メインクラスが見つかりません」などと言われます。
なので、

backend$ sbt compile

をして、プロジェクトをインポートし直し、ここにあるように、ビルドパスを設定します。

  • Eclipse上でプロジェクトを右クリック > Build Path > Configure Build Path...
  • LibraryタブからAdd Class Folderを選択
  • 開いたダイアログでtarget/scala-2.12/classesにチェックを入れてOK

そしたら実行できるようになるはずです。
実行できました。できたらコンソールに

PublicKey:
BP198Ghlpaac41UMKrRvYyNt56tt7JCcgusX1JSh1e3W-kApskF2BLqCNi0c2GBjPx5BflPezwvi0OwVsaGTclI=
PrivateKey:
Kzh4c_SbGLXoybns7pCiRdvhx9b0m_H5AEyD8DD9Bvw=

のように出力されるのではないかと。

繰り返しになりますが、アルゴリズムに従って生成された鍵ペアさえあれば良いので、上記のものをそのまま利用してもらっても大丈夫なはずです(なのでこのクラスを作らずに上の鍵を使うのでも良いような)。セキュリティ的にどうなのかというのはひとまずおいといて。

生成したサーバー鍵をクライアント側に送信

クライアントからリクエストを送って、そのレスポンスでサーバー鍵を渡します。
HomeController.javaに

import play.libs.Json;

public Result vapidKey() {
     // さっきの鍵をそのままペタッ
     String serverKey = "BP198Ghlpaac41UMKrRvYyNt56tt7JCcgusX1JSh1e3W-kApskF2BLqCNi0c2GBjPx5BflPezwvi0OwVsaGTclI=";
     // サーバーキーを返すだけ
     return ok(Json.toJson(serverKey);
}

というように追記した上で、routesに

GET /vapidKey        controllers.HomeController.vapidKey

としておくことで、この/vapidKeyにリクエストが送られたときにサーバーの公開鍵を返すようになります。
普通です。

プッシュサーバーにサーバー鍵を登録

subscriptionからエンドポイントやクライアントの公開鍵等を取得

取得した諸々の情報をサーバー側へ送信

クライアントでPush通知を受け取った際にデスクトップ通知として表示

4つまとめてやります。

app.component.tsを

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SwPush } from '@angular/service-worker';

@Componetn(中略)

export class AppComponent implements OnInit {

    constructor(private swPush: SwPush, private http: HttpClient) {

        // Push通知が送られて来たときに受け取るやつ
        this.swPush.messages.subscribe(
            (message) => {

                const options = {
                    body:  message['message'],
                    // trueにするとユーザーから消されるるまで表示し続けられます。
                    requireInteraction: true,
                    // tag名が異なると異なる通知として出され(下にずらっと並ぶ)、
                    // tag名が同じものはそれ以前からある通知に上書きされるようです。
                    tag: 'notification_tag',
                    // これが何かは忘れてしまいました
                    renotify: false,
                };

                // どうしてかよく分かりませんが、Notificationをnewすると表示されるみたいです。
                const n = new Notification(message['title'], options);

            });
    }

    ngOnInit() {
        this.setDesktopNotification();
    }

    setDesktopNotification(): void {
        // サーバー公開鍵を受け取るべくリクエストを送信
        this.http.get('http://localhost:9000/vapidKey')
            .subscribe(
                // レスポンスに鍵が入っているはずなので、それをserverPublicKeyにセット
                (res) => {
                    this.swPush.requestSubscription({
                        serverPublicKey: res.toString()
                    }).then(
                        (pushSubscription: PushSubscription) => {

                            // 必要なものは全てpushSubscriptionに入っているので、アクセスすると取れます。
                            // endpointとp256dhとauthを取得
                            const endpoint = pushSubscription.endpoint;
                            const p256dh: string = btoa(String.fromCharCode.apply(null, new Uint8Array(pushSubscription.getKey('p256dh'))));
                            const auth: string = btoa(String.fromCharCode.apply(null, new Uint8Array(pushSubscription.getKey('auth'))));

                            const body = {
                                       endpoint: endpoint,
                                       p256dh: p256dh,
                                       auth: auth
                            };

                            // 送り先は/webPushという場所にしました。
                            this.http.post('http://localhost:9000/webPush', body).subscribe(
                                (res) => {
                                    // console.log('[App] Add subscriber request answer', res);
                                 },
                                 (err) => {
                                     console.log(err);
                                 });
                        }).catch(
                            err => console.log(err)
                        );
                },
                (err) => {
                    console.log(err);
                });
    }

}

として、
あとComponentでHttpClientを使うために、app.module.tsにちろっと追記しておきます。

import { HttpClientModule } from '@angular/common/http';

@NgModule({
    (中略)
    imports: [
        ...
        HttpClientModule,
        ...
    ],
    ()
})

サーバー側からメッセージをエンドポイントへと送ると、
プッシュサーバーがクライアント側にPush通知を送ってくれます。
あとはそれを受け取ってあれこれするとデスクトップ通知ができるのです。きっと。ここでそのプッシュサーバーからの通知を受け取る役目を負っているのが、constructorの中のthis.swPush.messages.subscribe()です。

ちなみに、Notificationのオプションはここに載っているものが使えるのではないかと勝手に思っています。「Instance properties」のところですね。

一つ引っかかったところがありまして、pushSubscriptionに入っているならそれをそのまま返せば良いじゃないかと初めはなったのですが、そのまま返すとなぜか文字が欠けてしまったりします(この理由がよく分かってません)。
なので、文字列に変換してから必要な分だけを渡しています。

というわけで、送信できるようになりました。

クライアント側から送られてくるリクエストを受け付ける窓口作成

当然のことながら、上記でクライアントから送った情報を受け取る窓口が必要になります。
さっきと同じようにして、HomeController.javaに

import play.mvc.Http.Request;

public Result webPush() {

        Request req = request();

        // ここの受け取り方はクライアントからどう送るかによって色々です。たぶん。
        JsonNode jn = req.body().asJson();
        String endpoint = jn.get("endpoint").asText();
        String p256dh = jn.get("p256dh").asText();
        String auth = jn.get("auth").asText();

        System.out.println("endpoint : " + endpoint);
        System.out.println("p256dh : " + p256dh);
        System.out.println("auth : " + auth);

        return ok(Json.toJson("OK!"));
}

と追記し、そしてroutesに、

POST /webPush        controllers.HomeController.webPush

というような感じで追記しておくと、クライアントから送られて来た情報がコンソールに出力されると思います。

ここまで来たら次はサーバーとクライアントで通信させる必要があるので、サーバーとクライアントをそれぞれbuildします。
まずはサーバー側を

backend$ sbt run

おしまい。

次はクライアント側

frontend$ ng build --prod
frontend$ cd dist/frontend
frontend$ http-server

突然出て来たhttp-serverというのは、ありがたい記事によると

このhttp-serverをインストールしておくと、任意のディレクトリでhttp-serverというコマンドを実行するだけでそのディレクトリをドキュメントルートにしたウェブサーバーが起動します。

というものらしいです。とりあえず

$ npm install -g http-server

をしておけば良いのではないでしょうか。あって困るものではないでしょうから。
そしてあとはlocalhost:8080をブラウザで開くと、通信が行われて、サーバーのコンソールに

endpoint : https://fcm.googleapis.com/fcm/send/eit1VHpri4g:APA91bFWD6MZw-ulAbKRlIfSyqUvRYGqcPfUFmxqkj4bWmUzUaBB-6OZyUmvy9Cob6WYHJH_JpWeU-C_nPsDO7yiMEqPXg6DXD3KXKiS85BgbKgQxcuD15_LK3s09yxeiqKcCrx6IUasFfBq0oQ6nt5b0SxiPXX4ug
p256dh : BOeKRtnhXWm+dSVAHP3ws7EE6cYUi83Lb6ny7+IweHniUSywQAc9BzZmVtVEyY5SKGxR4QGG6PAWNzlr5LhadA8=
auth : 4rIMQM5HTe04kclcRU2Rhw==

というように出力されるのではないかと(上手くいかなかったらリロードなどを)。
ちなみに、サーバーの鍵ペアはここに載せてあるものでも使えますが、エンドポイント等はここにあるものをコピペしてもたぶん送信できませんのでお気をつけて。

大事なことを一つ。あとでPush通知を飛ばしたときに受け取らないといけないので、フロント側は動かしたままに。

エンドポイントへとメッセージを送信

これも先のライブラリを使わせていただくと、 とっても簡単に送信できてしまいます。便利。
適当にapp下にWebPush.java(名前は適当に)をこんな感じで作成し、

import java.security.Security;

import org.apache.http.HttpResponse;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import com.google.gson.JsonObject;

import nl.martijndwars.webpush.Notification;
import nl.martijndwars.webpush.PushService;
import nl.martijndwars.webpush.Utils;

public class WebPush {

    public static void main(String[] args) {
            try {
                new WebPush().exec();
            } catch (Throwable th) {
                th.printStackTrace();
            }
    }

    public void exec() throws Throwable {

            Security.addProvider(new BouncyCastleProvider());

            // jsonから取得と書いていますが、出力されたものをコピペです
            String endpoint = /* webPushに送られてくるjsonから取得(上記のendpointに相当) */;
            String userPublicKey = /* webPushに送られてくるjsonから取得(上記のp256dhに相当) */;
            String userAuth = /* webPushに送られてくるjsonから取得(上記のauthに相当) */;

            // Base64 string server public/private key
            String vapidPublicKey = /* 初めに作ったサーバーの公開鍵 */;
            String vapidPrivateKey = /* 初めに作ったサーバーの秘密鍵 */;

            // Construct notification
            Notification notification = new Notification(endpoint, userPublicKey, userAuth, getPayload());

            // Construct push service
            PushService pushService = new PushService();
            pushService.setSubject("mailto:admin@martijndwars.nl");
            pushService.setPublicKey(Utils.loadPublicKey(vapidPublicKey));
            pushService.setPrivateKey(Utils.loadPrivateKey(vapidPrivateKey));

            // Send notification!
            HttpResponse httpResponse = pushService.send(notification);

    }

    private byte[] getPayload() {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("title", "おっはー");
            jsonObject.addProperty("message", "World");

            return jsonObject.toString().getBytes();
    }
}

これを実行すると、クライアントへとPush通知が飛ぶようになります。
別のメインクラスが実行されたりそもそも実行されなかったりするときは(たぶん普通には実行できないように思うのですが)、sbt compileをしてEclipseでインポートし直すなどしてみてください。それで実行できるのではないかと思います。もっと良い方法があるように思いつつ。

ここまでで必要な準備は終わったので、あとはさっき作ったWebPush.javaを実行してメッセージを送ってあげればデスクトップ通知ができるのではないでしょうか。たぶん。
というわけで実行しましょう!

f:id:s-masuda:20180719203906p:plain でけた!!!!

めでたしめでたし。

というわけでデスクトップ通知の大枠はできたのではないかと思いますので、あとは煮るなり焼くなり炙るなりすればきっと素晴らしいデスクトップ通知が。

こんな感じで、ほぼ入門程度の技術研修(それもJavaだけ!!)を終えたばかりの新卒なのに「AngularとPlay使ってデスクトップ通知実装してね!」などと突然言われてしまうような会社ではありますが、僕みたいな初心者でもこうしてちゃんと実装しきるところまで助けてもらえますので、もしよければどうぞ。そしてぜひ大阪に。

fresh-recruiting.goalist.co.jp

お待ちしております。

Swaggerを使ってRestful APIを記述する

こんにちは、ゴーリストのトゥアンです。

ウェブ開発でRestful APIはだんだん普及していると共に、Resful APIを記述するためにいろいろな仕方があります。

Swaggerを使うことは下記の通りに、いくつかの利点があります。

  • Amazon API Gatewayへ展開できる
  • フロントエンド開発のため、モックサーバーを作りやすい
  • OpenAPI仕様の基準に基づくAPIの入力(リクエスト)・出力(レスポンス)を定義できる

Swagger 3.0 は2017年07月にリリースされましたが、現在AWSはSwagger 2.0だけサポートしているので、今回の記事でSwagger 2.0について説明します。

Swaggerファイルのフォーマット

SwaggerでサポートしているフォーマットはYAMLとJSONです。

Swaggerファイルの構造は複数階層のオブジェクトです。ルートオブジェクトは「Swagger」というオブジェクトです。

1. Swagger オブジェクト

詳しい情報は下記のリンクにご参考ください。

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object

大切なフィールドは:

フィールド名 タイプ 説明
swagger String 必須です。"2.0"という固定な価値があります。
info Object 必須です。情報のオブジェクトです。後で説明します。
host String ホスト名又はホストのIPアドレスです。
basePath String 全部APIの共有なパスです
schemes String 全部APIのプロトコル(httpやhttps, ws, wssなど)です
paths Object 必須です。
全部APIを一つずつ記述するためのオブジェクトです。
後で説明します。
definitions Object 自由なオブジェクトを定義できます。後で説明します。

2. info オブジェクト

APIについてのメタデータです。 詳しい情報は下記のリンクにご参考ください。 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#info-object

大切なフィールドは:

フィールド名 タイプ 説明
title String 必須です。アプリケーションのタイトルです
version String 必須です。API記述仕様書のバーションです。

3. paths オブジェクト

APIの記述情報です。 詳しい情報は下記のリンクにご参考ください。 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#paths-object

大切なフィールドは:

フィールド名 タイプ 説明
/{path}
{path}は任意に定義できる
Object 定義されたpathに該当するAPIの記述です。
「Path Item」 というオブジェクトです。
後で説明します。

Path Itemオブジェクトの詳しい情報は下記のリンクにご参考ください。 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#paths-item-object

大切なフィールドは:

フィールド名 タイプ 説明
get Object 定義されたpathに該当するGETメソードの記述です。
「Operation」というオブジェクトです。後で説明します。
put Object pathに該当するPUTメソードの記述です。
タイプはgetのOperationオブジェクトと同じです。
post Object pathに該当するPOSTメソードの記述です。
タイプはgetのOperationオブジェクトと同じです。
delete Object 定義されたpathに該当するDELETEメソードの記述です。
タイプはgetのOperationオブジェクトと同じです。
options Object pathに該当するOPTIONSメソードの記述です。
タイプはgetのOperationオブジェクトと同じです。
head Object pathに該当するHEADメソードの記述です。
タイプはgetのOperationオブジェクトと同じです。
patch Object pathに該当するPATCHメソードの記述です。
タイプはgetのOperationオブジェクトと同じです。
parameters Array 全部のメソッドの共有なリクエストパラメーターです。
配列要素のタイプは以下のいずれかです。
・definitionsに自由に定義したオブジェクトに参考
・「Parameter」というオブジェクト。後で説明します。

4. operation オブジェクト

定義されたパスの一つのメソッドのリクエスト・レスポンスの記述です。
詳しい情報は下記のリンクにご参考ください。

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object

大切なフィールドは:

フィールド名 タイプ 説明
parameters Array メソードのリクエストパラメーターです。
配列要素のタイプは以下のいずれかです。
・definitionsに自由に定義したオブジェクトに参考
・「Parameter」というオブジェクト
responses Object レスポンスを記述するためのオブジェクトです。
後で説明します。

5. parameter オブジェクト

リクエストパラメーターの記述です。
詳しい情報は下記のリンクにご参考ください。

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameter-object

大切なフィールドは:

フィールド名 タイプ 説明
name String 必須です。パラメーターの名前です。
in String 必須です。リクエストにパラメーターを含める場所です。
価値は以下のいずれかです。
・"query"
・"header"
・"path"
・"formData"
・"body"
required Boolean 定義されたパラメーターは必須かどうか記述です。

6. responses オブジェクト

APIレスポンスの記述です。 詳しい情報は下記のリンクにご参考ください。 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#responses-object

大切なフィールドは:

フィールド名 タイプ 説明
{HTTP ステータスコード}
(400や404, 500など)
Object 定義されたHTTPステータスコードに該当するレスポンスの記述です。
タイプは以下のいずれかです。
・definitionsに自由に定義したオブジェクトに参考 
・「Response」 というオブジェクトです。後で説明します。

Responseオブジェクトの詳しい情報は下記のリンクにご参考ください。 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#responseObject

大切なフィールドは:

フィールド名 タイプ 説明
description String 必須です。レスポンスの概念記述です。
schema Object APIレスポンスの構造詳細記述です。後で説明します。

7. definitions オブジェクト

自由なオベジェクトの定義です。
詳しい情報は下記のリンクにご参考ください。

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#definitionsObject

definitions オブジェクトのフィールドは:

フィールド名 タイプ 説明
{name}
name: 定義したいオベジェクトの名前です。
Object 指定されたnameがあるオブジェクトの記述です。
タイプはresponseのschemaオブジェクトと同じです。
後で説明します。

SchemaオブジェクトはJSON Schema仕様書に基づいて定義されます。
JSON Schema仕様書は下記のリンクにご参考ください。
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#definitionsObject 
また、JSON Schema仕様書の他にSwaggerのSchemaオブジェクトは特別なフィールドもあります。
詳しい情報は下記のリンクにご参考ください。

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schema-object

大切なフィールドは:

フィールド名 タイプ 説明
type String 定義したいオブジェクトのタイプです。
サポートしているタイプは:
原始タイプ("integer"や"string"など)、
"array"と"object"です

typeが"array"である場合、下記のフィルードがあります。

フィールド名 タイプ 説明
items Object 配列要素を定義するためのSchemaオブジェクトです。

typeが"object"である場合、下記のフィルードがあります。

フィールド名 タイプ 説明
required Array 必須なフィルード名の配列です。
properties Object オブジェクトのフィルードを定義するための各Schema オブジェクトです。

サンプル

課題:https://example.swagger.io/v1/users/{user_id} パスのGETメソッドとPUTメソッドを記述します。

GETメソッド:
リクエストのパラーメータ:
+ user_id : パスにある
レスポンスのオブジェクト:ユーザの名前とユーザのメールアドレス

PUTメソッド:
リクエストのパラーメータ:
+ user_id : パスにある
+ name: ボディにある
+ email: ボディにある
レスポンスのオブジェクト:成功な場合に200コードを返却して、失敗な場合に400コードと404コードを返却します。

上記のAPIを記述するために、下記のSwaggerファイルを作ります。

swagger: "2.0"
info: # infoオブジェクト
  title: "Swaggerの例"
  version: "1.0.0"
host: "example.swagger.io"
basePath: "/v1"
schemes:
- "https"
paths: # pathsオブジェクト
  /user/{user_id}:
    get: # getメソッドのoperationオブジェクト
      tags:
      - "userAPI"
      summary: "ユーザーIDによって、ユーザー情報を取得する"
      operationId: "getUserByUserId"
      produces:
      - "application/json"
      parameters: # parameterオブジェクトの配列
      - name: "user_id"
        in: "path"
        description: "ユーザーID"
        required: true
        type: "number"
      responses: # responsesオブジェクト
        200:
          description: "成功なレスポンス"
          schema: # schemaオブジェクト
            $ref: "#/definitions/User" 
            # definitionsに定義されたオブジェクトの参考
        400:
          description: "無効なユーザーID"
        404:
          description: "ユーザーを見つけていません"
    put: # putメソッドのoperationオブジェクト
      tags:
      - "userAPI"
      summary: "ユーザーIDによって、ユーザー情報を更新する"
      operationId: "updateUserByUserId"
      produces:
      - "application/json"
      parameters: # parameterオブジェクトの配列
      - name: "user_id"
        in: "path"
        description: "ユーザーID"
        required: true
        type: "number"
      - in: "body"
        name: "body"
        required: true
        schema: # schemaオブジェクト
          $ref: "#/definitions/UpdateUserReq" 
          # definitionsに定義されたオベジェクの参考
      responses: # responsesオブジェクト
        200:
          description: "成功なレスポンス"
        400:
          description: "無効なユーザーID"
        404:
          description: "ユーザーを見つけていません"
definitions: # definitionsオブジェクト
  User: # schemaオブジェクト
    type: "object"
    properties:
      id: # schemaオブジェクト
        type: "integer"
        format: "int64"
      name: # schemaオブジェクト
        type: "string"
      email: # schemaオブジェクト
        type: "string"
  UpdateUserReq: # schemaオブジェクト
    type: "object"
    required:
      - "name"
      - "email"
    properties:
      name: # schemaオブジェクト
        type: "string"
      email: # schemaオブジェクト
        type: "string"

Swagger UI」を使って、記述したAPIは綺麗に表示できます。

API概要の表示 f:id:t-nguyen:20180709155906p:plain GETメソッドの表示 f:id:t-nguyen:20180709154630p:plain PUTメソッドの表示 f:id:t-nguyen:20180709154752p:plain

ツール

Swaggerを書くために、「Swagger Editor」を使って、エラーがある場合、すぐ報告は出ます。

Swaggerファイルを作成した後で、「Swagger UI」を使って、記述したAPIは綺麗に表示できます。

この二つのツールをローカル環境にダウンロードすることと、ウェブブラウザーにオンラインで使うことと、どちらもできます。

参考書類

今回の記事でSwagger 2.0について少し説明しました。
詳しい情報は下記のリンクにご参考ください。

バーション2.0 : https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
バーション3.0 : https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md

【求人情報ビッグデータ】HR EXPO 2018出展しています

こんにちは、開発部の飯尾です。
昨日7月11日水曜日、タイトル通りHR EXPOに参加してきました!

www.hr-expo.jp

企業の人事担当者さま向けに、サービス紹介・商談を行うイベントです。
ゴーリストもHR Tech領域で自社サービスを紹介してきました。

日本中の求人情報ビッグデータから、採用を読み解くHRogシリーズ

f:id:y-iio:20180712120616p:plain

https://chart.hrog.net/chart.hrog.net

map.hrog.net

ブースの様子
自社メディアのキャラクター「ふろぐん」カラーのTシャツで目立つ…

f:id:y-iio:20180712113445j:plain

私は現在、HRogチャートHRogマップの開発・運用を担当しています。
ときおり既存ユーザーの営業訪問に同行することはありますが
ど新規の営業イベントに参加するのは初です!

あんまり力になれてるわけでは無いですが

  • 呼び込みスクリプトの考案
  • とりあえず声出して足を止めてもらう

などなど、自分にもできることをがんばりました〜
普段営業メンバーがさらっとサービス紹介していることのすごさを実感ですね。

とはいえ「アルバイト採用担当されてるんですか?私が作ったサービスでこういうのあるんです!よかったらデモ触っていきませんか!」の最高の流れが実現できたので
個人的には満足です、ガハハ


このHR EXPOは明日13日金曜日まで開催中です。
ぜひゴーリストブースへ遊びに来てください!
\カワイラシイふろぐんうちわも配ってます/

こういうお祭り的なイベントもオモチロイものですね。
窓の外に出ると己の姿もよく見えると思います。

技術ブログとはいえ、採用のためにやってるものなので
社内エンジニアがどんな仕事してるかとか会社の雰囲気もこんなかんじで紹介していきます💪('ω'💪)