flickr 1.0.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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