aliexpress 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 72fe66faf65e1f93edd912f2ba24148ca70c5ae1
4
+ data.tar.gz: 5ec3aa35f98e2deea15312fd5d243169d281dd43
5
+ SHA512:
6
+ metadata.gz: db423a70fbb44d9063a2e7e5f809aaa3948a0bf7bbf7bcd5ea69bb7b53e53d75e3e77de01cf51d509eafcd193182d3f635b754b060fdc7b902f5f49b96344cec
7
+ data.tar.gz: 2caf268645d24586782369475cb5437e7ced287508746db1e4afa52616df55130638dd558bfb51b5f4ae1101f6046ca26e2b9ef5ec4a9fd524e14a0e6255c8a2
@@ -0,0 +1 @@
1
+ service_name: travis-ci
@@ -0,0 +1,11 @@
1
+ *.gem
2
+ /Gemfile.lock
3
+ /.approvals
4
+ /.bundle
5
+ /coverage
6
+ .DS_Store
7
+
8
+ /.client_id
9
+ /.client_secret
10
+ /.access_token
11
+ /.refresh_token
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,38 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.2
3
+
4
+ Metrics/AbcSize:
5
+ Enabled: false
6
+ Metrics/CyclomaticComplexity:
7
+ Enabled: false
8
+ Metrics/PerceivedComplexity:
9
+ Enabled: false
10
+
11
+ Metrics/LineLength:
12
+ Enabled: false
13
+ Metrics/ClassLength:
14
+ Enabled: false
15
+ Metrics/MethodLength:
16
+ Enabled: false
17
+ Metrics/BlockLength:
18
+ Enabled: false
19
+ Metrics/ParameterLists:
20
+ Enabled: false
21
+
22
+ Lint/EndAlignment:
23
+ Enabled: false
24
+
25
+ Style/Documentation:
26
+ Enabled: false
27
+ Style/IndentationWidth:
28
+ Enabled: false
29
+ Style/ElseAlignment:
30
+ Enabled: false
31
+ Style/GuardClause:
32
+ Enabled: false
33
+ Style/NumericLiterals:
34
+ Enabled: false
35
+
36
+ # This is temporary - it can be remove after updating rubocop to the latest.
37
+ Style/PercentLiteralDelimiters:
38
+ Enabled: false
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2
4
+ # branches:
5
+ # only:
6
+ # - master
7
+ script:
8
+ - bundle exec rubocop
9
+ - bundle exec rake
10
+ cache: bundler
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ group :development, :test do
5
+ gem 'awesome_print'
6
+ gem 'pry'
7
+ gem 'rake'
8
+ gem 'rubocop'
9
+ end
10
+
11
+ group :test do
12
+ gem 'coveralls', require: false
13
+ gem 'rspec'
14
+ gem 'webmock'
15
+ end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Tophatter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ [![Build Status](https://travis-ci.org/tophatter/aliexpress-api-ruby.svg?branch=master)](https://travis-ci.org/tophatter/aliexpress-api-ruby)
2
+ [![Coverage Status](https://coveralls.io/repos/github/tophatter/aliexpress-api-ruby/badge.svg?branch=master)](https://coveralls.io/github/tophatter/aliexpress-api-ruby?branch=master)
3
+
4
+ ### AliExpress SDK for Ruby:
5
+
6
+ #### Authentication:
7
+
8
+ To use this SDK, you must have an app key and app secret (a.k.a. client id and client secret). These can be obtained [here](https://portals.aliexpress.com/adcenter/apiSetting.htm) if you're using the affiliate program, or [here](http://isvus.aliexpress.com/isv/index.htm) if you're using the ISV API.
9
+
10
+ In the affiliate program API settings, you'll see the following:
11
+
12
+ ![](https://i.imgur.com/ycMNgMY.png)
13
+
14
+ The "api key" is your client id, and the "digital signature" is your client secret.
15
+
16
+ The API uses OAuth for authentication. You need to first go through a manual authorization sequence to get a short-live authorization code. That code can be exchanged for an access token that last 10 hours, and a refresh token that lasts significantly longer.
17
+
18
+ Using this gem's console, you get obtain an access token as follows:
19
+
20
+ ```
21
+ bash $ bin/console
22
+ [1] pry(main)> AliExpress.client_id = [YOUR CLIENT ID]
23
+ [2] pry(main)> AliExpress.client_secret = [YOUR CLIENT SECRET]
24
+ [3] pry(main)> AliExpress::Auth.authorize
25
+ Open https://gw.api.alibaba.com/auth/authorize.htm?_aop_signature=REDACTED&client_id=REDACTED&redirect_uri=REDACTED&site=aliexpress&state=REDACTED in your browser, and then enter the code below.
26
+ authorization_code> [ENTER YOUR CODE HERE]
27
+ {
28
+ "refresh_token_timeout" => "20180402143144000-0700",
29
+ "aliId" => "[REDACTED]",
30
+ "resource_owner" => "[REDACTED]",
31
+ :access_token => "[REDACTED]",
32
+ :refresh_token => "[REDACTED]",
33
+ :expires_at => 1507194954
34
+ }
35
+ ```
36
+
37
+ Once your tokens are obtained, you can instruct this SDK to use them:
38
+
39
+ ```
40
+ AliExpress.access_token = [YOUR ACCESS TOKEN]
41
+ AliExpress.refresh_token = [YOUR REFRESH TOKEN]
42
+ ```
43
+
44
+ If your access token expires, you can get a new one with the following:
45
+
46
+ ```
47
+ AliExpress::Auth.refresh
48
+ ```
49
+
50
+ #### Affiliate API:
51
+
52
+ ##### api.listPromotionProduct
53
+
54
+ ```
55
+ [1] pry(main)> AliExpress::Affiliate.list_promotion_product(keywords: 'drone')
56
+ D, [2017-10-05T11:23:03.971387 #69079] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listPromotionProduct/[REDACTED]?fields=productId%2CproductTitle%2CproductUrl%2CimageUrl%2CallImageUrls%2CoriginalPrice%2CsalePrice%2ClocalPrice%2Cdiscount%2Cvolume%2CpackageType%2ClotNum%2CvalidTime&keywords=drone
57
+ [
58
+ [ 0] {
59
+ "lotNum" => 1,
60
+ "packageType" => "piece",
61
+ "imageUrl" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg",
62
+ "allImageUrls" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1O6LUSFXXXXX5aXXXq6xXFXXXB/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1aA.DSFXXXXXgXXXXq6xXFXXX3/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1N.kySFXXXXaUXXXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1lc_1SFXXXXcgXVXXq6xXFXXX9/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB16lEkSFXXXXcpXpXXq6xXFXXXK/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg",
63
+ "productId" => 32817202058,
64
+ "discount" => "5%",
65
+ "originalPrice" => "US $27.99",
66
+ "productTitle" => "100% Original SYMA X5C (Upgrade Version) RC <font><b>Drone</b></font> With 2MP HD Camera 6-Axis RC Quadcopter Helicopter X5 Dron Without Camera",
67
+ "validTime" => "2017-10-08",
68
+ "productUrl" => "https://www.aliexpress.com/item/SYMA-X5C-1-Upgrade-Version-SYMA-X5C-Professional-RC-Drone-6-Axis-Helicopter-Quadcopter-With-2MP/32817202058.html",
69
+ "volume" => "14",
70
+ "salePrice" => "US $26.59"
71
+ },
72
+ <snip>
73
+ ```
74
+
75
+ ##### api.getPromotionProductDetail
76
+
77
+ ```
78
+ [1] pry(main)> AliExpress::Affiliate.get_promotion_product_detail(32817202058)
79
+ D, [2017-10-05T11:24:19.235970 #69079] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.getPromotionProductDetail/[REDACTED]?fields=productId%2CproductTitle%2CproductUrl%2CimageUrl%2CallImageUrls%2CoriginalPrice%2CsalePrice%2ClocalPrice%2Cdiscount%2Cvolume%2CpackageType%2ClotNum%2CvalidTime&productId=32817202058
80
+ {
81
+ "lotNum" => 1,
82
+ "packageType" => "piece",
83
+ "imageUrl" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg",
84
+ "allImageUrls" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1O6LUSFXXXXX5aXXXq6xXFXXXB/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1aA.DSFXXXXXgXXXXq6xXFXXX3/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1N.kySFXXXXaUXXXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1lc_1SFXXXXcgXVXXq6xXFXXX9/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB16lEkSFXXXXcpXpXXq6xXFXXXK/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg",
85
+ "productId" => 32817202058,
86
+ "discount" => "5%",
87
+ "originalPrice" => "US $27.99",
88
+ "productTitle" => "100% Original SYMA X5C (Upgrade Version) RC Drone With 2MP HD Camera 6-Axis RC Quadcopter Helicopter X5 Dron Without Camera",
89
+ "validTime" => "2017-10-08",
90
+ "productUrl" => "https://www.aliexpress.com/item/SYMA-X5C-1-Upgrade-Version-SYMA-X5C-Professional-RC-Drone-6-Axis-Helicopter-Quadcopter-With-2MP/32817202058.html",
91
+ "volume" => "14",
92
+ "salePrice" => "US $26.59"
93
+ }
94
+ ```
95
+
96
+ ##### api.listHotProducts
97
+
98
+ ```
99
+ [1] pry(main)> AliExpress::Affiliate.list_hot_products(3)
100
+ D, [2017-10-05T11:21:54.835675 #68982] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listHotProducts/[REDACTED]?categoryId=3&language=en&localCurrency=USD
101
+ [
102
+ [ 0] {
103
+ "localPrice" => "21.00 $",
104
+ "imageUrl" => "https://ae01.alicdn.com/kf/HTB1YsQwQFXXXXcvXpXXq6xXFXXX0/Preself-Hoodies-Sweatshirt-Women-Casual-Outwear-Hoody-Loose-Long-Sleeve-Mantle-Hooded-Cover-Pullover-Clothes-2017.jpg",
105
+ "productUrl" => "https://www.aliexpress.com/item/New-Women-Loose-Fashion-Autumn-Winter-Hooded-Oversize-Sweatshirt-Coat-Outwear/32763830156.html",
106
+ "validTime" => "2017-10-18",
107
+ "productTitle" => "Preself Hoodies Sweatshirt Women Casual Outwear Hoody Loose Long Sleeve Mantle Hooded Cover Pullover Clothes 2017 New",
108
+ "volume" => "",
109
+ "salePrice" => "US $21.00",
110
+ "productId" => 32763830156
111
+ },
112
+ <snip>
113
+ ```
114
+
115
+ #### ISV API:
116
+
117
+ In progress.
@@ -0,0 +1,3 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec)
3
+ task default: :spec
@@ -0,0 +1,29 @@
1
+ # To publish the next version:
2
+ # gem build aliexpress.gemspec
3
+ # gem push aliexpress-{VERSION}.gem
4
+ Gem::Specification.new do |s|
5
+ s.name = 'aliexpress'
6
+ s.version = '0.4'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.licenses = ['MIT']
9
+ s.authors = ['Chris Estreich']
10
+ s.email = ['chris@tophatter.com']
11
+ s.homepage = 'https://github.com/tophatter/aliexpress-api-ruby'
12
+ s.summary = 'AliExpress SDK for ruby.'
13
+ s.description = 'AliExpress SDK for ruby.'
14
+
15
+ s.extra_rdoc_files = ['README.md']
16
+
17
+ s.required_ruby_version = '~> 2.0'
18
+
19
+ s.add_dependency 'rest-client', '~> 1.6'
20
+ s.add_dependency 'addressable'
21
+ s.add_dependency 'oauth2'
22
+
23
+ s.post_install_message = 'Documentation is available at: http://isvus.aliexpress.com/isv/index.htm'
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
28
+ s.require_paths = ['lib']
29
+ end
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup' # Otherwise "require 'aliexpress'" won't work.
4
+ require 'aliexpress'
5
+ require 'awesome_print'
6
+ require 'pry'
7
+
8
+ %w(client_id client_secret access_token refresh_token).each do |method|
9
+ begin
10
+ AliExpress.send("#{method}=", File.read(".#{method}").chomp)
11
+ puts "AliExpress.#{method}: #{AliExpress.send(method)}"
12
+ rescue Errno::ENOENT
13
+ puts "No .#{method} file, using default: #{AliExpress.send(method)}"
14
+ end
15
+ end
16
+
17
+ AliExpress.logger = Logger.new(STDOUT)
18
+ AliExpress.logger.level = Logger::DEBUG
19
+
20
+ AwesomePrint.pry!
21
+ Pry.start
@@ -0,0 +1,45 @@
1
+ require 'json'
2
+ require 'rest-client'
3
+ require 'addressable'
4
+ require 'oauth2'
5
+
6
+ module AliExpress
7
+ class << self
8
+ attr_accessor :client_id, :client_secret, :access_token, :refresh_token
9
+
10
+ attr_writer :protocol, :host, :base_uri
11
+ attr_writer :currency, :language
12
+ attr_writer :logger
13
+
14
+ def protocol
15
+ @protocol || 'https'
16
+ end
17
+
18
+ def host
19
+ @host || 'gw.api.alibaba.com'
20
+ end
21
+
22
+ def base_uri
23
+ @base_uri || "#{protocol}://#{host}/openapi"
24
+ end
25
+
26
+ def currency
27
+ @currency || 'USD'
28
+ end
29
+
30
+ def language
31
+ @language || 'en'
32
+ end
33
+
34
+ def logger
35
+ @logger ||= Logger.new('/dev/null')
36
+ @logger
37
+ end
38
+ end
39
+ end
40
+
41
+ require File.dirname(__FILE__) + '/aliexpress/base'
42
+ require File.dirname(__FILE__) + '/aliexpress/response_error'
43
+ require File.dirname(__FILE__) + '/aliexpress/auth'
44
+ require File.dirname(__FILE__) + '/aliexpress/product'
45
+ require File.dirname(__FILE__) + '/aliexpress/affiliate'
@@ -0,0 +1,141 @@
1
+ module AliExpress
2
+ class Affiliate < Base
3
+ CATEGORIES = {
4
+ 'Apparel & Accessories' => 3,
5
+ 'Automobiles & Motorcycles' => 34,
6
+ 'Beauty & Health' => 66,
7
+ 'Books for Local Russian' => 200004360,
8
+ 'Computer & Office' => 7,
9
+ 'Home Improvement' => 13,
10
+ 'Consumer Electronics' => 44,
11
+ 'Electrical Equipment & Supplies' => 5,
12
+ 'Electronic Components & Supplies' => 502,
13
+ 'Food' => 2,
14
+ 'Furniture' => 1503,
15
+ 'Hair & Accessories' => 200003655,
16
+ 'Hardware' => 42,
17
+ 'Home & Garden' => 15,
18
+ 'Home Appliances' => 6,
19
+ 'Industry & Business' => 200001996,
20
+ 'Jewelry & Accessories' => 36,
21
+ 'Lights & Lighting' => 39,
22
+ 'Luggage & Bags' => 1524,
23
+ 'Mother & Kids' => 1501,
24
+ 'Office & School Supplies' => 21,
25
+ 'Phones & Telecommunications' => 509,
26
+ 'Security & Protection' => 30,
27
+ 'Shoes' => 322,
28
+ 'Special Category' => 200001075,
29
+ 'Sports & Entertainment' => 18,
30
+ 'Tools' => 1420,
31
+ 'Toys & Hobbies' => 26,
32
+ 'Travel and Coupon Services' => 200003498,
33
+ 'Watches' => 1511,
34
+ 'Weddings & Events' => 320
35
+ }.freeze
36
+
37
+ FILTERS = %w(category keywords min_price max_price high_quality).freeze
38
+ SORTS = %w(orignalPriceUp orignalPriceDown sellerRateUp sellerRateDown).freeze
39
+ FIELDS = %w(productId productTitle productUrl imageUrl allImageUrls originalPrice salePrice localPrice discount volume).freeze
40
+
41
+ SUCCESS = 20010000
42
+ ERRORS = {
43
+ 20020000 => 'System error.',
44
+ 20030000 => 'Unauthorized transfer request.',
45
+ 20030010 => 'Required parameters.',
46
+ 20030020 => 'Invalid protocol format.',
47
+ 20030030 => 'API version input parameter error.',
48
+ 20030040 => 'API namespace input parameter error.',
49
+ 20030050 => 'API name input parameter error.',
50
+ 20030060 => 'Fields input parameter error.',
51
+ 20030070 => 'Keyword input parameter error.',
52
+ 20030080 => 'Category ID input parameter error.',
53
+ 20030140 => 'Page number input parameter error.',
54
+ 20030150 => 'Page size input parameter error (default = 20, max = 40).',
55
+ 20030160 => 'Sort input parameter error.'
56
+ }.freeze
57
+
58
+ class << self
59
+ def search(query:, filters: {}, sort: 'orignalPriceUp', page: nil, per_page: 40, fields: FIELDS)
60
+ response = get(
61
+ api_call: 'api.listPromotionProduct',
62
+ params: {
63
+ keywords: query,
64
+ # filters
65
+ categoryId: CATEGORIES[filters[:category]] || filters[:category],
66
+ originalPriceFrom: filters[:min_price],
67
+ originalPriceTo: filters[:max_price],
68
+ highQualityItems: filters[:high_quality],
69
+ # sorting
70
+ sort: sort,
71
+ # paging
72
+ pageNo: page,
73
+ pageSize: per_page,
74
+ # response format
75
+ fields: fields.join(',')
76
+ }.delete_if { |_k, v| v.nil? }
77
+ )
78
+
79
+ raise_response_error(response) unless response['errorCode'] == SUCCESS
80
+ response
81
+ end
82
+
83
+ def find(id, fields: FIELDS)
84
+ response = get(
85
+ api_call: 'api.getPromotionProductDetail',
86
+ params: { productId: id, fields: fields.join(',') }
87
+ )
88
+
89
+ raise_response_error(response) unless response['errorCode'] == SUCCESS
90
+ response
91
+ end
92
+
93
+ def similar(id)
94
+ response = get(
95
+ api_call: 'api.listSimilarProducts',
96
+ params: { productId: id }
97
+ )
98
+
99
+ raise_response_error(response) unless response['errorCode'] == SUCCESS
100
+ response
101
+ end
102
+
103
+ def popular(category)
104
+ response = get(
105
+ api_call: 'api.listHotProducts',
106
+ params: { categoryId: CATEGORIES[category] || category }
107
+ )
108
+
109
+ raise_response_error(response) unless response['errorCode'] == SUCCESS
110
+ response
111
+ end
112
+
113
+ private
114
+
115
+ def get(hash)
116
+ defaults = {
117
+ api_namespace: 'portals.open',
118
+ api_version: 2,
119
+ auth: false,
120
+ sign: false,
121
+ params: {
122
+ localCurrency: currency,
123
+ language: language
124
+ }
125
+ }
126
+ params = defaults[:params].merge(hash.delete(:params))
127
+ super(defaults.merge(hash).merge(params: params))
128
+ end
129
+
130
+ def raise_response_error(response)
131
+ exception = ResponseError.new(
132
+ code: response['error_code'] || response['errorCode'],
133
+ message: response['error_message'] || ERRORS[response['errorCode']] || response['errorCode'],
134
+ response: response
135
+ )
136
+
137
+ raise exception
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,86 @@
1
+ module AliExpress
2
+ class Auth < Base
3
+ class << self
4
+ def authorize(state = nil)
5
+ oauth_client = client
6
+ oauth_params = params(state || SecureRandom.hex(24))
7
+ oauth_params[:_aop_signature] = sign(oauth_params)
8
+ logger.debug oauth_params
9
+ url = oauth_client.authorize_url(oauth_params)
10
+
11
+ if RUBY_PLATFORM == 'x86_64-darwin16'
12
+ begin
13
+ system('open', url)
14
+ rescue Exception => e # rubocop:disable Lint/RescueException
15
+ logger.debug "Call to open failed: #{e.message}"
16
+ end
17
+ end
18
+
19
+ $stdout.puts "Open #{url} in your browser, and then enter the code below."
20
+ $stdout.print 'authorization_code> '
21
+ authorization_code = $stdin.gets
22
+
23
+ token = oauth_client.get_token(
24
+ code: authorization_code,
25
+ grant_type: 'authorization_code',
26
+ need_refresh_token: 'true',
27
+ parse: :json,
28
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob'
29
+ ).to_hash
30
+
31
+ token
32
+ end
33
+
34
+ def refresh
35
+ if refresh_token
36
+ response = post(
37
+ api_call: 'getToken',
38
+ api_namespace: 'system.oauth2',
39
+ api_version: '1',
40
+ # protocol: 'http',
41
+ auth: false,
42
+ sign: false,
43
+ params: {
44
+ grant_type: 'refresh_token',
45
+ client_id: client_id,
46
+ client_secret: client_secret,
47
+ refresh_token: refresh_token
48
+ }
49
+ )
50
+ AliExpress.access_token = response['access_token']
51
+ else
52
+ raise Exception, 'You must have a refresh_token to refresh authorization. You need to call `AliExpress::Auth.authorize` and follow the instructions to generate one.'
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def client
59
+ OAuth2::Client.new(
60
+ client_id,
61
+ client_secret,
62
+ site: 'https://gw.api.alibaba.com',
63
+ authorize_url: '/auth/authorize.htm',
64
+ token_url: "/openapi/http/1/system.oauth2/getToken/#{client_id}"
65
+ )
66
+ end
67
+
68
+ def sign(hash)
69
+ OpenSSL::HMAC.hexdigest(
70
+ OpenSSL::Digest.new('sha1'),
71
+ client_secret,
72
+ hash.sort.flatten.join.to_s
73
+ ).upcase
74
+ end
75
+
76
+ def params(state)
77
+ {
78
+ client_id: client_id,
79
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
80
+ site: 'aliexpress',
81
+ state: state
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,87 @@
1
+ module AliExpress
2
+ class Base
3
+ class << self
4
+ def client_id
5
+ AliExpress.client_id
6
+ end
7
+
8
+ def client_secret
9
+ AliExpress.client_secret
10
+ end
11
+
12
+ def access_token
13
+ AliExpress.access_token
14
+ end
15
+
16
+ def refresh_token
17
+ AliExpress.refresh_token
18
+ end
19
+
20
+ def currency
21
+ AliExpress.currency
22
+ end
23
+
24
+ def language
25
+ AliExpress.language
26
+ end
27
+
28
+ def logger
29
+ AliExpress.logger
30
+ end
31
+
32
+ def get(params)
33
+ request(params.merge(method: :get))
34
+ end
35
+
36
+ def post(params)
37
+ request(params.merge(method: :post))
38
+ end
39
+
40
+ private
41
+
42
+ def sign_payload(path, payload)
43
+ signature_factor = path + payload.map { |key, value| "#{key}#{value}" }.sort.join
44
+ payload[:_aop_signature] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), client_secret, signature_factor).upcase
45
+ logger.debug "signature_factor=#{signature_factor}, signature=#{payload[:_aop_signature]}"
46
+ end
47
+
48
+ def request(method:, protocol: 'param2', api_version: 1, api_namespace: 'aliexpress.open', api_call:, auth: true, sign: true, params: {})
49
+ path = [protocol, api_version, api_namespace, api_call, client_id].join('/')
50
+
51
+ payload = params
52
+ payload[:access_token] = access_token if auth
53
+ sign_payload(path, payload) if sign
54
+ url = [AliExpress.base_uri, path].join('/')
55
+
56
+ resp = if method == :get
57
+ unless params.length.zero?
58
+ uri = Addressable::URI.new
59
+ uri.query_values = params
60
+ url += "?#{uri.query}"
61
+ end
62
+
63
+ logger.debug "GET #{url}"
64
+ RestClient.get(url)
65
+ else
66
+ logger.debug "POST #{url} | #{payload}"
67
+ RestClient.post(url, payload)
68
+ end
69
+
70
+ begin
71
+ JSON.parse(resp.body)
72
+ rescue JSON::ParserError
73
+ resp.body
74
+ end
75
+ rescue RestClient::Exception => exception
76
+ resp = begin
77
+ JSON.parse(exception.response)
78
+ rescue JSON::ParserError
79
+ exception.response
80
+ end
81
+
82
+ logger.debug "#{exception.class.name} (#{exception.response.code}): #{resp.inspect}"
83
+ resp
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,22 @@
1
+ module AliExpress
2
+ class Product < Base
3
+ class << self
4
+ def find(id)
5
+ post(api_call: 'api.findAeProductById', params: { productId: id })
6
+ end
7
+
8
+ private
9
+
10
+ def get(params)
11
+ super(default_params.merge(params))
12
+ end
13
+
14
+ def default_params
15
+ {
16
+ api_namespace: 'aliexpress.open',
17
+ api_version: 1
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module AliExpress
2
+ class ResponseError < StandardError
3
+ attr_reader :code, :message, :response
4
+
5
+ def initialize(code:, message:, response:)
6
+ @code = code
7
+ @message = message
8
+ @response = response
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/affiliate_spec.rb
4
+ describe AliExpress::Affiliate do
5
+ let(:response) do
6
+ mock = double
7
+ allow(mock).to receive(:code).and_return(200)
8
+ allow(mock).to receive(:body).and_return(json)
9
+ mock
10
+ end
11
+
12
+ let(:success) { 20010000 }
13
+ let(:hash) { { 'errorCode' => success } }
14
+ let(:json) { hash.to_json }
15
+
16
+ # before do
17
+ # AliExpress.logger = Logger.new(STDOUT)
18
+ # AliExpress.logger.level = Logger::DEBUG
19
+ # end
20
+
21
+ describe '.search' do
22
+ let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listPromotionProduct/fake-client-id?categoryId=3&fields=productId&keywords=drone&language=en&localCurrency=USD&pageNo=2&pageSize=5&sort=orignalPriceDown' }
23
+
24
+ it 'returns a successful response' do
25
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
26
+ response = AliExpress::Affiliate.search(query: 'drone', filters: { category: 'Apparel & Accessories' }, sort: 'orignalPriceDown', page: 2, per_page: 5, fields: %w(productId))
27
+ expect(response['errorCode']).to eq(success)
28
+ end
29
+ end
30
+
31
+ describe '.find' do
32
+ let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.getPromotionProductDetail/fake-client-id?fields=productId&language=en&localCurrency=USD&productId=12345' }
33
+
34
+ it 'returns a product' do
35
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
36
+ response = AliExpress::Affiliate.find(12345, fields: %w(productId))
37
+ expect(response['errorCode']).to eq(success)
38
+ end
39
+ end
40
+
41
+ describe '.similar' do
42
+ let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listSimilarProducts/fake-client-id?language=en&localCurrency=USD&productId=12345' }
43
+
44
+ it 'returns a product' do
45
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
46
+ response = AliExpress::Affiliate.similar(12345)
47
+ expect(response['errorCode']).to eq(success)
48
+ end
49
+ end
50
+
51
+ describe '.popular' do
52
+ let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listHotProducts/fake-client-id?categoryId=3&language=en&localCurrency=USD' }
53
+
54
+ context 'when a category id is used' do
55
+ it 'returns a successful response' do
56
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
57
+ response = AliExpress::Affiliate.popular(3)
58
+ expect(response['errorCode']).to eq(success)
59
+ end
60
+ end
61
+
62
+ context 'when a category name is used' do
63
+ it 'returns a successful response' do
64
+ expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
65
+ response = AliExpress::Affiliate.popular('Apparel & Accessories')
66
+ expect(response['errorCode']).to eq(success)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/aliexpress_spec.rb
4
+ describe AliExpress do
5
+ describe '.client_id' do
6
+ subject { AliExpress.client_id }
7
+ it { is_expected.to eq('fake-client-id') }
8
+
9
+ context('when assigned') do
10
+ before { AliExpress.client_id = 'foo' }
11
+ it { is_expected.to eq('foo') }
12
+ end
13
+ end
14
+
15
+ describe '.client_secret' do
16
+ subject { AliExpress.client_secret }
17
+ it { is_expected.to eq('fake-client-secret') }
18
+
19
+ context('when assigned') do
20
+ before { AliExpress.client_secret = 'foo' }
21
+ it { is_expected.to eq('foo') }
22
+ end
23
+ end
24
+
25
+ describe '.access_token' do
26
+ subject { AliExpress.access_token }
27
+ it { is_expected.to eq('fake-access-token') }
28
+
29
+ context('when assigned') do
30
+ before { AliExpress.access_token = 'foo' }
31
+ it { is_expected.to eq('foo') }
32
+ end
33
+ end
34
+
35
+ describe '.refresh_token' do
36
+ subject { AliExpress.refresh_token }
37
+ it { is_expected.to eq('fake-refresh-token') }
38
+
39
+ context('when assigned') do
40
+ before { AliExpress.refresh_token = 'foo' }
41
+ it { is_expected.to eq('foo') }
42
+ end
43
+ end
44
+
45
+ describe '.protocol' do
46
+ subject { AliExpress.protocol }
47
+ it { is_expected.to eq('https') }
48
+
49
+ context('when assigned') do
50
+ before { AliExpress.protocol = 'foo' }
51
+ it { is_expected.to eq('foo') }
52
+ end
53
+ end
54
+
55
+ describe '.host' do
56
+ subject { AliExpress.host }
57
+ it { is_expected.to eq('gw.api.alibaba.com') }
58
+
59
+ context('when assigned') do
60
+ before { AliExpress.host = 'foo' }
61
+ it { is_expected.to eq('foo') }
62
+ end
63
+ end
64
+
65
+ describe '.base_uri' do
66
+ subject { AliExpress.base_uri }
67
+ it { is_expected.to eq('https://gw.api.alibaba.com/openapi') }
68
+
69
+ context('when assigned') do
70
+ before { AliExpress.base_uri = 'foo' }
71
+ it { is_expected.to eq('foo') }
72
+ end
73
+ end
74
+
75
+ describe '.currency' do
76
+ subject { AliExpress.currency }
77
+ it { is_expected.to eq('USD') }
78
+
79
+ context('when assigned') do
80
+ before { AliExpress.currency = 'foo' }
81
+ it { is_expected.to eq('foo') }
82
+ end
83
+ end
84
+
85
+ describe '.language' do
86
+ subject { AliExpress.language }
87
+ it { is_expected.to eq('en') }
88
+
89
+ context('when assigned') do
90
+ before { AliExpress.language = 'foo' }
91
+ it { is_expected.to eq('foo') }
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/auth_spec.rb
4
+ describe AliExpress::Auth do
5
+ describe '.authorize' do
6
+ let(:authorization_code) { 'b5ce38e1-833c-4f15-bd29-65e85db9bd2e' }
7
+ let(:response) do
8
+ {
9
+ 'refresh_token_timeout' => '20180402143144000-0700',
10
+ 'aliId' => 'fake-ali-id',
11
+ 'resource_owner' => 'fake-resource-owner',
12
+ access_token: 'new-fake-access-token',
13
+ refresh_token: 'new-fake-refresh-token',
14
+ expires_at: 1507266354
15
+ }
16
+ end
17
+
18
+ it 'creates a new access token' do
19
+ stub_const('RUBY_PLATFORM', 'test')
20
+ allow($stdin).to receive(:gets).and_return(authorization_code)
21
+ allow($stdout).to receive(:puts).and_return(nil)
22
+ allow($stdout).to receive(:print).and_return(nil)
23
+ expect_any_instance_of(OAuth2::Client).to receive(:get_token).exactly(1).times.with(hash_including(code: authorization_code)).and_return(response)
24
+ expect(AliExpress::Auth.authorize).to eq(response)
25
+ end
26
+ end
27
+
28
+ describe '.refresh' do
29
+ let(:response) do
30
+ mock = double
31
+ allow(mock).to receive(:code).and_return(200)
32
+ allow(mock).to receive(:body).and_return('{"aliId":"fake-ali-id","resource_owner":"fake-resource-owner","expires_in":"36000","access_token":"jabber_wocky"}')
33
+ mock
34
+ end
35
+
36
+ it 'exchanges the refresh token for a new access token' do
37
+ expect(RestClient).to receive(:post).exactly(1).times.and_return(response)
38
+ expect(AliExpress::Auth.refresh).to eq('jabber_wocky')
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/base_spec.rb
4
+ describe AliExpress::Base do
5
+ let(:response) do
6
+ mock = double
7
+ allow(mock).to receive(:code).and_return(200)
8
+ allow(mock).to receive(:body).and_return('{}')
9
+ mock
10
+ end
11
+
12
+ describe '.get' do
13
+ context 'when auth is true' do
14
+ it 'uses the access token' do
15
+ expect(RestClient).to receive(:get).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id?access_token=fake-access-token').and_return(response)
16
+ AliExpress::Base.get(api_call: 'foo', sign: false, auth: true)
17
+ end
18
+ end
19
+
20
+ context 'when sign is true' do
21
+ it 'signs the request' do
22
+ expect(RestClient).to receive(:get).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id?_aop_signature=8F19FBA00C4BF35E43FA9ED143639337047624C0').and_return(response)
23
+ AliExpress::Base.get(api_call: 'foo', sign: true, auth: false)
24
+ end
25
+ end
26
+
27
+ context 'when bogus params are used' do
28
+ it 'raises an error' do
29
+ expect { AliExpress::Base.get(api_call: 'foo', foo: 'bar') }.to raise_error(ArgumentError)
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '.post' do
35
+ context 'when auth is true' do
36
+ it 'uses the access token' do
37
+ expect(RestClient).to receive(:post).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id', access_token: 'fake-access-token').and_return(response)
38
+ AliExpress::Base.post(api_call: 'foo', sign: false, auth: true)
39
+ end
40
+ end
41
+
42
+ context 'when sign is true' do
43
+ it 'signs the request' do
44
+ expect(RestClient).to receive(:post).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id', _aop_signature: '8F19FBA00C4BF35E43FA9ED143639337047624C0').and_return(response)
45
+ AliExpress::Base.post(api_call: 'foo', sign: true, auth: false)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ require 'aliexpress'
5
+ require 'awesome_print'
6
+
7
+ gemspec = Gem::Specification.find_by_name('aliexpress')
8
+ Dir["#{gemspec.gem_dir}/spec/support/**/*.rb"].each { |f| require f }
@@ -0,0 +1,12 @@
1
+ RSpec.configure do |config|
2
+ config.before(:each) do
3
+ AliExpress.client_id = 'fake-client-id'
4
+ AliExpress.client_secret = 'fake-client-secret'
5
+ AliExpress.access_token = 'fake-access-token'
6
+ AliExpress.refresh_token = 'fake-refresh-token'
7
+
8
+ %i(protocol host base_uri currency language logger).each do |method|
9
+ AliExpress.send("#{method}=", nil)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ require 'webmock/rspec'
2
+
3
+ WebMock.disable_net_connect!(allow_localhost: true)
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aliexpress
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.4'
5
+ platform: ruby
6
+ authors:
7
+ - Chris Estreich
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: addressable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: oauth2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: AliExpress SDK for ruby.
56
+ email:
57
+ - chris@tophatter.com
58
+ executables:
59
+ - console
60
+ extensions: []
61
+ extra_rdoc_files:
62
+ - README.md
63
+ files:
64
+ - ".coveralls.yml"
65
+ - ".gitignore"
66
+ - ".rspec"
67
+ - ".rubocop.yml"
68
+ - ".travis.yml"
69
+ - Gemfile
70
+ - LICENSE.md
71
+ - README.md
72
+ - Rakefile
73
+ - aliexpress.gemspec
74
+ - bin/console
75
+ - lib/aliexpress.rb
76
+ - lib/aliexpress/affiliate.rb
77
+ - lib/aliexpress/auth.rb
78
+ - lib/aliexpress/base.rb
79
+ - lib/aliexpress/product.rb
80
+ - lib/aliexpress/response_error.rb
81
+ - spec/affiliate_spec.rb
82
+ - spec/aliexpress_spec.rb
83
+ - spec/auth_spec.rb
84
+ - spec/base_spec.rb
85
+ - spec/spec_helper.rb
86
+ - spec/support/auth.rb
87
+ - spec/support/webmock.rb
88
+ homepage: https://github.com/tophatter/aliexpress-api-ruby
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message: 'Documentation is available at: http://isvus.aliexpress.com/isv/index.htm'
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '2.0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.6.12
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: AliExpress SDK for ruby.
112
+ test_files:
113
+ - spec/affiliate_spec.rb
114
+ - spec/aliexpress_spec.rb
115
+ - spec/auth_spec.rb
116
+ - spec/base_spec.rb
117
+ - spec/spec_helper.rb
118
+ - spec/support/auth.rb
119
+ - spec/support/webmock.rb