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