flickr 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/flickr.rb ADDED
@@ -0,0 +1,281 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'flickr/version'
4
+ require 'flickr/util'
5
+ require 'flickr/errors'
6
+ require 'flickr/oauth_client'
7
+ require 'flickr/request'
8
+ require 'flickr/response'
9
+ require 'flickr/response_list'
10
+
11
+ class Flickr
12
+
13
+ USER_AGENT = "Flickr/#{VERSION} (+https://github.com/hanklords/flickraw)".freeze
14
+ END_POINT = 'https://api.flickr.com/services'.freeze
15
+ FLICKR_OAUTH_REQUEST_TOKEN = (END_POINT + '/oauth/request_token').freeze
16
+ FLICKR_OAUTH_AUTHORIZE = (END_POINT + '/oauth/authorize').freeze
17
+ FLICKR_OAUTH_ACCESS_TOKEN = (END_POINT + '/oauth/access_token').freeze
18
+ REST_PATH = (END_POINT + '/rest/').freeze
19
+ UPLOAD_PATH = (END_POINT + '/upload/').freeze
20
+ REPLACE_PATH = (END_POINT + '/replace/').freeze
21
+ PHOTO_SOURCE_URL = 'https://farm%s.staticflickr.com/%s/%s_%s%s.%s'.freeze
22
+ URL_PROFILE = 'https://www.flickr.com/people/'.freeze
23
+ URL_PHOTOSTREAM = 'https://www.flickr.com/photos/'.freeze
24
+ URL_SHORT = 'https://flic.kr/p/'.freeze
25
+
26
+ # Authenticated access token
27
+ attr_accessor :access_token
28
+
29
+ # Authenticated access token secret
30
+ attr_accessor :access_secret
31
+
32
+ attr_reader :client
33
+
34
+ @@initialized = false
35
+ @@mutex = Mutex.new
36
+
37
+ def initialize(api_key = ENV['FLICKR_API_KEY'], shared_secret = ENV['FLICKR_SHARED_SECRET'])
38
+
39
+ raise FlickrAppNotConfigured.new("No API key defined!") if api_key.nil?
40
+ raise FlickrAppNotConfigured.new("No shared secret defined!") if shared_secret.nil?
41
+
42
+ @access_token = @access_secret = nil
43
+ @oauth_consumer = oauth_consumer api_key, shared_secret
44
+
45
+ @@mutex.synchronize do
46
+ unless @@initialized
47
+ build_classes retrieve_endpoints
48
+ @@initialized = true
49
+ end
50
+ end
51
+ @client = self # used for propagating the client to sub-classes
52
+ end
53
+
54
+ # This is the central method. It does the actual request to the Flickr server.
55
+ #
56
+ # Raises FailedResponse if the response status is _failed_.
57
+ def call(req, args={}, &block)
58
+ oauth_args = args.delete(:oauth) || {}
59
+ http_response = @oauth_consumer.post_form(REST_PATH, @access_secret, {:oauth_token => @access_token}.merge(oauth_args), build_args(args, req))
60
+ process_response(req, http_response.body)
61
+ end
62
+
63
+ # Get an oauth request token.
64
+ #
65
+ # token = flickr.get_request_token(:oauth_callback => "https://example.com")
66
+ def get_request_token(args = {})
67
+ @oauth_consumer.request_token(FLICKR_OAUTH_REQUEST_TOKEN, args)
68
+ end
69
+
70
+ # Get the oauth authorize url.
71
+ #
72
+ # auth_url = flickr.get_authorize_url(token['oauth_token'], :perms => 'delete')
73
+ def get_authorize_url(token, args = {})
74
+ @oauth_consumer.authorize_url(FLICKR_OAUTH_AUTHORIZE, args.merge(:oauth_token => token))
75
+ end
76
+
77
+ # Get an oauth access token.
78
+ #
79
+ # flickr.get_access_token(token['oauth_token'], token['oauth_token_secret'], oauth_verifier)
80
+ def get_access_token(token, secret, verify)
81
+ access_token = @oauth_consumer.access_token(FLICKR_OAUTH_ACCESS_TOKEN, secret, :oauth_token => token, :oauth_verifier => verify)
82
+ @access_token, @access_secret = access_token['oauth_token'], access_token['oauth_token_secret']
83
+ access_token
84
+ end
85
+
86
+ # Use this to upload the photo in _file_.
87
+ #
88
+ # flickr.upload_photo '/path/to/the/photo', :title => 'Title', :description => 'This is the description'
89
+ #
90
+ # See https://www.flickr.com/services/api/upload.api.html for more information on the arguments.
91
+ def upload_photo(file, args={})
92
+ upload_flickr(UPLOAD_PATH, file, args)
93
+ end
94
+
95
+ # Use this to replace the photo with :photo_id with the photo in _file_.
96
+ #
97
+ # flickr.replace_photo '/path/to/the/photo', :photo_id => id
98
+ #
99
+ # See https://www.flickr.com/services/api/replace.api.html for more information on the arguments.
100
+ def replace_photo(file, args={})
101
+ upload_flickr(REPLACE_PATH, file, args)
102
+ end
103
+
104
+ private
105
+
106
+ def retrieve_endpoints
107
+ if Flickr.cache and File.exist?(Flickr.cache)
108
+ YAML.load_file Flickr.cache
109
+ else
110
+ endpoints = call('flickr.reflection.getMethods').to_a
111
+ File.open(Flickr.cache, 'w') do |file|
112
+ file.write(YAML.dump endpoints)
113
+ end if Flickr.cache
114
+ endpoints
115
+ end
116
+ end
117
+
118
+ def oauth_consumer(api_key, shared_secret)
119
+ client = OAuthClient.new api_key, shared_secret
120
+ client.proxy = Flickr.proxy
121
+ client.check_certificate = Flickr.check_certificate
122
+ client.ca_file = Flickr.ca_file
123
+ client.ca_path = Flickr.ca_path
124
+ client.user_agent = USER_AGENT
125
+ client
126
+ end
127
+
128
+ def build_classes(endpoints)
129
+
130
+ endpoints.sort.each do |endpoint|
131
+
132
+ *breadcrumbs, tail = endpoint.split '.'
133
+
134
+ raise "Invalid namespace" unless 'flickr' == breadcrumbs.shift
135
+
136
+ base_class = breadcrumbs.reduce(::Flickr) do |memo, klass|
137
+
138
+ cklass = klass.capitalize
139
+
140
+ if memo.const_defined? cklass, false
141
+ memo.const_get cklass
142
+ else
143
+ new_class = Class.new { include ::Flickr::Request }
144
+ memo.const_set cklass, new_class
145
+ memo.send(:define_method, klass) do
146
+ new_class.new @client
147
+ end
148
+ new_class
149
+ end
150
+ end
151
+
152
+ base_class.send(:define_method, tail) do |*args, &block|
153
+ @client.call(endpoint, *args, &block)
154
+ end unless base_class.method_defined? tail
155
+
156
+ end
157
+
158
+ end
159
+
160
+ def build_args(args={}, method_name=nil)
161
+ args['method'] = method_name if method_name
162
+ args.merge('format' => 'json', 'nojsoncallback' => '1')
163
+ end
164
+
165
+ def process_response(req, response)
166
+ puts response.inspect if ENV['FLICKR_DEBUG']
167
+
168
+ if /\A<\?xml / === response # upload_photo returns xml data whatever we ask
169
+ if response[/stat="(\w+)"/, 1] == 'fail'
170
+ msg = response[/msg="([^"]+)"/, 1]
171
+ code = response[/code="([^"]+)"/, 1]
172
+ raise FailedResponse.new(msg, code, req)
173
+ end
174
+
175
+ type = response[/<(\w+)/, 1]
176
+ h = {
177
+ 'secret' => response[/secret="([^"]+)"/, 1],
178
+ 'originalsecret' => response[/originalsecret="([^"]+)"/, 1],
179
+ '_content' => response[/>([^<]+)<\//, 1]
180
+ }.delete_if { |_, v| v.nil? }
181
+
182
+ Response.build h, type
183
+ else
184
+ json = JSON.load(response.empty? ? '{}' : response)
185
+ raise FailedResponse.new(json['message'], json['code'], req) if json.delete('stat') == 'fail'
186
+ type, json = json.to_a.first if json.size == 1 and json.values.all? { |x| Hash === x }
187
+
188
+ Response.build json, type
189
+ end
190
+ end
191
+
192
+ def upload_flickr(method, file, args={})
193
+ oauth_args = args.delete(:oauth) || {}
194
+ args = build_args(args)
195
+ if file.respond_to? :read
196
+ args['photo'] = file
197
+ else
198
+ args['photo'] = open(file, 'rb')
199
+ close_after = true
200
+ end
201
+
202
+ http_response = @oauth_consumer.post_multipart(method, @access_secret, {:oauth_token => @access_token}.merge(oauth_args), args)
203
+ args['photo'].close if close_after
204
+ process_response(method, http_response.body)
205
+ end
206
+
207
+ class << self
208
+ # Your flickr API key, see https://www.flickr.com/services/api/keys for more information
209
+ attr_accessor :api_key
210
+
211
+ # The shared secret of _api_key_, see https://www.flickr.com/services/api/keys for more information
212
+ attr_accessor :shared_secret
213
+
214
+ # Use a proxy
215
+ attr_accessor :proxy
216
+
217
+ # Use ssl connection
218
+ attr_accessor :secure
219
+
220
+ # Check the server certificate (ssl connection only)
221
+ attr_accessor :check_certificate
222
+
223
+ # Set path of a CA certificate file in PEM format (ssl connection only)
224
+ attr_accessor :ca_file
225
+
226
+ # Set path to a directory of CA certificate files in PEM format (ssl connection only)
227
+ attr_accessor :ca_path
228
+
229
+ # Set path to a file that can be used to store endpoints
230
+ attr_accessor :cache
231
+
232
+ BASE58_ALPHABET = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'.freeze
233
+
234
+ def base58(id)
235
+ id = id.to_i
236
+ alphabet = BASE58_ALPHABET.split(//)
237
+ base = alphabet.length
238
+ begin
239
+ id, m = id.divmod(base)
240
+ r = alphabet[m] + (r || '')
241
+ end while id > 0
242
+ r
243
+ end
244
+
245
+ def url(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '', 'jpg'] end
246
+ def url_m(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_m', 'jpg'] end
247
+ def url_s(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_s', 'jpg'] end
248
+ def url_t(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_t', 'jpg'] end
249
+ def url_b(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_b', 'jpg'] end
250
+ def url_z(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_z', 'jpg'] end
251
+ def url_q(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_q', 'jpg'] end
252
+ def url_n(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_n', 'jpg'] end
253
+ def url_c(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_c', 'jpg'] end
254
+ def url_h(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_h', 'jpg'] end
255
+ def url_k(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.secret, '_k', 'jpg'] end
256
+ def url_o(r); PHOTO_SOURCE_URL % [r.farm, r.server, r.id, r.originalsecret, '_o', r.originalformat] end
257
+ def url_profile(r); URL_PROFILE + (r.owner.respond_to?(:nsid) ? r.owner.nsid : r.owner) + '/' end
258
+ def url_photopage(r); url_photostream(r) + r.id end
259
+ def url_photosets(r); url_photostream(r) + 'sets/' end
260
+ def url_photoset(r); url_photosets(r) + r.id end
261
+ def url_short(r); URL_SHORT + base58(r.id) end
262
+ def url_short_m(r); URL_SHORT + 'img/' + base58(r.id) + '_m.jpg' end
263
+ def url_short_s(r); URL_SHORT + 'img/' + base58(r.id) + '.jpg' end
264
+ def url_short_t(r); URL_SHORT + 'img/' + base58(r.id) + '_t.jpg' end
265
+ def url_short_q(r); URL_SHORT + 'img/' + base58(r.id) + '_q.jpg' end
266
+ def url_short_n(r); URL_SHORT + 'img/' + base58(r.id) + '_n.jpg' end
267
+ def url_photostream(r)
268
+ URL_PHOTOSTREAM +
269
+ if r.respond_to?(:pathalias) && r.pathalias
270
+ r.pathalias
271
+ elsif r.owner.respond_to?(:nsid)
272
+ r.owner.nsid
273
+ else
274
+ r.owner
275
+ end + '/'
276
+ end
277
+ end
278
+
279
+ self.check_certificate = true
280
+
281
+ end
@@ -0,0 +1,15 @@
1
+ class Flickr
2
+ class Error < StandardError; end
3
+
4
+ class FlickrAppNotConfigured < Error; end
5
+
6
+ class FailedResponse < Error
7
+ attr_reader :code
8
+ alias :msg :message
9
+ def initialize(msg, code, req)
10
+ @code = code
11
+ super("'#{req}' - #{msg}")
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,175 @@
1
+ require 'openssl'
2
+ require 'net/https'
3
+
4
+ class Flickr
5
+ class OAuthClient
6
+
7
+ class UnknownSignatureMethod < Error; end
8
+
9
+ class FailedResponse < Error
10
+ def initialize(str)
11
+ @response = OAuthClient.parse_response(str)
12
+ super(@response['oauth_problem'])
13
+ end
14
+ end
15
+
16
+ class << self
17
+ def encode_value(v)
18
+ v = v.to_s.encode('utf-8').force_encoding('ascii-8bit') if RUBY_VERSION >= '1.9'
19
+ v.to_s
20
+ end
21
+
22
+ def escape(s)
23
+ encode_value(s).gsub(/[^a-zA-Z0-9\-\.\_\~]/) do |special|
24
+ special.unpack("C*").map { |i| sprintf("%%%02X", i) }.join
25
+ end
26
+ end
27
+
28
+ def parse_response(text); Hash[text.split('&').map { |s| s.split('=') }] end
29
+
30
+ def signature_base_string(method, url, params)
31
+ params_norm = params.map { |k, v| "#{escape(k)}=#{escape(v)}" }.sort.join('&')
32
+ "#{method.to_s.upcase}&#{escape(url)}&#{escape(params_norm)}"
33
+ end
34
+
35
+ def sign_plaintext(method, url, params, token_secret, consumer_secret)
36
+ "#{escape(consumer_secret)}&#{escape(token_secret)}"
37
+ end
38
+
39
+ def sign_rsa_sha1(method, url, params, token_secret, consumer_secret)
40
+ text = signature_base_string(method, url, params)
41
+ key = OpenSSL::PKey::RSA.new(consumer_secret)
42
+ digest = OpenSSL::Digest::SHA1.new
43
+ [key.sign(digest, text)].pack('m0').gsub(/\n$/,'')
44
+ end
45
+
46
+ def sign_hmac_sha1(method, url, params, token_secret, consumer_secret)
47
+ text = signature_base_string(method, url, params)
48
+ key = "#{escape(consumer_secret)}&#{escape(token_secret)}"
49
+ digest = OpenSSL::Digest::SHA1.new
50
+ [OpenSSL::HMAC.digest(digest, key, text)].pack('m0').gsub(/\n$/,'')
51
+ end
52
+
53
+ def gen_timestamp; Time.now.to_i end
54
+
55
+ def gen_nonce; [OpenSSL::Random.random_bytes(32)].pack('m0').gsub(/\n$/,'') end
56
+
57
+ def gen_default_params
58
+ {
59
+ :oauth_version => "1.0",
60
+ :oauth_signature_method => 'HMAC-SHA1',
61
+ :oauth_nonce => gen_nonce,
62
+ :oauth_timestamp => gen_timestamp,
63
+ }
64
+ end
65
+
66
+ def authorization_header(url, params)
67
+ params_norm = params.map { |k, v| %(#{escape(k)}="#{escape(v)}") }.sort.join(', ')
68
+ %(OAuth realm="#{url.to_s}", #{params_norm})
69
+ end
70
+ end
71
+
72
+ attr_accessor :user_agent
73
+ attr_reader :proxy
74
+ attr_accessor :check_certificate
75
+ attr_accessor :ca_file
76
+ attr_accessor :ca_path
77
+ def proxy=(url); @proxy = URI.parse(url || '') end
78
+
79
+ def initialize(consumer_key, consumer_secret)
80
+ @consumer_key, @consumer_secret = consumer_key, consumer_secret
81
+ self.proxy = nil
82
+ end
83
+
84
+ def request_token(url, oauth_params = {})
85
+ r = post_form(url, nil, {:oauth_callback => 'oob'}.merge(oauth_params))
86
+ OAuthClient.parse_response(r.body)
87
+ end
88
+
89
+ def authorize_url(url, oauth_params = {})
90
+ params_norm = oauth_params.map { |k, v| "#{OAuthClient.escape(k)}=#{OAuthClient.escape(v)}" }.sort.join('&')
91
+ url = URI.parse(url)
92
+ url.query = url.query ? "#{url.query}&#{params_norm}" : params_norm
93
+ url.to_s
94
+ end
95
+
96
+ def access_token(url, token_secret, oauth_params = {})
97
+ r = post_form(url, token_secret, oauth_params)
98
+ OAuthClient.parse_response(r.body)
99
+ end
100
+
101
+ def post_form(url, token_secret, oauth_params = {}, params = {})
102
+ encoded_params = Hash[*params.map { |k, v| [OAuthClient.encode_value(k), OAuthClient.encode_value(v)]}.flatten]
103
+ post(url, token_secret, oauth_params, params) { |request| request.form_data = encoded_params }
104
+ end
105
+
106
+ def post_multipart(url, token_secret, oauth_params = {}, params = {})
107
+ post(url, token_secret, oauth_params, params) do |request|
108
+ boundary = "Flickr#{OAuthClient.gen_nonce}"
109
+ request['Content-type'] = "multipart/form-data, boundary=#{boundary}"
110
+
111
+ request.body = ''
112
+ params.each do |k, v|
113
+ if v.respond_to? :read
114
+ basename = File.basename(v.path.to_s) if v.respond_to? :path
115
+ basename ||= File.basename(v.base_uri.to_s) if v.respond_to? :base_uri
116
+ basename ||= "unknown"
117
+ request.body << "--#{boundary}\r\n" <<
118
+ "Content-Disposition: form-data; name=\"#{OAuthClient.encode_value(k)}\"; filename=\"#{OAuthClient.encode_value(basename)}\"\r\n" <<
119
+ "Content-Transfer-Encoding: binary\r\n" <<
120
+ "Content-Type: image/jpeg\r\n\r\n" <<
121
+ v.read << "\r\n"
122
+ else
123
+ request.body << "--#{boundary}\r\n" <<
124
+ "Content-Disposition: form-data; name=\"#{OAuthClient.encode_value(k)}\"\r\n\r\n" <<
125
+ "#{OAuthClient.encode_value(v)}\r\n"
126
+ end
127
+ end
128
+
129
+ request.body << "--#{boundary}--"
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def sign(method, url, params, token_secret = nil)
136
+ case params[:oauth_signature_method]
137
+ when 'HMAC-SHA1'
138
+ OAuthClient.sign_hmac_sha1(method, url, params, token_secret, @consumer_secret)
139
+ when 'RSA-SHA1'
140
+ OAuthClient.sign_rsa_sha1(method, url, params, token_secret, @consumer_secret)
141
+ when 'PLAINTEXT'
142
+ OAuthClient.sign_plaintext(method, url, params, token_secret, @consumer_secret)
143
+ else
144
+ raise UnknownSignatureMethod, params[:oauth_signature_method]
145
+ end
146
+ end
147
+
148
+ def post(url, token_secret, oauth_params, params)
149
+ url = URI.parse(url)
150
+ default_oauth_params = OAuthClient.gen_default_params
151
+ default_oauth_params[:oauth_consumer_key] = @consumer_key
152
+ default_oauth_params[:oauth_signature_method] = 'PLAINTEXT' if url.scheme == 'https'
153
+ oauth_params = default_oauth_params.merge(oauth_params)
154
+ params_signed = params.reject { |_, v| v.respond_to? :read }.merge(oauth_params)
155
+ oauth_params[:oauth_signature] = sign(:post, url, params_signed, token_secret)
156
+
157
+ http = Net::HTTP.new(url.host, url.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
158
+ http.use_ssl = (url.scheme == 'https')
159
+ http.verify_mode = (@check_certificate ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
160
+ http.ca_file = @ca_file
161
+ http.ca_path = @ca_path
162
+ r = http.start do |agent|
163
+ request = Net::HTTP::Post.new(url.path)
164
+ request['User-Agent'] = @user_agent if @user_agent
165
+ request['Authorization'] = OAuthClient.authorization_header(url, oauth_params)
166
+
167
+ yield request
168
+ agent.request(request)
169
+ end
170
+
171
+ raise FailedResponse.new(r.body) if r.is_a? Net::HTTPClientError
172
+ r
173
+ end
174
+ end
175
+ end