pblog

pplog.net を作っている @ppworks こと越川直人(Koshikawa Naoto)のブログ。esa LLCで働いてます(\\( ⁰⊖⁰)/)

pplog iOS で push 通知をAmazon SNSに変えた

先日、pplog4歳になったタイミングのちょいまえにバージョンアップしたpplog iOSだけども、いくつか新しいことをナンカしている。それを記録がてら記事にしていこうというこのシリーズ。ネタとしては3回分ぐらいはある。今回は push 通知周りのお話。

新生 pplog iOS で push 通知を Amazon SNS

aws.amazon.com

で送るように変えた。

Swift側

特筆することがないので省略。紹介すると言いつつ、いきなり紹介しない。

Rails側

Rails側の機能を洗い出すと以下のようになる

  • iOSからのデバイストークンの受取
    • Amazon SNSにEndPointを生成
  • iOSからのデバイストークンの削除
    • Amazon SNSからEndPointを削除
  • ユーザーのアクションを Trigger にした push通知配信
    • Amazon SNSのEndPointに配信指示

次に、これらの機能をブレイクダウンする。

iOSからのデバイストークンの受取

  • ユーザーが push 通知に同意した際の処理
  • AppDeviceToken#create するリソースを定義(routingの設定とcontrollerの作成をしておく)
    • 普通のCRUDの一つなので今回は省略

iOSからのデバイストークンの削除

  • ユーザーがログアウトとした際に一旦デバイストークンを消す
  • AppDeviceToken#destroy するリソースを定義(routingの設定とcontrollerの作成をしておく)
    • 普通のCRUDの一つなので今回は省略

ユーザーのアクションを Trigger にした push通知配信

  • あるユーザーがポエムを書くと購読しているユーザーに通知を配信
  • あるユーザーがポエムを「読んだよ」して「足あと」を付けると、ポエムを書いたユーザーへ通知を配信

紹介するコード

  • ActiveRecordモデル
  • Amazon SNS クライアント
  • 通知周りのActiveJob

あたりを紹介しておく。

token保存用のテーブル定義

ユーザーのテーブルからhas_many :apple_device_tokensで関連付けるイメージ。

class CreateAppleDeviceTokens < ActiveRecord::Migration[5.1]
  def change
    create_table :apple_device_tokens do |t|
      t.references :user, null: false, foreign_key: true
      t.string :token, null: false, index: { unique: true }
      t.string :endpoint_arn, null: false, default: ''
      t.integer :via, null: false, default: 0
      t.timestamps
    end
  end
end
  • token: APNから取得するデバイストークン
  • endpoint_arn: Amazon SNS の Endpoint の ARN
  • via: iOS アプリの push 通知証明書が sandbox 用か、プロダクション用かを示す

token保存用モデル

Aws::SNS::Errors::EndpointDisabledが発生するのは、APN側で無効となったデバイストークンだったり、サンドボックスのものをプロダクションに送ったりした場合など。 その場合は、Amazon SNS上で勝手にdisabledになっているので、RDB側からも消しておくわけ。

ActiveRecordのモデルにしては責務が多い気もするのでクラスを分けても良いかもしれない。

class AppleDeviceToken < ApplicationRecord
  enum via: {
    sandbox: 0,
    prod: 1
  }

  belongs_to :user

  validates :token, presence: true, uniqueness: true

  after_commit :fetch_endpoint_arn_async, on: :create

  def fetch_endpoint_arn!
    created_response = client.create_platform_endpoint(
      token: token,
      custom_user_data: { user_id: self.user.id }.to_json
    )

    if created_response.successful?
      update!(endpoint_arn: created_response.endpoint_arn)
    end
  end

  def send!(alert:, path:)
    return if self.endpoint_arn.blank?
    return if alert.nil? || path.nil?

    message = {
      default: alert,
      json_root => {
        aps: {
          alert: alert
        },
        path: path
      }.to_json
    }

    # http://docs.aws.amazon.com/sdkforruby/api/Aws/SNS/Client.html#publish-instance_method
    client.publish(
      target_arn: self.endpoint_arn,
      message: message.to_json,
      message_structure: 'json'
    )
  rescue Aws::SNS::Errors::EndpointDisabled
    self.destroy!
  end

  private

  attr_reader :client

  def client
    @client ||= self.sandbox? ? AwsSnsClient.sandbox : AwsSnsClient.prod
  end

  def json_root
    sandbox? ? 'APNS_SANDBOX' : 'APNS'
  end

  def fetch_endpoint_arn_async
    SnsEndpointJob.perform_later(self)
  end
end

AwsSnsClient

Amazon SNS の処理。今回使う API に特化して雑なクライアント設計となった。まあ最小限でいいかという判断。

ポイントは、already exists with the same Token, but different attributes.が返ってきたら、雑に一旦消して投げ直す処理かな。

class AwsSnsClient
  APP_ARN = ENV.fetch('SNS_APP_ARN_PROD_ENDPOINT')
  APP_ARN_SANDBOX = ENV.fetch('APP_ARN_SANDBOX_ENDPOINT')

  def initialize(app_arn:)
    @aws = Aws::SNS::Client.new(
      access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
      secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY'),
      region: ENV.fetch('AWS_SNS_REGION'))
    @app_arn = app_arn
  end

  def create_platform_endpoint(token:, custom_user_data: nil)
    aws.create_platform_endpoint(
      platform_application_arn: app_arn,
      token: token,
      custom_user_data: custom_user_data
    )
  rescue Aws::SNS::Errors::InvalidParameter => e
    if e.message =~ /already exists with the same Token, but different attributes./
      if end_point_arn = e.message.scan(/arn:aws:sns[\S]+/).first
        delete_endpoint(endpoint_arn: end_point_arn)
        retry
      end
    end
  end

  def delete_endpoint(endpoint_arn:)
    aws.delete_endpoint(
      endpoint_arn: endpoint_arn
    )
  end

  def publish(options = {})
    aws.publish(options)
  end

  class << self
    def prod
      self.new(app_arn: APP_ARN)
    end

    def sandbox
      self.new(app_arn: APP_ARN_SANDBOX)
    end
  end

  private

  attr_reader :aws, :app_arn
end

Active Job

  • EndPointを登録するJob
  • EndPointに配信するJob

の2つを定義した。

SnsEndpointJob

EndPointを登録するジョブ。

class SnsEndpointJob < ApplicationJob
  queue_as :aws_sns

  def perform(apple_device_token)
    apple_device_token.fetch_endpoint_arn!
  end
end

SnsSendingJob

EndPointに配信するジョブ。

通知のメッセージをモデルに定義したくなかったので、app/views/application配下にメッセージのテンプレートを置いて、ApplicationController.render()でレンダリングして配信メッセージとして送るようにした。

class SnsSendingJob < ApplicationJob
  queue_as :aws_sns

  def perform(apple_device_token, trigger)
    payload_json = ApplicationController.render(
      "apple_push_notification_for_#{trigger.class.name.downcase}",
      assigns: {
        trigger: trigger
      }
    )
    payload = JSON.parse(payload_json)

    apple_device_token.send!(alert: payload['alert'], path: payload['path'])
  end
end

以下のように呼ぶ

user.apple_device_tokens.each do |apple_device_token|
  SnsSendingJob.perform_later(apple_device_token, trigger)
end

感想

Amazon SNSに雑に投げれば良いのはホント楽だったけど、GAEでもはもっと楽かもしれない。知らない。もっとあれなら検討しても良いかも。

pplogの通知がたまって、いっきにバーーーン!ってした話。

Sidekiq.configure_server do |config|
  config.options[:concurrency] = ENV.fetch('SIDEKIQ_CONCURRENCY', 5)
  config.redis = { url: ENV['REDIS_URL'] }
end

Sidekiqのconcurrencyを環境変数から、スッと変えられるようにこんな設定ファイルを書いてました。

そのせいで、環境変数SIDEKIQ_CONCURRENCYへ値をセットすると、sidekiqが立ち上がらないという状況が起きていて、通知がredisには溜まるけど、配信されないというつらい状況に。

原因はこの設定を参照する以下の行でのエラー

https://github.com/mperham/sidekiq/blob/3b00587ea0b3f887df9053ee2f6b42a69481b5ce/lib/sidekiq/redis_connection.rb#L15

size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)

ここで、no implicit conversion of Integer into Stringが発生します。

環境変数といえば?

irb
irb(main):001:0> ENV.fetch('SIDEKIQ_CONCURRENCY', 5)
=> 5
SIDEKIQ_CONCURRENCY=1 irb
irb(main):001:0> ENV.fetch('SIDEKIQ_CONCURRENCY', 5)
=> "1"

あ!環境変数から読み込んだら文字列になるじゃん、そりゃそう。

てなわけで、

Sidekiq.configure_server do |config|
 -  config.options[:concurrency] = ENV.fetch('SIDEKIQ_CONCURRENCY', 5)
 +  config.options[:concurrency] = Integer(ENV.fetch('SIDEKIQ_CONCURRENCY', '5'))
end

としてあげる必要がありました。さらに、push通知は驚くだろうし、redisから消してから配信という手もあったかもしれません。

ご迷惑をおかけしまして申し訳ございません₍₍ ε=з ⁾⁾

自分との差を見る先を建設的に変える

f:id:naoto5959:20170930084059p:plain

他人との自分との基準の違いにイライラしてばかりして、イライラを溜め込むことでは何も変わらない。

目指す人たちを見つけて、その理想と自分との差分を埋める努力をする方が建設的だ。

って話をした。

他人は変えられない。自分だけは変えることが出来る。

自分を変える際のヒント

  • 口癖を変える
  • 習慣を変える
  • 付き合う相手を変える
  • 飲み会などで話す話題を変える

無意識に染み付いた癖や習慣、周りからの影響、普段の話題、ちょっとずつ見直してみよう。

pplog の Lab 機能にメールアドレスでのログインを追加しました

まずはメールアドレスの設定が必要となります。

メールアドレスの設定

右上のメニューから「設定」→「メールアドレス設定

にて、メールアドレスが登録されているかをご確認下さい。

f:id:naoto5959:20170928142706p:plain

未設定の場合は以下のような表示になります。

f:id:naoto5959:20170928142439p:plain

Lab機能ページで設定

さて、本題。

右上のメニューから「設定」→「Lab機能

にて、本機能のON/OFFを設定できます。初めてLab機能ページへアクセスされた方は他の項目にも目が行ってしまうかもしれませんが気にしないでください。

f:id:naoto5959:20170928142817p:plain

こちらで、機能をONにすると

https://pplog.net/?via=email

からメールアドレスでログインできるようになります。

ログイン画面

ログイン中の場合は一度ログアウトしてからお試しください。

f:id:naoto5959:20170928143309p:plain

ここからメールアドレスを入力するとURLが送られてるくるのでそちらからログインしてみて下さい。 iOSアプリでログインしたい場合はiOSアプリ用の方をクリックして下さい。

新規アカウント作成

新規アカウント作成時に、Twitter連携を不要とするかどうかは検討中です。

pplogが4歳になった

ポエムを書くサービスであるpplog.netが4歳になった。

  • 2013.9.26 18:36 に作りたいと思った
  • 2013.9.26 19:05 に pplog.net ドメインを取得
  • 2013.9.26 19:29 に rails4baseからリネームして作り始めた
  • 2013.9.26 19:41 に Heroku へ push
  • 2013.9.26 21:28 に初のポエムを書いた
  • 2013.9.26 23:09 に一緒に作ったデザイナーさんがポエムを書いた
  • 2013.9.27 02:25 に友達がポエムを書く

Herokuへpushしてから初ポエムまでラグがあるのは、おそらく最初はポエムを書く機能がなかったと思われる。徹底的に削ぎ落とした機能開発だった。もはや何もなかったのだ。いいのかそれで。まあ1時間半後にポエム書けているし、いいや。

友達へ初めて教えたのが、27日だったので、誕生日を一日勘違いしていた。つまり今日だと思いこの記事を書き始めた。書き始めて気づいた。ごめんな( ˘ω˘)"

ポエム駆動開発によるWEBサービスの作り方 pplog誕生ものがたり - pblog

4周年記念

appsto.re

誕生日に先駆けてiOSのアプリを3年ぶりにアップデートして2.0.0をリリースした。2017年8月29日のことだ。それから3度アップデートし、現在は2.1.0となる。主な変化は以下の通り。

  • ₍₍ ε=з ⁾⁾が本体の傾きに応じて傾くようになった
  • iOS11対応とその他どうでもいいこと色々

1.0.xは、えびちゃんが作ってくれたRubyMotionによるアプリであった(その節はありがとう!)が、今回はフルスクラッチでSwiftで書いた。

  • Swiftで書きたい
  • 認証フローにSFSafariViewController使いたい
  • 4周年が近い
  • iOS11で32bitアプリが、、、!
  • ふざけた改修を思いついた時にすぐ実装したい(ので自分で作る)

というわけでまずはSwiftの勉強からであった。

詳解Swift 第3版

詳解Swift 第3版

この本は大変お世話になった。眠れない夜にぐっすりと眠れた。眠れなければこの本をめくればよい。数秒で眠ることが出来た。

それはおいておいて、本当に良書で、Swiftの細かい部分にまで言及してくれているので、Swiftの知識ゼロな状態でアプリを書ききってリリース出来たのは、この本のおかげ。感謝しか無い。ありがとう!

スマートフォンアプリをアップデートすると、「お、やっとるな」感が出るのか、ちょっとポエム書くひとが多少増えたような気がする。自分が前よりもアプリを見るようになっただけで、気のせいかもしれない。「何も変わってないな」みたいな感想がちらほらあって良かった。自分でも何やってたんだ、と思う。

Android版も気が向いたら Kotlin とかで書いてみようかなとか思っている。ただ最近は Unity に夢中なので、後回しか、 Android 版は Unity によるゲームになるかもしれない。ポエムゲーム。

₍₍ ε=з ⁾⁾の改修

考えている改修のうち、特別に一部チラ見せしてしまおう。一部というか、実はこれが全てで、あんまり他のことは未だ考えていない。大丈夫かな。

₍₍ ε=з ⁾⁾をな、もっと、ハンドスピナーみたいな体験をしたい。わかるか。わからんよな。

ちょっと難しいかなと思ったいたけど、最近 Unity 勉強してみたら、出来る気がしてきている。やばい。

なにやってたんだ

サーバーサイトはなにやってたんだ。本当に何もやってなかった。ここ数年は

  • 毎日のbundle update
  • それに伴うコードの更新
  • Railsのバージョンアップ(4.0.0から徐々に育てて今は5.1.4)
  • それに伴うコードの更新
  • 今や誰も使っていないgemを使うのをやめる
  • pjaxをやめてturbolinks5にする
  • 検索にElasticsearch導入(現在lab機能)
  • キャッシュ周りの改善
  • webpayの導入
  • webpayからpayjpへの移行
  • push通知をAWSのSNSへ移行

などしかやってない。なんもしてない。なんもしてないけど毎日bundle updateしてると世の中の流れが何となく見えて面白い。何もしなくても面白いことはあるものだ。

インフラは未だにheroku使ってる。何故か無料枠だと思われる節があるけど、それなりに有料dynoだとかaddon使ってる。

4年経っての想い

作ったときと特に変わっていないので、特別なにかをバーン!と言う感じではない。「4周年なのでこの4年にあったことをポエムにしてね!」とかも思わないし、いつもどおり気が向いたらポエムを書いたり、ポエムを見に来たり、そもそも pplog の存在を忘れたり、好きに過ごして欲しい。100億円で買収したいという話も今のところ未だ来ていない。

ふと「そういや pplog ってあったよな」となった時に、ちゃんとそこにあるようにしておきたい。

appsto.re

てなわけで、5年目のポエムはこれからも相変わらずな感じでやっていく。よろしくね₍₍ ε=з ⁾⁾

思考ロックを外す断捨離術

一昨年の年末あたりに実際にやった断捨離。

  1. 本棚の本をすべて出す
  2. すべて車に積む
  3. ブックオフに持っていく
  4. 売る

ポイントは、愛蔵書・ファンクラブ会員本など二度度手に入らないものであろうと区別せず全てだ。

欲しくなったらまた買う。その際、なるべくKindleなど電子版で買うようにする。 限定本などの場合、結構つらいけど、1冊どうしても欲しくなってしまった本はメルカリでゲットした。 まあ、その手の本は最初から手放さなければよい気もするが、その選別で多くの本が残ってしまう可能性が高い。

実際にやってみると、本当に手元においておきたい本なんて大して多くなかった事に気づいた。 書い直したのは2冊だったな。

一番いいのは置く場所がある家に住むことだが、誰しもが取れる選択肢ではない。 逆に誰しもが取れない選択肢だけど、自分になら出来る選択肢ってはある。

中々思いつかないけど、やってみると意外となんとかなるタイプの断捨離の話でした。

今回の方法自体を無理にオススメはしないけど、思考がロックされがちな部分のロックを外してみると選択肢が広がります。 今回ロックされていたのは、「本をすべて手放すなんて、とんでもない」という思考でした。

『ELASTIC LEADERSHIP』良書だッ

ご恵贈にあずかりましたので、感想を。

エラスティックリーダーシップ ―自己組織化チームの育て方

エラスティックリーダーシップ ―自己組織化チームの育て方

チームリーダーマニフェストを紹介し、リーダー、メンバーともに変化を恐れず常に新しい挑戦をし続けることの大切さを説くところからはじまる。

チームのフェーズを3つに分け、それぞれのフェーズで有効なリーダーシップの手段について紹介されている。

  • サバイバルフェーズ => 指揮統制型
  • 学習フェーズ => コーチ
  • 自己組織化フェーズ => ファシリテーター

このフェーズ分けが直感的に、スッと入ってきた。今まで言語化出来てなかったが感じていたナニカが言語化されていると気持ちが良いものだ。このあたりですっかり本書の虜となってしまった。

このフェーズを意識してリーダーシップを取らないと、サバイバルフェーズのチームに対して、ファシリテーターとして参加してしまい、何も回らないで終わるということが起こりうる。

「サバイバルフェーズでは、指揮統制型で進んでも良いのかー、そりゃそうだよな」と思った。ほんとそっすよね。

また、この3つのフェーズは、様々な状況に応じて行き来する事がありうることを認識するのが大事ぽい。変化を恐れず常に新しい挑戦し続けるのだ。

バス因子に関する話題でも

チームメンバーのうち何人がバスに轢かれたらプロジェクトあるいわチームが破綻するか。 つまり、バス係数が1のときが最も危険

あるある話が登場。情報共有や学習の機会を設けることの大切さを具体的に解説されていて、それな感がすごい。

1〜10章で、ELASTIC LEADERSHIPの解説は終わる。100ページ強なのであっという間に読み終えることが出来ると思う。

続く11章〜46章(33〜46は日本版のみの特典)では、各所のリーダーのエッセイが掲載されていて読み応えがある。

チームリーダー経験のあるなしに関わらずオススメ。チームのみんなで、自分たちが今いるフェーズを自覚して、リーダーはどのような手段でチームを率いていゆくかを宣言しても良いかもしれない。

心に残った言葉

誠実さが姿を見せ、コミットメント言語が機能するには、メンバーの制御下にあるものだけにコミットを求めなくてはならない。

エラスティックリーダーシップ ―自己組織化チームの育て方

エラスティックリーダーシップ ―自己組織化チームの育て方

各所のチームが、よりよくなってみんな幸せになると良いな。 皆で読んで実践しよッ!(\( ⁰⊖⁰)/)