shopify_api.rb 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. require 'active_resource'
  2. require 'ostruct'
  3. require 'digest/md5'
  4. module ShopifyAPI
  5. #
  6. # The Shopify API authenticates each call via HTTP Authentication, using
  7. # * the application's API key as the username, and
  8. # * a hex digest of the application's shared secret and an
  9. # authentication token as the password.
  10. #
  11. # Generation & acquisition of the beforementioned looks like this (assuming the ):
  12. #
  13. # 0. Developer (that's you) registers Application (and provides a
  14. # callback url) and receives an API key and a shared secret
  15. #
  16. # 1. User visits Application and are told they need to authenticate the
  17. # application first for read/write permission to their data (needs to
  18. # happen only once). User is asked for their shop url.
  19. #
  20. # 2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?app=<API key>
  21. # (See Session#create_permission_url)
  22. #
  23. # 3. User logs-in to Shopify, approves application permission request
  24. #
  25. # 4. Shopify redirects to the Application's callback url (provided during
  26. # registration), including the shop's name, and an authentication token in the parameters:
  27. # GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
  28. #
  29. # 5. Authentication password computed using the shared secret and the
  30. # authentication token (see Session#computed_password)
  31. #
  32. # 6. Profit!
  33. # (API calls can now authenticate through HTTP using the API key, and
  34. # computed password)
  35. #
  36. # LoginController and ShopifyLoginProtection use the Session class to set ActiveResource::Base.site
  37. # so that all API calls are authorized transparently and end up just looking like this:
  38. #
  39. # # get 3 products
  40. # @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
  41. #
  42. # # get latest 3 orders
  43. # @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
  44. #
  45. # As an example of what your LoginController should look like, take a look
  46. # at the following:
  47. #
  48. # class LoginController < ApplicationController
  49. # def index
  50. # # Ask user for their #{shop}.myshopify.com address
  51. # end
  52. #
  53. # def authenticate
  54. # redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
  55. # end
  56. #
  57. # # Shopify redirects the logged-in user back to this action along with
  58. # # the authorization token t.
  59. # #
  60. # # This token is later combined with the developer's shared secret to form
  61. # # the password used to call API methods.
  62. # def finalize
  63. # shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
  64. # if shopify_session.valid?
  65. # session[:shopify] = shopify_session
  66. # flash[:notice] = "Logged in to shopify store."
  67. #
  68. # return_address = session[:return_to] || '/home'
  69. # session[:return_to] = nil
  70. # redirect_to return_address
  71. # else
  72. # flash[:error] = "Could not log in to Shopify store."
  73. # redirect_to :action => 'index'
  74. # end
  75. # end
  76. #
  77. # def logout
  78. # session[:shopify] = nil
  79. # flash[:notice] = "Successfully logged out."
  80. #
  81. # redirect_to :action => 'index'
  82. # end
  83. # end
  84. #
  85. class Session
  86. cattr_accessor :api_key
  87. cattr_accessor :secret
  88. cattr_accessor :protocol
  89. self.protocol = 'https'
  90. attr_accessor :url, :token, :name
  91. def self.setup(params)
  92. params.each { |k,value| send("#{k}=", value) }
  93. end
  94. def initialize(url, token = nil)
  95. raise ArgumentError.new("You must provide at least a URL to a Shopify store!") if url.blank?
  96. url.gsub!(/https?:\/\//, '') # remove http:// or https://
  97. url = "#{url}.myshopify.com" unless url.include?('.') # extend url to myshopify.com if no host is given
  98. self.url, self.token = url, token
  99. end
  100. def shop
  101. Shop.current
  102. end
  103. def create_permission_url
  104. "http://#{url}/admin/api/auth?api_key=#{api_key}"
  105. end
  106. # Used by ActiveResource::Base to make all non-authentication API calls
  107. #
  108. # (ActiveResource::Base.site set in ShopifyLoginProtection#shopify_session)
  109. def site
  110. "#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
  111. end
  112. def valid?
  113. [url, token].all?
  114. end
  115. private
  116. # The secret is computed by taking the shared_secret which we got when
  117. # registring this third party application and concating the request_to it,
  118. # and then calculating a MD5 hexdigest.
  119. def computed_password
  120. Digest::MD5.hexdigest(secret + token.to_s)
  121. end
  122. end
  123. # Shop object. Use Shop.current to receive
  124. # the shop. Since you can only ever reference your own
  125. # shop this model does not have a .find method.
  126. #
  127. class Shop
  128. def self.current
  129. ActiveResource::Base.find(:one, :from => "/admin/shop.xml")
  130. end
  131. end
  132. # Custom collection
  133. #
  134. class CustomCollection < ActiveResource::Base
  135. def products
  136. Product.find(:all, :params => {:collection_id => self.id})
  137. end
  138. def add_product(product)
  139. Collect.create(:collection_id => self.id, :product_id => product.id)
  140. end
  141. def remove_product(product)
  142. collect = Collect.find(:first, :params => {:collection_id => self.id, :product_id => product.id})
  143. collect.destroy if collect
  144. end
  145. end
  146. class SmartCollection < ActiveResource::Base
  147. def products
  148. Product.find(:all, :params => {:collection_id => self.id})
  149. end
  150. end
  151. # For adding/removing products from custom collections
  152. class Collect < ActiveResource::Base
  153. end
  154. class ShippingAddress < ActiveResource::Base
  155. end
  156. class BillingAddress < ActiveResource::Base
  157. end
  158. class LineItem < ActiveResource::Base
  159. end
  160. class ShippingLine < ActiveResource::Base
  161. end
  162. class Order < ActiveResource::Base
  163. def close; load_attributes_from_response(post(:close)); end
  164. def open; load_attributes_from_response(post(:open)); end
  165. def transactions
  166. Transaction.find(:all, :params => { :order_id => id })
  167. end
  168. def capture(amount = "")
  169. Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
  170. end
  171. end
  172. class Product < ActiveResource::Base
  173. # Share all items of this store with the
  174. # shopify marketplace
  175. def self.share; post :share; end
  176. def self.unshare; delete :share; end
  177. # compute the price range
  178. def price_range
  179. prices = variants.collect(&:price)
  180. format = "%0.2f"
  181. if prices.min != prices.max
  182. "#{format % prices.min} - #{format % prices.max}"
  183. else
  184. format % prices.min
  185. end
  186. end
  187. def collections
  188. CustomCollection.find(:all, :params => {:product_id => self.id})
  189. end
  190. def smart_collections
  191. SmartCollection.find(:all, :params => {:product_id => self.id})
  192. end
  193. def add_to_collection(collection)
  194. collection.add_product(self)
  195. end
  196. def remove_from_collection(collection)
  197. collection.remove_product(self)
  198. end
  199. end
  200. class Variant < ActiveResource::Base
  201. self.prefix = "/admin/products/:product_id/"
  202. end
  203. class Image < ActiveResource::Base
  204. self.prefix = "/admin/products/:product_id/"
  205. # generate a method for each possible image variant
  206. [:pico, :icon, :thumb, :small, :medium, :large, :original].each do |m|
  207. reg_exp_match = "/\\1_#{m}.\\2"
  208. define_method(m) { src.gsub(/\/(.*)\.(\w{2,4})/, reg_exp_match) }
  209. end
  210. def attach_image(data, filename = nil)
  211. attributes[:attachment] = Base64.encode64(data)
  212. attributes[:filename] = filename unless filename.nil?
  213. end
  214. end
  215. class Transaction < ActiveResource::Base
  216. self.prefix = "/admin/orders/:order_id/"
  217. end
  218. class Fulfillment < ActiveResource::Base
  219. self.prefix = "/admin/orders/:order_id/"
  220. end
  221. class Country < ActiveResource::Base
  222. end
  223. class Page < ActiveResource::Base
  224. end
  225. class Blog < ActiveResource::Base
  226. def articles
  227. Article.find(:all, :params => { :blog_id => id })
  228. end
  229. end
  230. class Article < ActiveResource::Base
  231. self.prefix = "/admin/blogs/:blog_id/"
  232. end
  233. class Comment < ActiveResource::Base
  234. def remove; load_attributes_from_response(post(:remove)); end
  235. def ham; load_attributes_from_response(post(:ham)); end
  236. def spam; load_attributes_from_response(post(:spam)); end
  237. def approve; load_attributes_from_response(post(:approve)); end
  238. end
  239. class Province < ActiveResource::Base
  240. self.prefix = "/admin/countries/:country_id/"
  241. end
  242. class Redirect < ActiveResource::Base
  243. end
  244. end