2012年5月2日水曜日

APNs:APNsの実装-Ruby on Rails

今回APNsを実装するサービスは初めてだったので四苦八苦しました。
以下のサイトを参考にして構築しました。
【iPhone】Push Notificationの実装方法

作成するのはモデルです。
notification_provider.rbを作成します。
# coding: ascii-8bit

require "openssl"
require "socket"

class NotificationProvider
  include ActionView::Helpers::JavaScriptHelper

  HOST = case Rails.env
    # when "test"        then "gateway.sandbox.push.apple.com"
    when "development" then "gateway.sandbox.push.apple.com"
    when "production"  then "gateway.push.apple.com"
    end
  PORT = 2195

  CERT_FILE = Rails.root.join("config/鍵名.cert")
  RSA_KEY_FILE = Rails.root.join("config/鍵名.key")

  attr_reader :ssl
以上の様にします。
「CERT_FILE」と「RSA_KEY_FILE」が出てきたのでこちらを作成します。
まずは作成した鍵(pem)を/configの直下に置きます。
そしてその鍵内上の
「-----BEGIN CERTIFICATE-----」から「-----END CERTIFICATE-----」を全て
をコピーして「〜.cert」として作りpem鍵と同じ場所に置きます。
同じくその下の
「-----BEGIN RSA PRIVATE KEY-----」から「-----END RSA PRIVATE KEY-----」
を全てコピーして「〜.key」として作りpem鍵と同じ場所に置きます。
これで準備OK!さぁ作りましょう〜

続いて同じnotification_provider.rbに追記します。
class << self
    def open
      provider = self.new
      provider.open
      provider
    end
  end

  def open
    return if @ssl
    if HOST
      @socket = TCPSocket.new(HOST, PORT)
      context = OpenSSL::SSL::SSLContext.new("SSLv3")
      context.cert = OpenSSL::X509::Certificate.new(File.read(CERT_FILE))
      context.key  = OpenSSL::PKey::RSA.new(File.read(RSA_KEY_FILE))
      @ssl = OpenSSL::SSL::SSLSocket.new(@socket, context)
      @ssl.connect
    else
      @ssl = DummyServer.new
    end
  end

  def close
    @ssl.close if @ssl
    @socket.close if @socket
    @ssl = @socket = nil
  end

  def closed?
    @ssl ? @ssl.closed? : true
  end

  def message(identifier, expired_at, token, alert)
    alert = j(alert.dup)
    alert.force_encoding("ascii-8bit") if RUBY_VERSION.to_f >= 1.9
    payload = %Q!{ "aps":{ "alert":"#{alert}", "sound":"default" } }!
 
    "\x01" +                       # 常に1
    [identifier].pack("N") +       # 識別子、4バイト
    [expired_at.to_i].pack("N") +  # 期限、4バイト(big endian)
    "\x00\x20" +                   # トークン長、2バイト、常に32
    token +                        # デバイストークン
    [payload.size].pack("n") +     # ペイロード長、2バイト
    payload                        # ペイロード
  end

  def notify(device_token, content,
        expired_at = 1.days.from_now, logger = Rails.logger)
    raise "Provider is not opened!" unless @ssl
 
    request = message(0, expired_at, device_token.token, content)
 
    response = nil
    @ssl.write(request)
    if @ssl.kind_of? OpenSSL::SSL::SSLSocket
      if IO.select([@ssl], nil, nil, 0.5)
        response = @ssl.read(6)
      end
    else
      response = @ssl.read(6)
    end
 
    if response.present?
      arr = response.unpack("ccN") # 1バイト目:8、2バイト目:エラーコード
      if arr[0] == 8
        logger.info("error code: #{arr[1]}, divece token id: #{device_token.id}")
      else
        logger.info("unknown error, divece token id: #{device_token.id}")
      end
      return false
    end
    true
  end
これはもう定型文でいいかもしれません。
さぁあとはcronスクリプトを作成する必要がある。
とりあえずcronスクリプトは置いておいてcronスクリプトを動かすための構築をします。
今回はお知らせを表示するので「announcemnet」を編集します。
def display_status
    { nil => "未送信", "delivered" => "送信済み", "delivering" => "送信中" }[status]
  end

  def not_delivered?
    status.nil?
  end

  def delivered?
    status == "delivered"
  end

  def delivering?
    status == "delivering"
  end

  def update_directly(updates)
    assign_attributes(updates, :without_protection => true)
    Announcement.update_all(updates, :id => self.id)
  end

  def deliver
    return unless not_delivered?
    update_directly(:status => "delivering")
 
    provider = NotificationProvider.open
    success = 0
    failure = 0
    total = DeviceToken.count
    DeviceToken.all.each do |dt|
      if provider.notify(dt, self.content, どれくらい送り続けるかの日数, self)
        success += 1
      else
        failure += 1
      end
      if provider.closed?
        info("APN Server closed unexpectedly!")
        break
      end
    end
    provider.close
    info("total: #{total}, success: #{success}, failure: #{failure}.")
    update_directly(:log => self.log, :status => "delivered")
  end

  def info(text)
    self.log = "" if log.blank?
    self.log += Time.now.strftime("%Y-%m-%d %H:%M:%S ") + text + "\n"
    # cronログを作成している
  end

  class << self
    def deliver_all
      DeviceToken.where("updated_at < ?", デバイストークンの有効期限).delete_all
     # デバイストークンをupdateしていない場合削除される

      announcements = active.not_delivered.past
      count = announcements.count
      announcements.each do |ann|
        ann.deliver
      end
      Rails.logger.info "executed delivered_all, #{count}"
    end
  end
以上の内容でお知らせをAPNsを使ってiPhoneやiPadでのアプリに表示させることが出来る。
ほぼ定型文で保存かな?

0 件のコメント:

コメントを投稿