spaceship使用

簡介

spaceship exposes both the Apple Developer Center and the App Store Connect API. It’s super fast, well tested and supports all of the operations you can do via the browser. It powers parts of fastlane, and can be leveraged for more advanced fastlane features. Scripting your Developer Center workflow has never been easierjavascript

  • Blazing fast communication using only a HTTP client
  • Object oriented access to all resources
  • Resistant against front-end design changes of the of the Apple Developer Portal
  • One central tool for the communication
  • Automatic re-trying of requests in case a timeout occurs
  • No web scraping
  • 90%+ test coverage by stubbing server responses

spaceship同時暴露了Apple Developer Center 和 App Store Connect 的api操做,速度快,在瀏覽器上能夠完成的操做,它均可以使用腳本實現。由於spaceship的全部操做都是使用http直接請求蘋果底層api,而非採用網絡爬蟲的方式,因此會比在瀏覽器上操做更快。減小了一些冗餘的網絡資源請求以及加載java

在 Apple Developer Center 下能夠添加證書、設備等操做。在 App Store Connect 中能夠建立新app、修改現有app信息、管理成員信息、提交testflight、獲取財務帳單等等操做。git

因此平時一些頻繁或者複雜的操做徹底可使用spaceship來替代,避免由於使用瀏覽器而進行的漫長等待。並且官方也在一步步的公開api訪問的方式,說明這是一個趨勢github

源碼目錄以下:web

space_1.png

  • connect_apijson

    包裝蘋果官方開放的App Store Connsect APIapi

  • portal瀏覽器

    處理蘋果開發者中心的一些操做,操做證書、設備、描述文件等須要用此套apiruby

  • test_flightmarkdown

    操做app的testflight

  • tunes

    處理appstoreconnect的一些操做,此代碼爲老版的操做方式,包含api比較全。部分操做可以使用 connect_api 採用蘋果官方開發的api

登陸

spaceship的全部操做都須要登陸,因此首先研究的就是登陸的步驟。

require 'spaceship'

# tunes登陸
tunes = Spaceship::Tunes.login('xxx', 'xxx')

# portal登陸
portal = Spaceship::Portal.login('xxx', 'xxx');
複製代碼

調用鏈以下:

client.rb 中的 login() 方法檢驗帳號密碼的合法性,而後調用client.rb 中的 do_login() 方法,內部調用子類中實現的 `send_login_request()方法

send_login_request 的具體實如今對應的子類中(tunes_client.rbportal_client.rb

def send_login_request(user, password)
    clear_user_cached_data
    result = send_shared_login_request(user, password)

    store_cookie

    return result
end
複製代碼

最終的邏輯處理都是在client.rb 中的 send_shared_login_request() 方法內

def send_shared_login_request(user, password)
      
      if load_session_from_file
        # Check if the session is still valid here
        begin
          return true if fetch_olympus_session
        rescue
          puts("Available session is not valid any more. Continuing with normal login.")
        end
      end
      
  
      # The user can pass the session via environment variable (Mainly used in CI environments)
      if load_session_from_env
        # see above
        begin
          # see above
          return true if fetch_olympus_session
        rescue
          puts("Session loaded from environment variable is not valid. Continuing with normal login.")
          # see above
        end
      end
      #
      # After this point, we sure have no valid session any more and have to create a new one
      #

      data = {
        accountName: user,
        password: password,
        rememberMe: true
      }

      begin
        # 請求登陸接口

      # Now we know if the login is successful or if we need to do 2 factor

      # 根據返回狀態執行下一步操做
      case response.status
      when 403
        raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
      when 200
        fetch_olympus_session
        return response
      when 409
        # 2 step/factor is enabled for this account, first handle that
        handle_two_step_or_factor(response)
        # and then get the olympus session
        fetch_olympus_session
        return true     
      else
        ......
      end
end
複製代碼
  1. load_session_from_file 中加載cookie數據,若是有數據,調用fetch_olympus_session判斷session是否有效,如有效,說明登陸還在有效期,則直接返回成功

  2. load_session_from_env 中獲取用戶是否設置了本地環境變量(ENV["FASTLANE_SESSION"] || ENV["SPACESHIP_SESSION"])存儲session值,如有值,接下來跟第一步後續操做同樣,判斷session是否有效

  3. 若前兩步都未成功,咱們須要從新登陸,獲取有效的session

    data = {
      accountName: user,
      password: password,
      rememberMe: true
    }
    
    important_cookie = @cookie.store.entries.find { |a| a.name.include?("DES") }
    if important_cookie
      modified_cookie = self.cookie # returns a string of all cookies
      unescaped_important_cookie = "#{important_cookie.name}=#{important_cookie.value}"
      escaped_important_cookie = "#{important_cookie.name}=\"#{important_cookie.value}\""
      modified_cookie.gsub!(unescaped_important_cookie, escaped_important_cookie)
    end
    
    response = request(:post) do |req|
      req.url("https://idmsa.apple.com/appleauth/auth/signin")
      req.body = data.to_json
      req.headers['Content-Type'] = 'application/json'
      req.headers['X-Requested-With'] = 'XMLHttpRequest'
      req.headers['X-Apple-Widget-Key'] = self.itc_service_key
      req.headers['Accept'] = 'application/json, text/javascript'
      req.headers["Cookie"] = modified_cookie if modified_cookie
    end
    複製代碼
  4. 調用登陸接口完成以後,根據response.status判斷執行下一步操做

    • 403

      用戶或密碼有問題,登陸不成功

    • 200

      登陸成功。調用 fetch_olympus_session 請求session數據

    • 409

      首先須要進行雙重認證 handle_two_step_or_factor(response),而後請求session數據。

    雙重認證的處理邏輯都在此 two_step_or_factor_client.rb 文件中處理,包含了受信任設備以及受信任手機號的處理,完成驗證以後會存儲session,下一次請求直接加載seesion,無需再次登陸

    if r.body.kind_of?(Hash) && r.body["trustedDevices"].kind_of?(Array)
      # 受信任設備
      handle_two_step(r)
    elsif r.body.kind_of?(Hash) && r.body["trustedPhoneNumbers"].kind_of?(Array) && r.body["trustedPhoneNumbers"].first.kind_of?(Hash)
      # 受信任手機號
      handle_two_factor(r)
    else
      ...
    end
    複製代碼
    # Responsible for setting all required header attributes for the requests
    # to succeed
    def update_request_headers(req)
      req.headers["X-Apple-Id-Session-Id"] = @x_apple_id_session_id
      req.headers["X-Apple-Widget-Key"] = self.itc_service_key
      req.headers["Accept"] = "application/json"
      req.headers["scnt"] = @scnt
    end
    複製代碼

App Store Connect API

可實現的功能詳細參考這裏 或者直接查看源碼 tunes_client.rb

拿兩個簡單的例子來看一下具體的用法。這裏使用的是老版本的方式,並非蘋果開放的官方apiApp Store Connect API

  • App解決方案中心
  • 添加內購

1. App解決方案中心

require 'spaceship'

# 1.登陸
Spaceship::Tunes.login(user, pwd)
Spaceship::Tunes.select_team(team_id: xxx)

# 2.根據bundleId選擇app
app = Spaceship::Tunes::Application.find(bundle_id)

# 3.調用解決方案中心接口
response = app.resolution_center

# 4.根據response查找出最新信息並郵件通知對應負責人
...
複製代碼

space_2.png

上圖爲解決方案中心實際返回的json數據,能夠經過抓包查看數據結構,也能夠直接調用spaceship的接口查看返回值。本身根據狀況對數據進行自定義的分析處理。

ps:原本準備本身經過抓包找到接口調用,後來在寫的過程當中發現spaceship已經給提供了對應的方法resolution_center 以及解釋。因此之後再遇到一些想實現的功能時,能夠提早查看源碼看系統是否已經提供,避免走彎路

# @return (Hash) Contains the reason for rejection.
      # if everything is alright, the result will be
      # `{"sectionErrorKeys"=>[], "sectionInfoKeys"=>[], "sectionWarningKeys"=>[], "replyConstraints"=>{"minLength"=>1, "maxLength"=>4000}, "appNotes"=>{"threads"=>[]}, "betaNotes"=>{"threads"=>[]}, "appMessages"=>{"threads"=>[]}}`
      def resolution_center
        client.get_resolution_center(apple_id, platform)
      end
複製代碼

2. 給App添加內購

require 'spaceship'

# 1.登陸
Spaceship::Tunes.login(user, pwd)
Spaceship::Tunes.select_team(team_id: xxx)

# 2.根據bundleId選擇app
app = Spaceship::Tunes::Application.find(bundle_id)

# 3. 建立內購商品
begin
  @app.in_app_purchases.create!( # 建立商品
    type: iapType,
    versions: {
      "zh-Hans": {
        name: name,
        description: desc
      }
    },
    reference_name: reference,
    product_id: product_id,
    review_notes: review_desc,
    review_screenshot: review_image_path,
    pricing_intervals: [
      {
        country: "WW",
        begin_date: nil,
        end_date: nil,
        tier: tier
      }
    ],
    family_id: family_id,
    subscription_free_trial: subscription_free_trial,
    subscription_duration: subscription_duration,
    subscription_price_target: subscription_price_target
  )
  puts "#建立成功!!! "
rescue Exception => error
  puts "#建立失敗: #{error} "
end
複製代碼
  • iapType商品對應的類型

    # 獲取到對應的商品類型
    def get_correct_iapType(type)
      if type == "消耗品"
        return Spaceship::Tunes::IAPType::CONSUMABLE
      elsif type == "非消耗品"
        return Spaceship::Tunes::IAPType::NON_CONSUMABLE
      elsif type == "非自動續訂"
        return Spaceship::Tunes::IAPType::NON_RENEW_SUBSCRIPTION
      elsif type == "自動續訂"
        return Spaceship::Tunes::IAPType::RECURRING
      end
    end
    複製代碼
  • product_id

    商品惟一id

    ...

對應的還有對商品修改方法

# 根據商品id查找到對應商品
purch = @app.in_app_purchases.find(product_id)
e = purch.edit # 編輯商品
e.versions = {
  "zh-Hans": {
    name: name,
    description: desc
  }
}
e.reference_name = reference
e.review_notes = review_desc
e.review_screenshot = review_image_path
e.pricing_intervals = [
  {
    country: "WW",
    begin_date: nil,
    end_date: nil,
    tier: tier
  }
]
# family_id = family_id,
e.subscription_free_trial = subscription_free_trial,
e.subscription_duration = subscription_duration,
e.subscription_price_target = subscription_price_target
e.save!
複製代碼

建立羣組方法

value = create_family(
  name: product["family_name"],
  product_id: product_id,
  reference_name: name,
  versions: {
    "zh-Hans": {
      subscription_name: "xxx",
      name: product["family_name"]
    }
  }
)
複製代碼

Apple Developer Portal API

可實現的功能詳細參考這裏 或者直接查看源碼 portal_client.rb

參考資料

spaceship

spaceship文檔

App Store Connect API

Authentication.md

Authenticating with Apple services

相關文章
相關標籤/搜索