2018年10月30日火曜日

Twitter の OAuth を使ってログイン機能を作ってみた

概要

Twitter の OAuth 機能を使って Web アプリにログイン機能を付けてみました
アプリは Sinatra で作成します

環境

  • macOS 10.14
  • Ruby 2.5.1p57
    • oauth 0.5.4
    • twitter 6.2.0

Twitter アプリの作成

https://developer.twitter.com/en/apps から作成します
アプリを作成するのにいろいろと情報を登録しなければいけなくなったので頑張って登録します
App Details はこんな感じです
ポイントはコールバック用の URL で http://localhost:9292/redirect に設定しあとでアプリ側で実装します

twitter_oauth01.png

Permissions は Read and Write にしました
ログインだけであれば Read だけで OK です
twitter_oauth02.png

アプリを作成したら Keys and tokens から Consumer API KeysAPI keyAPI secret key をメモしておきます

ライブラリインストール

  • bundle init
  • vim Gemfile
gem "oauth"
gem "sinatra"
gem "twitter"
  • bundle install --path vendor

oauth でログインしたあとに Twitter API をコールするためのアクセストークンを使って API をコールします

サンプルアプリ

  • vim config.ru
require './app'
run TwitterOAuth
  • vim app.rb
require 'sinatra'
require 'oauth'
require 'twitter'

class TwitterOAuth < Sinatra::Base
  TWITTER_API_KEY = 'api-key'
  TWITTER_API_SECRET = 'api-secret'
  TWITTER_CALLBACK = 'http://localhost:9292/redirect'

  enable :sessions

  helpers do
    def oauth
      OAuth::Consumer.new(
        TWITTER_API_KEY,
        TWITTER_API_SECRET,
        :site => 'https://api.twitter.com',
        :schema => :header,
        :method => :post,
        :request_token_path => '/oauth/request_token',
        :access_token_path => '/oauth/access_token',
        :authorize_path => '/oauth/authorize'
      )
    end
  end

  get '/' do
    request_token = oauth.get_request_token(:oauth_callback => TWITTER_CALLBACK)
    session[:token] = request_token.token
    session[:secret] = request_token.secret
    redirect request_token.authorize_url
  end

  get '/redirect' do
    oauth_client = oauth
    request_token = OAuth::RequestToken.new(oauth_client, session[:token], session[:secret])
    access_token = oauth_client.get_access_token(request_token, :oauth_verifier => params[:oauth_verifier])
    # access_token.params['screen_name']
    twitter_client = Twitter::REST::Client.new do |config|
      config.consumer_key = TWITTER_API_KEY
      config.consumer_secret = TWITTER_API_SECRET
      config.access_token = access_token.token
      config.access_token_secret = access_token.secret
    end
    "Logged in #{twitter_client.user.name}"
  end
end

説明

まず / にアクセスすると oauth.get_request_token を使ってログイン認証用の URL を取得し認証画面にリダイレクトされます

そこで Twitter 認証すると /redirect に飛ばされ認証時に渡される :oauth_verifier を使って oauth_client.get_access_token でトークンを取得します
あとは Twitter API のクライアントを使ってユーザ情報を取得しています

動作確認

ログインしていないと初めは認証画面が表示されます
認証に成功するとアプリからのアクセスを許可するか確認する画面になるので許可します
twitter_oauth3.png

あとは localhost に戻ってきて Twitter API がコールされユーザ名がブラウザに表示されると思います

最後に

Twitter の OAuth を使って Web アプリのログイン機能を実装してみました
今回は oauth というライブラリを使いましたが Twitter の場合、使えるライブラリは他にもいろいろあるようです (omniauth-twitter, Twitter ライブラリを使ってもできるようです)
この辺りは環境に合わせて好きなものを選択すれば良いと思います

Twitter の場合アクセストークンにシークレットもあるのが他の OAuth と少し違うところかなと思います
また権限も API 単位というわけではなく大雑把に Read or Write という感じで設定するのも他と異なる点かなと思います

2018年10月29日月曜日

Google の OAuth 認証を使ってみた

概要

環境

  • macOS 10.14
  • Ruby 2.5.1p57
  • Google OAuth2 (2018/10/29 時点)
  • Google Drive API (2018/10/29 時点)

事前作業

GCP でプロジェクトを作成しておきましょう

Google Drive API の有効化

今回は認証後に Google Drive API をコールしてドライブ内にあるファイルの一覧を取得します
プロジェクトが Google Drive API を有効な状態にしておく必要があるので有効にします
google_oauth6.png

OAuth 認証画面の設定

これを設定しないとクライアント ID を作成できないので先に作成しておきます
設定するのはアプリケーション名だけで OK です
google_oauth2.png

クライアント ID の作成

OAuth 用のクライアント ID を作成します
作成の導線はたくさんあるのでお好きな方法で作成してください
google_oauth1.png

GCP では用途に合わせていろいろな認証情報が作成できます
今回は OAuth なので「OAuth クライアント ID」を選択します

クライアントのタイプを選択する必要があるので「ウェブアプリケーション」を選択しましょう
google_oauth3.png

あと大事なのはコールバック用の URL です
今回は localhost で開発するので http://localhost:9292/redirect と入力しましょう
あとでこのルーティングをアプリ上で実装します

クライアント ID を作成すると client_secret_279273981030-rj9ivjoip1ocvs3f5bh2ldoavk6erums.apps.googleusercontent.com.json というような名前の認証用の JSON ファイルをダウンロードできるようになります
ダウンロードして名前を client_secrets.json に変更しましょう

ライブラリインストール

それではアプリを作成します
まずはライブラリをインストールします

  • bundle init
  • vim Gemfile
gem "sinatra"
gem "google-api-client"
  • bundle install --path vendor

サンプルアプリ

  • vim config.ru
require './app'
run TestWebApp
  • vim app.rb
require 'sinatra/base'
require 'google/api_client/client_secrets'
require 'google/apis/drive_v2'
require 'json'

module Rack
  class Lint
    def call(env = nil)
      @app.call(env)
    end
  end
end

class TestWebApp < Sinatra::Base

  before do
    client_secrets = Google::APIClient::ClientSecrets.load
    @auth_client = client_secrets.to_authorization
  end

  get '/' do
    @auth_client.update!(
      :redirect_uri => 'http://localhost:9292/redirect',
      :scope => 'https://www.googleapis.com/auth/drive.metadata.readonly'
    )
    auth_uri = @auth_client.authorization_uri.to_s
    redirect auth_uri
  end

  get '/redirect' do
    @auth_client.code = request['code']
    @auth_client.fetch_access_token!
    auth_client = Signet::OAuth2::Client.new(JSON.parse(@auth_client.to_json))
    drive = Google::Apis::DriveV2::DriveService.new
    files = drive.list_files(options: { authorization: auth_client })
    "<pre>#{JSON.pretty_generate(files.to_h)}</pre>"
  end
end

説明

ポイントを説明します
before で client_secrets = Google::APIClient::ClientSecrets.load とすることでダウンロードして JSON ファイルを読み込みクライント ID とシークレットを設定します
更にそこからトークンを取得するための @auth_client を作成しておきましょう
今回は before でやっていますがリクエストごとに毎回やる必要はないです

@auto_client.update!redirect_urlscope を設定します
redirect_url は先程クライアント ID 作成時に設定した http://localhost:9292/redirect を設定します
また scope はこのアプリが使用する API を指定します
今回は Google Drive API を使ってドライブの情報を確認するので https://www.googleapis.com/auth/drive.metadata.readonly を指定します
スコープの一覧はこちらを参考にしてください

redirect_uri と scope を指定したら @auth_client.authorization_uri.to_s で認証ページの URI を取得します
あとはこれにリダイレクトします

認証が成功したあとは /redirect に戻ってきます
戻ってきたら code を取得します
これを使って更にリクエストすることで token を取得します
あとは Drive API ようのクライアントを作成して drive.list_files でドライブ内のファイルの一覧を取得しています

動作確認

  • bundle exec rackup config.ru

localhost:9292 にアクセスすると認証画面にリダイレクトされます
google_oauth4.png

認証が成功すると Google Drive API にアクセスしても良いか確認する画面になるので許可を選択します
google_oauth5.png

すると Google Drive API から取得した JSON の生データがブラウザで確認できると思います
あとはお好きなように HTML なりを組み立てれば独自の Google Drive ブラウザができます

最後に

Google の OAuth を使ってみました
基本は OAuth なのでそれにそってクライアントを使うだけです

ポイントになりそうなのはスコープの指定方法かなと思います
基本は URL で指定する必要があります
あとは大量にスコープがあるのでどの API がどのスコープに紐付いているかわかりづらいです
また取得したい情報がどのスコープを有効にすることで取得できるのかもわからいづらいかなと思います
その辺りは各種 Google の API ドキュメントを見るしかないかなと思います

参考サイト

2018年10月27日土曜日

Let's Encrypt で manual プラグインで作成した証明書を更新する方法

概要

証明書を一番初めに作成した際に --manual オプションを付与した場合の更新方法です
やり方は同じように証明書を取得するだけです

更新

  • sudo certbot certonly --manual --preferred-challenges=dns -d your.domain.com

--preferred-challenges=dns は DNS チャレンジの場合に付与してください
あとは指定の TXT レコードを追加するだけです

  • dig -t txt _acme-challenge.your.domain.com

でレコードが引けるまで次に進まないように注意してください

最後に

--manual + DNS チャレンジで自動更新する方法はあるのだろうか
もしやるとしたら certbot-external-auth:out-handler を使って DNS の API を使って TXT レコードを登録するスクリプトを作る感じになると思います

2018年10月26日金曜日

Google Map Platform を使ってみた

概要

Google Map API は Google Map Platform として生まれ変わりました
API も整理され用途に応じてプラットフォームが用意されています
今回は各プラットフォームで用意されている API を試してみました
無料枠でできる範囲ですが Google Map Platform は有料のサービスになっているのでご注意ください

環境

  • macOS 10.14
  • Google Map Platform (2018/10/25)

Google Map Platform の有効化

ここから登録します
Google Map Platform には「Maps」「Routes」「Places」という 3 つの機能に分類されています
とりあえずすべての API を有効化しましょう
google_map_platform1.png

次にプロジェクトを選択します
無料枠はありますが Google Map Platform は有料サービスなのでプロジェクトも請求可能なプロジェクトを選択しましょう
google_map_platform2.png

API が有効化するとキーが発行されるのでメモしておきましょう
google_map_platform3.png

Maps Javascript API を使ってみる

Maps の Javascript API を使ってみます
Javascript から API をコールして Web サイトなどに地図を埋め込むことができます
チュートリアルにサンプルがあるので動かしてみます

  • vim maps.htm
<!DOCTYPE html>
<html>
  <head>
    <title>Simple Map</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <style>
      #map {
        height: 100%;
      }
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      var myLatLng = {lat: 35.6811716, lng: 139.7648629};
      var map;
      function initMap() {
        map = new google.maps.Map(document.getElementById('map'), {
          center: myLatLng,
          zoom: 15
        });
        var marker = new google.maps.Marker({
          position: myLatLng,
          map: map,
          title: 'Hello World!'
        });
      }
    </script>
    <script src="https://maps.googleapis.com/maps/api/js?key=xxxxxxxxxxxxx&callback=initMap"
    async defer></script>
  </body>
</html>
  • open maps.html

key=xxxxxxxxxxxxx の部分は先程取得した API Key を入力してください

これだけで Google Map アプリを Web 上に表示できます
特にピンなどは立てず単純に地図を表示しているだけです
やっていることは単純で <div id="map"></div> のところに Maps API をコールした結果を埋め込んでいるだけです

Maps API から結果が返ってくると initMap が呼ばれその内容に応じて初めに表示する地図の内容を変更します

center は地図が表示されたときの画面中央に来る座標です
座標情報は Google Map などを使って確認することが可能です
サンプルは東京駅の座標です
google_map_platform5.png

zoom はマップを表示した際の初期の拡大率を設定できます
値は大きくなればなるほど拡大されます
拡大率と値の関係は以下の通りです

  • 1: World -> 世界地図全部が見える
  • 5: Landmass/continent -> 大陸全体が見えるくらい拡大
  • 10: City -> 都市全体が見えるくらい拡大
  • 15: Streets -> 道が見えるくらい拡大
  • 20: Buildings -> 建物名が見えるくらい拡大

Directions API を使ってみる

今度は Routes の Directions API の機能を使ってみます
経路をマップ上に表示することができます

まずスタートとゴールをもとに経路情報を取得します

  • curl 'https://maps.googleapis.com/maps/api/directions/json?origin=Disneyland&destination=Universal+Studios+Hollywood&key=xxxxxxxxxxxxx'

すると JSON 情報がずらーっと取得できると思います
デフォルトだと車での経路情報になります
他にも mode があり driving, walking, bicycling, transit などが指定できるようです

出発点 (origin) と行き先 (destination) を自然言語で指定できるのは Directions API を使う上で嬉しいポイントかなと思います

応用: Maps Javascript API + Directions API で経路を Web 上に表示する

Directions API は単純に JSON を取得するだけです
もしマップ上に経路を表示したい場合は Maps Javascript API と連携します
他もそうですが可視化するには基本的には Maps のどれかと連携する必要があります
ここのサンプルを紹介します

  • vim maps_directions.html
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <title>Directions Service</title>
    <style>
      #map {
        height: 100%;
      }
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
      #floating-panel {
        position: absolute;
        top: 10px;
        left: 25%;
        z-index: 5;
        background-color: #fff;
        padding: 5px;
        border: 1px solid #999;
        text-align: center;
        font-family: 'Roboto','sans-serif';
        line-height: 30px;
        padding-left: 10px;
      }
    </style>
  </head>
  <body>
    <div id="floating-panel">
    <b>Start: </b>
    <select id="start">
      <option value="chicago, il">Chicago</option>
      <option value="st louis, mo">St Louis</option>
      <option value="joplin, mo">Joplin, MO</option>
      <option value="oklahoma city, ok">Oklahoma City</option>
      <option value="amarillo, tx">Amarillo</option>
      <option value="gallup, nm">Gallup, NM</option>
      <option value="flagstaff, az">Flagstaff, AZ</option>
      <option value="winona, az">Winona</option>
      <option value="kingman, az">Kingman</option>
      <option value="barstow, ca">Barstow</option>
      <option value="san bernardino, ca">San Bernardino</option>
      <option value="los angeles, ca">Los Angeles</option>
    </select>
    <b>End: </b>
    <select id="end">
      <option value="chicago, il">Chicago</option>
      <option value="st louis, mo">St Louis</option>
      <option value="joplin, mo">Joplin, MO</option>
      <option value="oklahoma city, ok">Oklahoma City</option>
      <option value="amarillo, tx">Amarillo</option>
      <option value="gallup, nm">Gallup, NM</option>
      <option value="flagstaff, az">Flagstaff, AZ</option>
      <option value="winona, az">Winona</option>
      <option value="kingman, az">Kingman</option>
      <option value="barstow, ca">Barstow</option>
      <option value="san bernardino, ca">San Bernardino</option>
      <option value="los angeles, ca">Los Angeles</option>
    </select>
    </div>
    <div id="map"></div>
    <script>
      function initMap() {
        var directionsService = new google.maps.DirectionsService;
        var directionsDisplay = new google.maps.DirectionsRenderer;
        var map = new google.maps.Map(document.getElementById('map'), {
          zoom: 7,
          center: {lat: 41.85, lng: -87.65}
        });
        directionsDisplay.setMap(map);

        var onChangeHandler = function() {
          calculateAndDisplayRoute(directionsService, directionsDisplay);
        };
        document.getElementById('start').addEventListener('change', onChangeHandler);
        document.getElementById('end').addEventListener('change', onChangeHandler);
      }

      function calculateAndDisplayRoute(directionsService, directionsDisplay) {
        directionsService.route({
          origin: document.getElementById('start').value,
          destination: document.getElementById('end').value,
          travelMode: 'DRIVING'
        }, function(response, status) {
          if (status === 'OK') {
            directionsDisplay.setDirections(response);
          } else {
            window.alert('Directions request failed due to ' + status);
          }
        });
      }
    </script>
    <script async defer
    src="https://maps.googleapis.com/maps/api/js?key=xxxxxxxxxxxxx&callback=initMap">
    </script>
  </body>
</html>
  • open maps_directions.html

少し長いですがそこまで難しいことはしていません
ポイントは google.maps.DirectionsService で JSON 情報を取得し google.maps.DirectionsRenderer でマップ上に経路情報を表示します
google.maps.DirectionsRenderergoogle.maps.DirectionsService から取得したレスポンス情報をそのまま使えるようになっています (directionsDisplay.setDirections(response))

実際に経路情報を取得部分は directionsService.route になっており travelMode'DRIVING' になっています

あとは出発点と行き先を選択できるセレクトボックスの状態を監視して値が変化したら再度 DirectionsService を使って経路情報を取得するようにするだけです (document.getElementById('start').addEventListener('change', onChangeHandler))

google_map_platform4.png

テキスト情報を使って通知などで使う場合には Directions API だけでも十分かなと思います

Place Details API を使ってみる

最後に Place の Place Details API を使ってみます
この API は placeid と呼ばれる場所を特定する ID をもとにその場所の詳細情報を取得することができます

  • curl 'https://maps.googleapis.com/maps/api/place/details/json?placeid=ChIJN1t_tDeuEmsRUsoyG83frY4&fields=name,rating,formatted_phone_number&key=xxxxxxxxxxxxx'

サンプルは Google の本社の情報を取得しています

{
   "html_attributions" : [],
   "result" : {
      "formatted_phone_number" : "(02) 9374 4000",
      "name" : "Google",
      "rating" : 4.2
   },
   "status" : "OK"
}

Directions API 同様テキスト情報として取得できます
Maps Javascrip API と組み合わせてマップ上に表示したい場合はこの辺りを参考にしてみると良いかと思います

最後に

Google Map Platform の各プラットフォームの API を 1 つずつですが試してみました
経路や場所のテキスト情報を Routes や Places を使って取得して、それらを可視化したい場合に Maps を使うのが基本的な使い方だと思います

今回紹介した API はほんの一部です
Geolocation API や Street View API もあるので興味があれば調べてみてください
公式のドキュメントがかなり充実しているので特につまづくことなく使えると思います

おそろく強力な API なので使い方を知っておいて損はないかなと思います
作成された API Key は使用しないのであれば削除しておくことをおすすめします

参考サイト

2018年10月25日木曜日

git credential で独自のヘルパを作ってみた

概要

前回 git-credential のビルトインの機能を使ってみました
そこでヘルパは任意のスクリプトで実装できることを知りました
今回は独自のヘルパを Ruby + Redis で作ってみました
Redis に認証情報を格納してそれを取り出し Github などの認証ができるようになります

バージョン

  • Ubuntu 18.01
  • Ruby 2.5.1p57
  • Redis 4.0.9

事前準備

  • apt -y install redis-server

ライブラリインストール

  • gem install redis

スクリプトを PATH 上に配置する関係でグローバルインストールします

ヘルパスクリプト

  • vim /usr/local/bin/git-credential-redis-helper
#!/usr/bin/env ruby

require 'redis'
require 'json'
require 'erb'
require 'securerandom'

class RedisHelper
  def initialize
    @redis = Redis.new
  end

  def list
    @redis.keys.map { |key|
      {key => @redis.get(key)}
    }
  end

  def get
    known = {}
    while line = STDIN.gets
      break if line.strip == ''
      k,v = line.strip.split '=', 2
      known[k] = v
    end
    @auth = {}
    list.each { |auths|
      auths.each { |k,v|
        auth = JSON.parse(v)
        if auth["protocol"] == known["protocol"] and auth["host"] == known["host"]
          @auth = auth
          break
        end
      }
    }
    erb = ERB.new(File.read("/usr/local/bin/auth.erb"))
    erb.result(binding)
  end

  def store
  end  

  def qstore(key, value)
    @redis.set(key, value)
  end

  def erase(key)
    @redis.del(key)
  end
end

def main
  rh = RedisHelper.new
  command = ARGV[0]
  case command
  when 'list' then
    puts rh.list
  when 'get' then
    puts rh.get
  when 'store' then
    rh.store
  when 'qstore' then
    rh.qstore(ARGV[1], ARGV[2])
  when 'erase' then
    rh.erase(ARGV[1])
  else
  end
end
  • chmod 755 /usr/local/bin/git-credential-redis-helper

拡張子なしで作成するためシェバングを付与しています
git からコールされるため実行権限を付与します

説明

main 関数を用意したのでそこから見ると良いと思います
引数のコマンドに応じて redis に対する処理が変わってきます

get

必ず実装しなければいけないのが get です

get は標準入力を受け取って、その入力条件に当てはまる認証情報を redis から取得します
そして決められたフォーマットで認証情報を標準出力します
今回の redis のフォーマットは SET を使って key + hash として認証情報を登録することを想定しています
hash 側に認証情報があるのでそこと比較して当てはまるものがあれば erb でフォーマットして出力している感じです

redis に登録する際のフォーマットは改良の余地ありかなと思います
また、erb も必須ではありません
改行などもあるので、今回は使いましたが直接 puts しても問題ないです

qstore

引数に key + hash を取りそれをそのまま redis に格納します
store とコマンド名がかぶらないように q を付与しています

list

list は確認用の便利コマンドとして用意しました
redis 内にあるすべての key 情報に対してデータを取得して表示してくれます

erase

指定の key を redis から削除してくれます

store

不明
後で実行方法を紹介しますが、おそらくこれがちゃんと実装されていれば一度目の認証情報をちゃんと redis に格納できるようになるんだと思います

テンプレートファイルの配置

  • vim /usr/local/bin/auth.erb
protocol=<%= @auth["protocol"] %>
host=<%= @auth["host"] %>
username=<%= @auth["username"] %>
password=<%= @auth["password"] %>

これは正直なくてもいいかもしれません
直接スクリプト内で puts しても OK です

動作確認 (使い方)

redis-server は起動しておきましょう
また現状は localhost:6379 でのみ動作していることを想定しています

まず認証情報を登録します

git credential-redis-helper qstore 'github' '{"protocol":"https","host":"github.com","username":"hawksnowlog","password":"xxxxxxxx"}'

こんな感じです
別に Github の情報でなくても OK です
登録した認証情報は list コマンドで確認できます

  • git credential-redis-helper list

また git credential-redis-helper get はインタラクティブに認証情報を検索することができます
これは store や cache ヘルパと同じ仕様になります

登録したら認証が聞かれるか確認してみましょう
Github の場合であれば push 時に認証が聞かれなければ OK です

登録した認証情報を削除したい場合は

  • git credential-redis-helper erase github

で可能です
もちろん redis-cli でログインして DEL しても OK です

最後に

git credential で独自のヘルパスクリプトを実装してみました
今回は Ruby + Redis で開発しましたが、同じように別の言語や RDB でも実装可能です
store の挙動がよくわからなかったので今回の仕様では事前に redis に認証情報を登録する必要が出てしましました
本来は一度目の認証を redis に登録してそれ以降は redis にある認証情報を使いたかったのです

その辺りの問題を解決して、もう少しリファクタリングすれば公開できるかなと思います (ホスト名の指定やパスワード情報の暗号化なども実装したほうが良いかなと思います)
redis をインストールする必要もあるので需要があるか不明ですが

2018年10月24日水曜日

git credential を使おう

概要

git の認証を毎回入力するのは面倒なので localhost 上に認証情報を保存することができます
今回は基本的な使い方を紹介します

環境

  • Ubuntu 16.04
  • git 2.7.4

永続的に保存 (store)

認証情報をファイルに保存する方法です
認証情報が消えることはないので登録すればそれ以降はパスワードを尋ねられることはないですが残念ながら保存方法が平文しかありません

永続的に保存する場合は store を使います

  • git config --global credential.helper store
  • cat ~/.gitconfig
[credential]
        helper = store

これで store を使う準備ができました
あとは認証情報を記載したファイルを作成するだけです

  • git credential-store store

で対話的に登録することができます
以下のような感じで入力していけば OK です

protocol=https
host=github.com
username=hawksnowlog
password=xxxxxxxxxxxxxx

Ubuntu の場合、認証情報が登録されるデフォルトのファイルは ~/.git-credentials になります
中身を確認すると平分で保存されているのが分かると思います

https://hawksnowlog:xxxxxxxxxxxxxx@github.com
  • git credential fill

で対話的に認証情報を検索することもできます

暗号化するには

とは言えパスワード情報なので暗号化したくなるので普通です
その場合には git-credential-libsecret を使います
ただこれを条件として

  • Xserver が必要
  • git の 2.11+ が必要

があります
なので使える状況としてはデスクトップ Ubuntu として使っている場合かなと思います

一応使う場合は以下の通りです
まず Ubuntu で git の最新版を使えるようにします

  • add-apt-repository ppa:git-core/ppa
  • apt -y install git

これで最新版が使えるようになります
あとは libsecret helper を作成し登録します

  • make --directory=/usr/share/doc/git/contrib/credential/libsecret/
  • git config --global credential.helper /usr/share/doc/git/contrib/credential/libsecret/git-credential-libsecret
  • cat ~/.gitconfig
[credential]
        helper = /usr/share/doc/git/contrib/credential/libsecret/git-credential-libsecret

これで一度パスワードを聞かれたあとに認証情報を入力すれば、その後は聞かれなくなると思います

ちなみに Mac で Github などに認証するときにパスワードを聞かれなくなるのは osxkeychain という helper を使っているためです

  • git config --list
credential.helper=osxkeychain

macOS に含まれてる「キーチェーンアクセス」という認証情報を管理する仕組みを使っています
キーチェーンアクセスを開いて保存されているパスワードの一覧を開くと Github の認証が保存されているのを確認できると思います
git_credential1.png

Tips: git-credential-gnome-keyring は deprecated になっている

以下の方法は deprecated になっているようです

  • apt install -y libgnome-keyring-dev
  • make --directory=/usr/share/doc/git/contrib/credential/gnome-keyring
  • git config --global credential.helper /usr/share/doc/git/contrib/credential/gnome-keyring/git-credential-gnome-keyring

一時的に保存 (cache)

認証情報をメモリに一時的に保存する方法もあります
デーモンが常駐しそのデーモンが認証情報をキャッシュしてくれます
指定された timeout が過ぎると自動的に認証情報が削除されます
helper に cache を登録すれば完了です

  • git config --global credential.helper cache
  • cat ~/.gitconfig
[credential]
       helper = cache

timeout のデフォルトは 900sec (15miin) になっているようです
--timeout で指定することもできます

  • git config --global credential.helper cache --timeout 30000

cache の場合指定時間で認証情報は消えてしまいますが store のように平文ファイルを作らないため安全です
また緊急で全キャッシュを削除したい場合は

  • git credential-cache exit

で可能です

実は helper は自分で作れる

これまでビルトインである store と cache を紹介しましたが同じように自分でも helper を作ることはできます
git 側で決められたフォーマットで出力すれば良いだけなので簡単に作れます
このページに簡単な Ruby のスクリプトヘルパもあります

例えば認証情報を格納した json ファイルからパスワードを取り出して認証させるみたいなヘルパを作成することもできます

この辺の git のアーキテクチャは素晴らしいなと思いました

最後に

git-credential を使ってみました
ヘルパという外部のプログラムと連携することで認証情報を任意の場所に補完して取り出すことができるようになります
ビルトインは store と cache が使えるようです

またヘルパは独自で作成することもできます
認証情報をすでに管理している仕組みがあるのであればそこにアクセスして取ってくるみたいなことができるようになります

参考サイト

2018年10月17日水曜日

Xcode10 + mojave で CreateML を触ってみる

概要

CreateML が正式に試せるようになったので Playground でとりあえず試してみました
学習されるデータは画像情報になります

環境

  • macOS 10.14
  • Xcode 10.0

Playground なプロジェクトを作成する

CreateML を最も簡単に試せるのは Playground です
Xcode を立ち上げて「Get started with a playground」を選択します
createml1.png

macOS を選択する

Playground は macOS アプリとして動作させるので macOS を選択しましょう
以下では Single View を選択していますが何でも OK です
createml2.png

コーディング

以下のコードを貼り付けましょう

import CreateMLUI

let builder = MLImageClassifierBuilder()
builder.showInLiveView()

左側の矢印から Playground を実行します
そして右上の「Show the Assistant editor」から Live view を表示します
createml3.png

Live view が表示されない場合は Xcode を立ち上げて Assistant editor を開いて少し待ってみましょう
ダイアログが表示され許可すると Live view が立ち上がると思います
それでもダメな場合は再起動して許可のダイアログが表示されるまで繰り返してください

学習および評価データ

Playground の CreateML は画像情報を学習し評価してくれます
今回は犬と猫の画像データをここから取得しました
犬と猫の画像データがそれぞれ 1 万件ほどあります
学習データのラベル付けやテストデータのラベル付けはディレクトリに分けるだけで OK です

今回は犬の学習データ (train/dog) とテストデータ (test/dog)
そして猫の学習データ (train/cat) とテストデータを (test/cat) をそれぞれ 100 ファイルづつコピーして使いました

1 万ファイルすべてを使いたい場合は 9000:1000 くらいで学習:テストで分けると良いと思います

$ tree -d
.
├── test
│   ├── cat (1.jpg から 100.jpgをコピー)
│   └── dog (1.jpg から 100.jpgをコピー)
└── train
    ├── cat (101.jpg から 200.jpgをコピー)
    └── dog (101.jpg から 200.jpgをコピー)

学習データと評価データの選択

Live view にプルダウンがあるのでクリックすると「Training data」と「Validation data」を選択する部分があります
ここに先程作成したデータのディレクトリをそれぞれ選択します
createml4.png

あとは「Train」を押せば学習が始まります
学習している画像が次々と表示されると思います

モデルを保存する

学習が完了するとモデルを保存することができます
与えたテストデータでは 97% の精度が出ていることが確認できます
コンソールには更に詳しい学習状況が表示されています

createml5.png

ImageClassifier.mlmodel という名前のモデルが保存されていると思います
作成したモデルは CoreML を使うことで iOS アプリに組み込むことができます
https://developer.apple.com/documentation/vision/classifying_images_with_vision_and_core_ml

最後に

とりあえず CreateML で画像認識してみました
モデルだけであればかなり簡単に作成できました
あとはモデルを使う方法を学べばすぐにアプリにできると思います

他にもテキストデータを学習したり分類などもできるようです

参考サイト

2018年10月16日火曜日

ブックマークの一覧を取得する方法

概要

Firefox の extension に bookmark の API があります
これを使えばブックマークのタイトルや URL を取得することができます
今回はブックマークツールバーにあるブックマークの一覧を取得してみました

環境

  • macOS 10.13.6
  • Firefox 62.0

manifest.json

  • vim manifest.json
{
  "manifest_version": 2,
  "name": "Test bookmarks",
  "version": "1.0",
  "description": "Fetch bookmarks info",
  "background": {
    "scripts": ["main.js"]
  },
  "permissions": [
    "bookmarks"
  ]
}

permissions.bookmarks が必要です

main.js

  • vim main.js
browser.bookmarks.getTree(function(results) {
  results[0].children.forEach(function(toolbar) {
    if (toolbar.id == "toolbar_____") {
      toolbar.children.forEach(function(bookmark) {
        console.log(bookmark.id);
        console.log(bookmark.url);
      })
    }
  });
});

browser.bookmarks.getTree でブックマーク全体の情報を取得します
次に results[0].children で直下にあるブックマークのディレクトリ情報を取得します
このディレクトリの中にある "toolbar_____" という ID を持つディレクトリがブックマークツールバーになります

この配下の toolbar.children でブックマークの一覧の配列を取得しあとは forEach で回すだけです

取得できる情報は BookmarkTreeNode のオブジェクトになります
今回は id と url の情報を表示しています

動作確認

about:debugging からデバッグ画面を表示して確認すると以下のように表示されると思います
bookmark1.png

最後に

Firefox の extension でブックマークの操作をしてみました
今回は取得しかしませんでしたがブックマークの登録や削除などもできます
提供されている API は以下の参考サイトのリンクに記載されています

参考サイト

2018年10月15日月曜日

Firefox の WebExtension で簡単なローカルストレージを使う方法

概要

Firefox の Webextension で設定内容などを保存したい場合にはブラウザのローカルストレージが使えます
特にクラウドでデータを連携する必要がない場合などは簡単かつ軽量に使えるので便利です
今回は簡単な使い方を紹介します

環境

  • macOS 10.13.6
  • Firefox 62.0

manifest.json

  • vim manifest.json
{
  "manifest_version": 2,
  "name": "Local storage demo",
  "version": "1.0",
  "description": "Store and save your data in local storage",
  "icons": {
    "48": "icons/border-48.png"
  },
  "background": {
    "scripts": ["main.js"]
  },
  "permissions": [
    "storage"
  ]
}

permissions.storage が必要になります

main.js

  • vim main.js
browser.storage.local.get("config", function(value) {
  if (value.config === undefined) {
    browser.storage.local.set({
      config: {
        count: 1
      }
    });
  } else {
    var c = value.config.count;
    c++;
    console.log(c);
    browser.storage.local.set({
      config: {
        count: c
      }
    });
  }
});

あまり良いサンプルじゃないかもしれません、、
ローカルストレージからのデータの取得は browser.storage.local.get で行います
ローカルストレージへのデータの保存は browser.storage.local.set で行います

データは JSON 形式で保存します
そして取得するときも JSON になるのでキーをドットでつないで参照します
初回はデータがなく undefined になるので、その場合は初期化する処理を書いています

今回のサンプルの場合 extension を読み込むたびにカウントアップしていくサンプルになります
about:debugging の画面でデバッグ画面を開いて何度も最読み込みするとカウントアップするのがわかると思います
普通はオプション画面などで使うと思います
ちなみにオプション画面は manifest.json に options_ui を定義することがで実現できます

"options_ui": {
  "page": "options.html"
}

この HTML 内で更に js ファイルを参照してそこで browser.storage.local.set を呼ぶ感じです
こうすることでページやタブを跨いでもデータを横断して参照することができるようになります

最後に

Firefox の Webextension でローカルストレージを使ってみました
データは extension が削除されたりユーザがブラウザのキャッシュ情報などを意図的に削除するとなくなります

用途してはテンポラリー的な感じかなと思うので重要なデータなどはクラウドストレージなどと連携すると良いと思います
storage.sync などを使えば Firefox Account 同士でデータを共有することができます
ただその場合は認証なども必要になるので少し大変な実装になるかとは思います

2018年10月14日日曜日

Patreon の OAuth 認証を使ってみた

概要

Patreon には OAuth の仕組みがありこれを使えば Patreon のサーバ情報に API を使ってアクセスすることができます
今回は OAuth を実現するためのログイン画面の使い方から OAuth 後の API の呼び出しまで基本的な流れを試してみました

環境

  • macOS 10.14
  • Ruby 2.5.1p57
  • patreon-ruby 0.5.0

クライアントアプリ作成

まずは OAuth 用のクライアントアプリを作成しましょう
このページから作成できます
コールバック用の URL は localhost で動作させるアプリを指定します

patreon_oauth3.png

クライアントを作成すると「Client ID」と「Client Secret」が取得できるのでメモしておきましょう

ライブラリインストール

  • bundle init
  • vim Gemfile
gem "sinatra"
gem "patreon"
  • bundle install --path vendor

アプリ作成

今回のアプリの流れとしては

  • Patreon でログインページへ遷移
  • ログインできた場合はコールバック用のページでトークンを取得
  • 取得したトークンを使って Patreon の情報を取得

になります

ログイン画面の作成

まずは Patreon のログイン画面に遷移させるページを作成します

  • vim app.rb
require 'sinatra/base'

class TestOAuth < Sinatra::Base
  get '/login' do
    erb :login
  end
end

/login にアクセスした場合にログイン画面に遷移するリンクを表示します

  • mkdir views
  • vim views/login.erb
<html>
<head>
</head>
<body>
  <a href="https://www.patreon.com/oauth2/authorize?response_type=code&client_id=0xmmvlIcKC0PAhdZHdQ31myNO1qPD4MDQBqHOLoZQ19n5DCmfupfyZdlhwv8ikMe&redirect_uri=http://localhost:9292/callback">login</a>
</body>
</html>

Patreon のログイン画面へのリンクにはフォーマットが決められており https://www.patreon.com/oauth2/authorize に対して response_typeclient_idredirect_url をパラメータに付与してリクエストします
ここで client_id は先程クライアント作成時にメモしておいた「Client ID」を記載してください
また redirect_url もクライアントアプリを作成するときに指定した URL を指定してください
間違っている場合ログイン画面が表示されません

コールバック用のページの作成

ログインに成功した場合に呼び出されるコールバック用のページを作成します
app.rb にコールバック用のリクエストを受け付けるルーティング /callback を追加します

  • vim app.rb
require 'sinatra/base'
require 'patreon'

class TestOAuth < Sinatra::Base
  get '/login' do
    erb :login
  end

  get '/callback' do
    client_id = '0xmmvlIcKC0PAhdZHdQ31myNO1qPD4MDQBqHOLoZQ19n5DCmfupfyZdlhwv8ikMe'
    client_secret = 'Xex5ENhpLeZwv7UdZWBF2HS6bLqwE6cUYQJVkQWevcBMBc2bINhKKeh-l069Uypq'
    redirect_url = 'http://localhost:9292/callback'

    oauth_client = Patreon::OAuth.new(client_id, client_secret)
    tokens = oauth_client.get_tokens(params['code'], redirect_url)

    api_client = Patreon::API.new(tokens['access_token'])
    user = api_client.fetch_user()
    @user_data = user.data
    erb :callback
  end
end

client_id, client_secret は作成したクライアントのものを指定してください
その 2 つからトークンを取得するための oauth_client を作成します
コールバックされたページには code というパラメータが付与されて呼び出されます
その code と redirect_url そして oauth_client を使って get_tokens メソッドを呼び出すことでトークン情報を取得することができます

トークンにはいくつか種類がありますが API をコールするために必要なのは access_token になります
ハッシュとして受け取れるので access_token にアクセスしましょう

あとはトークンを元に Patreon::API で API をコールするためのクライアントを作成し fetch_user などのメソッドをコールすれば OK です

今回は取得したデータをテンプレートに渡してそちらでアイコンと名前を表示します

  • vim views/callback.erb
<html>
<head>
</head>
<body>
  <h1><%= @user_data.full_name %></h1>
  <img src="<%= @user_data.thumb_url %>">
</body>
</html>

動作確認

  • bundle exec rackup config.ru

でアプリを起動しましょう
あとは localhost:9292/login にアクセスするとログインへのリンクが表示されるのでそれを踏み Patreon のログイン画面でログインすればユーザ情報が表示されるはずです

patreon_oauth_demo.gif

最後に

Patreon の OAuth 機能を使ってログインから情報を取得するまでの基本的な流れを紹介しました
OAuth 自体に特に難しかった点はなかったのですが、Ruby のクライアントライブラリの使い方などは知っておく必要がありそうです
公式に Sinatra の OAuth のサンプルもあったのでそれも参考にすると良いかもしれません

今回使用したクライアントアプリはすでに削除しているので client_idclient_secret は使えませんのでご注意ください

参考サイト

2018年10月13日土曜日

ReactJS 入門

概要

ブラウザだけで ReactJS に入門してみました
簡単な英語のチュートリアルがあったのでそれの動かし方を説明します
とりあえず動かしてみたい人向けのチュートリアルになります

環境

  • macOS 10.14
  • ReactJS 16

サンプルコード

雛形の index.html の作成

  • vim index.html
<html>
<head>
  <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
  /* 
  ここにコードを記載します
  */
  </script>
</body>
</html>

head の部分に ReactJS を使うためのスクリプトファイルを追加します
babel-standalone も使うのでそれも追加します
どちらも CDN で配信してくれているのでそれを利用します
body 内に ReactJS のコードを記載することで DOM をレンダリングしていきます

Hello world を出力する DOM を追加する

先程のコードの「ここにコードを記載します」に追加します

  • vim index.html
<html>
<head>
  <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
  class Hello extends React.Component {
    render() {
      return <h1>Hello world!</h1>;
    }
  }
  ReactDOM.render(
    <Hello />, 
    document.getElementById("root")
  );
  </script>
</body>
</html>

React.Component を継承したクラスで render メソッドを実装します
そして ReactDOM.render で作成したクラスと描画する DOM の情報を指定します

動作確認

あとは index.html をブラウザで開けば「Hello world!」と表示されます
こんな感じで Component を作成して render する感じになります

props と state

ReactJS では props と state というデータが使えます

props

props は ReactDOM.render でクラスを指定した際に属性名と値を指定すると、その値をクラス側で参照することができます
具体的には以下のように変更します

class Hello extends React.Component {
  render() {
    return <h1>Hello {this.props.name}!</h1>;
  }
}
ReactDOM.render(
  <Hello name="hoge" />, 
  document.getElementById("root")
);

JSON を渡すこともできます

const d = {"first":"hoge","family":"fuga"}
class Hello extends React.Component {
  render() {
    return <h1>Hello {this.props.name.family}!</h1>;
  }
}
ReactDOM.render(
  <Hello name={d} />, 
  document.getElementById("root")
);

state

state は値が変更されると再度 render を走らせることができるデータです
ボタンをクリックすると state を変更し表示内容を変更してみます
以下のように ReactJS の部分を書き換えます

class Hello extends React.Component {
  render() {
    return(
      <div>
        <h1>Hello {this.state.name}!</h1>
        <button onClick={this.updateName}>Click me!</button>
      </div>
    )
  }
  constructor() {
    super();
    this.state = {
      name: ""
    };
    this.updateName = this.updateName.bind(this);
  }
  updateName() {
    this.setState({
      name: "hawk"
    });
  }
}
ReactDOM.render(
  <Hello />, 
  document.getElementById("root")
);

ReactDOM.render は元のやつに書き換えます
まず render() メソッドで初期状態の DOM を return するようにします
ボタンを新たに設置しクリック時に onClick={this.updateName} をコールさせます
Hello 側は state に応じた値を表示させるため {this.state.name} を設定します

updateName メソッド内で setState をコールします
state を変更する場合は直接プロパティを参照することはせず setState メソッドを呼ぶようにしましょう
これでクリック時の挙動は定義できました

あと新たに追加したのは constructor() になります
これは Hello クラスが呼び出された場合に一番始めにコールされる初期化用のメソッドです
ここで this.state に対して初期化します
今回の場合、空文字で初期化しているので画面を呼び出した際には名前は何も表示されません

そして最後に this.updateName = this.updateName.bind(this); しています
これは updateName メソッドに this を bind しているのですがこれをすることでどうなるかというと updateName 内で this が参照できるようになります

これで動作確認するとボタンが表示されボタンをクリックすると名前が表示されるのがわかると思います

最後に

ReactJS を使って簡単な DOM のレンダリングと基本となるデータの使い方を学びました
かなり簡単なチュートリアルだったので更にやってみたい方は公式のチュートリアルがオススメです
ゲームを開発するチュートリアルっぽいです
他には今回参考にさせて頂いたチュートリアルの続きでチャットアプリを開発するチュートリアルもあるのでこれも良いかもしれません

フロントエンドのフレームワークはやたらありそれぞれが独自の思想を持っているケースが多いのですべてを覚えてることは不可能ですが、ReactJS や Angular, jQuery あたりのメジャーどころは覚えておいて損はないかと思います

参考サイト

2018年10月12日金曜日

macOS で react-native 入門

概要

macOS で react-native を使って iOS アプリを開発してみました
なお今回は expo を使わずに react-native-cli を直接使います

環境

  • macOS 10.14
  • Xcode 10.0 (10A255)
  • node 10.1.0
  • watchman 4.9.0
  • react-native-cli 2.0.1
  • react-native 0.57.2 -> 0.57.1

事前準備

まずは環境を構築します

  • brew install node
  • brew install watchman

node は 8.3 以上が必要です

react-native-cli のインストール

react-native-cli プロジェクトを作成したり

  • npm install -g react-native-cli

Xcode コマンドラインツールのインストール

Xcode を開き Preferences -> Locations からインストールできます

新規アプリケーションの作成

とりあえずサンプル用のアプリケーションを作成します

  • react-native init AwesomeProject

いろいろとダウンロードが始まります
プロジェクトの作成が完了し各プラットフォームでのビルド方法が表示されれば OK です

To run your app on iOS:
   cd /Users/hawksnowlog/work/AwesomeProject
   react-native run-ios
   - or -
   Open ios/AwesomeProject.xcodeproj in Xcode
   Hit the Run button
To run your app on Android:
   cd /Users/hawksnowlog/work/AwesomeProject
   Have an Android emulator running (quickest way to get started), or a device connected
   react-native run-android
(node:67987) ExperimentalWarning: The fs.promises API is experimental

ios/AwesomeProject.xcodeproj を Xcode で開き設定変更

このままビルドしても Print: Entry, ":CFBundleIdentifier", Does Not Exist のエラーでビルドできません
一度プロジェクトの設定を変更します

  • File -> Project Settings
  • Advanced

で Build Location を以下のように変更します
react-native2.png

Products と Intermediates の先頭に build/ を入力しました
またプルダウンは「Relative to Workspace」を選択します

とりあえず動かしてみる

  • cd AwesomeProject
  • react-native run-ios

でシミュレータが起動しテストできます
8081 で LISTEN するビルドサーバが立ち上がるのですでにポートを使っている場合はプロセスを停止しましょう
ビルドしてパッケージを転送するのでアプリが起動する前に結構時間がかかります
またやたら warning が出ている感じがしますが気にせず進めます
環境によるかもしれませんがシミュレータは iPhone6 が立ち上がりました
react-native3.png

ターミナルでは以下のようになっていれば成功です
これが 100% にならない場合はどこかでビルドが失敗しています
react-native4.png

error: bundling failed: Error: Unable to resolve module ./../react-transform-hmr/lib/index.js

どうやら react-native 0.57.2 で出るようです (参考)
0.57.1 にダウングレードしてビルドし直しましょう

  • vim package.json
"react-native": "0.57.1"
  • npm add @babel/runtime
  • npm install
  • rm -rf $TMPDIR/react-*; rm -rf $TMPDIR/haste-*; rm -rf $TMPDIR/metro-*; watchman watch-del-all

をしてから再度 react-native run-ios してみてください

ちょろっとコードを改修する

App.js を編集しましょう
とりあえずウェルカムメッセージの部分を変更しました

<Text style={styles.welcome}>Welcome to React Native!!!</Text>

保存して Command + r をシミュレータ上で実行するとアプリの変更が反映されていると思います

最後に

react-native を使って iOS アプリをビルドしてみました
Android 用のビルド環境を作れば Android でもビルドできると思います

今回は最新バージョン (0.57.2) でバグのような挙動があったので苦労しましたが本来であればエラーは出ずにビルドできると思います
一度ビルドしてアプリを起動すれば変更などはすぐに確認できるのは良いかなと思います

ただ最近は Airbnb が react-native での開発をやめたりとあまり良いニュースがないイメージです
もともと Swift なり Kotlin で native 開発できるのであれば多少手間でも将来性を考えると native を使ったほうが良いかなと思います

参考サイト

2018年10月11日木曜日

Firefox の Webextension で Notifications を出す方法

概要

Firefox の Webextension では新たに Notification が使えるようになりました
今回は簡単な使い方を紹介します

環境

  • macOS 10.13.6
  • Firefox 62.0

manifest.json

  • vim manifest.json
{
  "manifest_version": 2,
  "name": "Notifications sample",
  "version": "1.0",
  "description": "Display a notification when starting",
  "icons": {
    "48": "icons/border-48.png"
  },
  "background": {
    "scripts": ["main.js"]
  },
  "permissions": [
    "notifications"
  ]
}

ポイントは permissionsnotifications を追加するところです

main.js

  • vim main.js
browser.notifications.create({
  "type": "basic",
  "iconUrl": browser.extension.getURL("icons/icon-48.png"),
  "title": "Hello",
  "message": "This notification is test"
});

type は basic を使っています
他にも image や list, progress などがあります
詳細はこちらをご覧ください
iconUrl は通知時にアイコンを指定することができます
指定のアイコンがない場合はデフォルトで Firefox のアイコンが表示されます
title, message は通知に表示する文字列情報になります

今回の場合はアドオンを読み込みした時点で通知します
本来であればクリックのイベントやコールバック処理などで呼び出しましょう

動作確認

about:debugging から manifest.json を読み込みましょう
すると以下のように通知が来ると思います
notification1.png

最後に

Firefox の Webextension で Notifications 機能を使ってみました
background で動作させる場合には window.alert などが使えないので代用として Notifications は良いかもしれません
また HTML が不要なのも嬉しい点かなと思います

通知時のイベントもあり onButtonClicked, onClicked, onClosed, onShown があります (参考)

通知はウザがられがちですが使い方によっては便利なのでうまく活用してみてください

2018年10月10日水曜日

Ruby で ReactiveX に入門してみた

概要

そもそも ReactiveX とは何かという話ですが簡単に言えば Ruby においては Enumerable の拡張という感じです
簡単に言い過ぎているので詳細は公式などを見てほしいのですが、例えば普通の Ruby の配列 ArrayRx::Observable な配列として扱うことで使いやすくすることができます
今回は RxRuby サンプルを動かしつつ ReactiveX を理解してみました

環境

  • macOS 10.14
  • Ruby 2.5.1p57
  • rx 0.0.3

ライブラリインストール

RxRuby という gem が公開されているのでこれを使います

  • bundle init
  • vim Gemfile
gem "rx"
  • bundle install --path vendor

とりあえず動かしてみる

サンプルにあるものをとりあえず動かしてみましょう
Rx::Observable な配列を作成して zip を使って各要素を結合します

  • vim rx1.rb
require 'rx'

a1 = Rx::Observable.from_array [1, 2, 3]
a2 = Rx::Observable.from_array [4, 5, 6]

sub = a1.zip(a2).subscribe(
  lambda { |x|
    p x
  },
  lambda { |e|
    p e
  },
  lambda {
    p "end"
  }
)

これで bundle exec ruby rx1.rb として実行すると

[1, 4]
[2, 5]
[3, 6]
"end"

という感じで表示されると思います

普通に zip してみる

もし Rx::Observable な配列を使わない場合は以下のようなるかと思います

  • vim nrx1.rb
a1 = [1, 2, 3]
a2 = [4, 5, 6]

r = a1.zip(a2)
r.each { |rt| p rt }

これでも意図した結果を得ることができます

何が違うか

この 2 つのサンプルで大きく違うのは各要素を結合した際の経過を監視 (observe) することがでている部分です
Rx::Observable を使った場合 zip メソッドを subscribe し各要素が結合された際にコールされる lambda を定義することができます
またこの 3 つの lambda は Ruby だけではなく ReactiveX の Observable として仕様が決まっており onNext, onError, onCompleted の 3 つ流れを受け取ることができます
それぞれ結合が「成功した場合」「失敗した場合」「終了した場合」にコールされます

このように結合の処理ごとにその状況を監視することが Rx::Observable を使う大きなメリットかなと思います

ちなみにここでとりあげた zip オペレーションは ReactiveX のページでこういう風に実装しろというのがしっかりと定義されています
各言語の ReactiveX 用のライブラリは基本的にこれらを参考に書くオペレーションが実装されています

また Rx::Observable の処理は非同期処理になります
なので subscribe の処理の完了を待たずに次の処理に進むので注意が必要です

その他のサンプルを動かしてみる

ReactiveX には zip 以外にも様々なオペレータが用意されています
すべて紹介するのは厳しいのでよく使われそうなものを紹介します

timer

  • vim rx_timer.rb
require 'rx'

t = Rx::Observable.timer(3, 1)
s = t.time_interval().pluck('interval').take(10)

sub = s.subscribe(
  lambda { |x|
    p x
  },
  lambda { |e|
    p e
  },
  lambda {
    p "end"
  }
)

while Thread.list.size > 1
  (Thread.list - [Thread.current]).each &:join
end

3 秒待ってから 1 秒ごとに 10 回カウントアップするタイマー処理になります
Rx::Observable.timer(3, 1) の部分がスタートしてから 3 秒待つ処理とインターバルの秒数を 1 秒ごとに設定している部分です
ここを変更すればスタートまでのウェイトとインターバルの秒数を変更できます

take(10) で 10 回カウントアップします
カウントアップはインターバルで指定した秒数待ってからカウントされます

同じようなオペレーションで interval というものもあります

flatMap

flatMap は配列内の各要素順番に処理し最終的に 1 つの配列としてまとめる処理です

  • vim rx_flat_map.rb
require 'rx'

times = [
    { value: 0, time: 0.1 },
    { value: 1, time: 0.6 },
    { value: 2, time: 0.4 },
    { value: 3, time: 0.7 },
    { value: 4, time: 0.2 }
]

s = Rx::Observable.from(times).flat_map { |item|
  Rx::Observable.of(item[:value]).delay(item[:time])
}

sub = s.subscribe(
  lambda {|x|
    p x
  },
  lambda {|e|
    p e
  },
  lambda {|r
    p "end"
  })

while Thread.list.size > 1
  (Thread.list - [Thread.current]).each &:join
end

上記のサンプルはいわゆる遅延インサート的なことを実現しています
Rx::Observable.of(item[:value]) は値をそのまま onNext に流す処理なのですが .delay(item[:time]) と組み合わせことで指定した時間遅延させてから処理を実行することができます

なので出力される順番が 0 から 4 の順番ではなく time で指定した値が小さい方から出力されることになります

0
4
2
1
3
"end"

こんな感じで複数の Observable と組み合わせて配列の中の値を処理したい場合には flatMap を使うのが便利です

reduce

reduce は各要素の前後を順番に取り出し処理するためのオペレータです
例えば普通の Ruby で配列の各要素を足し合わせる場合には sum を使います

a = [1, 2, 3, 4, 5]
p a.sum

# => 15

これを RxRuby で書き換えると以下のように書けます

  • vim rx_reduce.rb
require 'rx'

ra = Rx::Observable.from_array [1, 2, 3, 4, 5]
s = ra.reduce(0) { |x, y|
  x + y
}

s.subscribe(
  lambda {|x|
    p x
  },
  lambda {|e|
    p e
  },
  lambda {
    p "end"
  }
)

reduce(0) とすることで初期値を 0 に設定します
そして取り出す 2 つの要素を x, y で受け取り加算します
加算した値が次の x に代入されます
なので今回の加算の流れは以下のようになります

  1. x=1, y=2 => 3
  2. x=3, y=3 => 6
  3. x=6, y=4 => 10
  4. x=10, y=5 => 15

最後に

Ruby の RxRuby を使って ReactiveX に入門してみました
実際に比較して動かしてみることで使い方やメリットが理解できるかなと思います
リファレンスが Web 上になさそうなのでメソッドなどの詳細はコードを直接見るしかなさそうです

今回紹介したオペレータはほんの一部です
ReactiveX はこのオペレータたちを如何に使いこなすかがポイントなので使いこなすにはオペレータの使い方と挙動を覚える必要があるかなと思います
また Subject と呼ばれる Observer と Observable の 2 つの性能を持った機能も存在します

基本的に ReactiveX は「使うべきケース」「使えるケース」が決まっているかなと思っています
何でもかんでも ReactiveX で書くのは良くないと思います
既存のコードで配列処理を扱う部分がありその処理の内容を逐次監視したい場合などに使ったり、タイマー処理を時系列で眺めたい場合などには適しているかなと思います
また基本は非同期なので要素それぞれに対して処理したい場合には適していますが、結合結果をメイン側で受け取って何かしたい場合には適していないと思います

その辺りのユースケースも先駆者の経験からいろいろと Web を調べると出てくるので参考にしてみるといいかもしれません
ただ RxRuby のユースケースは少ない感じがするので、その場合は他の言語の適用ケースを見ると良いとかと思います

参考サイト