UNPKG

12.7 kBJavaScriptView Raw
1"use strict";
2
3/**
4 * Copyright 2013 Vimeo
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18var qs_module = require('querystring');
19var url_module = require('url');
20var crypto_module = require('crypto');
21var http_module = require('http');
22var https_module = require('https');
23var FileStreamer = require('./filestreamer');
24
25module.exports.request_defaults = {
26 protocol : 'https:',
27 hostname : 'api.vimeo.com',
28 port : 443,
29 method : 'GET',
30 query : {},
31 headers : {
32 Accept: "application/vnd.vimeo.*+json;version=3.2",
33 'User-Agent': 'Vimeo.js/1.2.1'
34 }
35};
36
37var auth_endpoints = module.exports.auth_endpoints = {
38 authorization : '/oauth/authorize',
39 accessToken : '/oauth/access_token',
40 clientCredentials : '/oauth/authorize/client'
41};
42
43/**
44 * This object is used to interface with the vimeo api
45 *
46 * @param {string} client_id OAuth 2 Client Identifier
47 * @param {string} client_secret OAuth 2 Client Secret
48 * @param (string) access_token OAuth 2 Optional pre-authorized access token
49 */
50var Vimeo = module.exports.Vimeo = function Vimeo (client_id, client_secret, access_token) {
51 this._client_id = client_id;
52 this._client_secret = client_secret;
53
54 if (access_token) {
55 this.access_token = access_token;
56 }
57};
58
59Vimeo.prototype._client_id = null;
60Vimeo.prototype._client_secret = null;
61Vimeo.prototype.access_token = null;
62
63/**
64 * Performs an api call.
65 * Can be called one of two ways.
66 * 1. Url + Callback
67 * If a url is provided, we fill in the rest of the request options with defaults (GET http://api.vimeo.com/{url}).
68 *
69 * 2. Options + callback
70 * If an object is provided, it should match the response of url_module.parse. Path is the only required parameter.
71 *
72 * hostname
73 * port
74 * query (will be applied to the url if GET, request body if POST)
75 * headers
76 * path (can include a querystring)
77 * method
78 *
79 * The callback takes two parameters, err and json.
80 * If an error has occured, your callback will be called as callback(err);
81 * If an error has not occured, your callback will be called as callback(null, json);
82 *
83 * @param {String|Object} options string path (default GET), or object with path, host, port, query, headers
84 * @param {Function} callback called when complete, function (err, json)
85 */
86Vimeo.prototype.request = function vimeo_request (options, callback) {
87 var client = null;
88
89 // if a url was provided, build an options object
90 if (typeof options === "string") {
91 options = url_module.parse(options, true);
92 options.method = "GET";
93 }
94
95 // if we don't have a path at this point, error. a path is the only required field. we have defaults for everything else important
96 if (typeof options.path !== "string") {
97 return callback(new Error('You must provide an api path'));
98 }
99
100 // Add leading slash to path if missing
101 if (options.path.charAt(0) !== '/') {
102 options.path = '/' + options.path;
103 }
104
105 // Turn the provided options into options that are valid for client.request
106 var request_options = this._buildRequestOptions(options);
107
108 // Locate the proper client from the request protocol
109 client = request_options.protocol === 'https:' ? https_module : http_module;
110
111 // Write the request body
112 if (['POST','PATCH','PUT','DELETE'].indexOf(request_options.method) !== -1) {
113 if (request_options.headers['Content-Type'] === 'application/json') {
114 request_options.body = JSON.stringify(options.query);
115 } else {
116 request_options.body = qs_module.stringify(options.query);
117 }
118
119 if (request_options.body) {
120 request_options.headers['Content-Length'] = Buffer.byteLength(request_options.body, 'utf8');
121 } else {
122 request_options.headers['Content-Length'] = 0;
123 }
124 }
125
126 // Perform the vimeo api request
127 var req = client.request(request_options, this._handleRequest(callback));
128
129 // Write the request body
130 if (request_options.body) {
131 req.write(request_options.body);
132 }
133
134 // notify user of any weird connection/request errors
135 req.on('error', function(e) {
136 callback(e);
137 });
138
139 // send the request
140 req.end();
141};
142
143/**
144 * Creates the standard request handler for http requests
145 *
146 * @param {Function} callback
147 * @return {Function}
148 */
149Vimeo.prototype._handleRequest = function (callback) {
150 return function (res) {
151 res.setEncoding('utf8');
152
153 var buffer = '';
154
155 res.on('readable', function () {
156 buffer += res.read() || '';
157 });
158
159 if (res.statusCode >= 400) {
160 // failed api calls should wait for the response to end and then call the callback with an error.
161 res.on('end', function () {
162 var err = new Error(buffer);
163 callback(err, buffer, res.statusCode, res.headers);
164 });
165 } else {
166 // successful api calls should wait for the response to end and then call the callback with the response body
167 res.on('end', function () {
168 try {
169 var body = buffer.length ? JSON.parse(buffer) : {};
170 } catch (e) {
171 return callback(buffer, buffer, res.statusCode, res.headers);
172 }
173 callback(null, body, res.statusCode, res.headers);
174 });
175 }
176 };
177};
178
179/**
180 * Merge the request options defaults into the request options
181 *
182 * @param {Object} options
183 * @return {Object}
184 */
185Vimeo.prototype._buildRequestOptions = function (options) {
186 // set up the request object. we always use the options paramter first, and if no value is provided we fall back to request defaults
187 var request_options = this._applyDefaultRequestOptions(options);
188
189 // Apply the access tokens
190 if (this.access_token) {
191 request_options.headers.Authorization = 'Bearer ' + this.access_token;
192 } else if (this._client_id && this._client_secret) {
193 request_options.headers.Authorization = 'Basic ' + new Buffer(this._client_id + ':' + this._client_secret).toString('base64');
194 }
195
196 // Set proper headers for POST, PATCH and PUT bodies
197 if (['POST','PATCH','PUT','DELETE'].indexOf(request_options.method) !== -1 && !request_options.headers['Content-Type']) {
198 request_options.headers['Content-Type'] = 'application/json';
199
200 // Apply parameters to the url for GET requests
201 } else if (request_options.method === 'GET') {
202 request_options.path = this._applyQuerystringParams(request_options, options);
203 }
204
205 return request_options;
206}
207
208/**
209 * Create an object of request options based on the provided list of options, and the request defaults.
210 *
211 * @param {Object} options
212 * @return {Object}
213 */
214Vimeo.prototype._applyDefaultRequestOptions = function (options) {
215 var request_options = {
216 protocol : options.protocol || module.exports.request_defaults.protocol,
217 host : options.hostname || module.exports.request_defaults.hostname,
218 port : options.port || module.exports.request_defaults.port,
219 method : options.method || module.exports.request_defaults.method,
220 headers : options.headers || {},
221 body : '',
222 path : options.path
223 };
224 var key = null;
225
226 // Apply the default headers
227 if (module.exports.request_defaults.headers) {
228 for (key in module.exports.request_defaults.headers) {
229 if (!request_options.headers[key]) {
230 request_options.headers[key] = module.exports.request_defaults.headers[key];
231 }
232 }
233 }
234
235 return request_options;
236}
237
238/**
239 * Apply the query parameter onto the final request url
240 *
241 * @param {Object} request_options
242 * @param {Object} options
243 * @return {String}
244 */
245Vimeo.prototype._applyQuerystringParams = function (request_options, options) {
246 var querystring = '';
247
248 if (!options.query) {
249 return request_options.path;
250 }
251
252 // If we have parameters, apply them to the url
253 if (Object.keys(options.query).length) {
254 if (request_options.path.indexOf('?') < 0) {
255 // If the existing path does not contain any parameters, apply them as the only options
256 querystring = '?' + qs_module.stringify(options.query);
257 } else {
258 // If the user already added parameters to the url, we want to add them as additional parameters
259 querystring = '&' + qs_module.stringify(options.query);
260 }
261 }
262
263 return request_options.path + querystring;
264}
265
266
267/**
268 * Exchange a code for an access token. This code should exist on your redirect_uri
269 *
270 * @param {String} code the code provided on your redirect_uri
271 * @param {String} redirect_uri the exact redirect_uri provided to buildAuthorizationEndpoint and configured in your api app settings
272 * @return {null}
273 */
274Vimeo.prototype.accessToken = function (code, redirect_uri, fn) {
275 var _self = this;
276
277 this.request({
278 method : 'POST',
279 hostname : module.exports.request_defaults.hostname,
280 path : auth_endpoints.accessToken,
281 query : {
282 grant_type : 'authorization_code',
283 code : code,
284 redirect_uri : redirect_uri
285 },
286 headers : {
287 'Content-Type' : 'application/x-www-form-urlencoded'
288 }
289 }, function (err, body, status, headers) {
290 if (err) {
291 return fn(err, null, status, headers);
292 } else {
293 fn(null, body, status, headers);
294 }
295 });
296};
297
298
299/**
300 * The first step of the authorization process.
301 *
302 * This function returns a url, which the user should be sent to (via redirect or link).
303 * The destination allows the user to accept or deny connecting with vimeo, and accept or deny each of the scopes you requested.
304 * Scopes are passed through the second parameter as an array of strings, or a space delimited list.
305 *
306 * Once accepted or denied, the user is redirected back to the redirect_uri.
307 * If accepted, the redirect url will
308 *
309 * @param {String} redirect_uri The uri that will exchange a code for an access token. Must match the uri in your app settings.
310 * @param {String} scope An array of scopes. see https://developer.vimeo.com/api/authentication#scopes for more
311 * @param {String} state A random state that will be returned to you on your redirect uri.
312 */
313Vimeo.prototype.buildAuthorizationEndpoint = function (redirect_uri, scope, state) {
314 var query = {
315 response_type : 'code',
316 client_id : this._client_id,
317 redirect_uri : redirect_uri
318 };
319
320 if (scope) {
321 if (Array.isArray(scope)) {
322 query.scope = scope.join(' ');
323 } else {
324 query.scope = scope;
325 }
326 } else {
327 query.scope = 'public';
328 }
329
330 if (state) {
331 query.state = state;
332 }
333
334 return module.exports.request_defaults.protocol + '//' + module.exports.request_defaults.hostname + auth_endpoints.authorization + '?' + qs_module.stringify(query);
335};
336
337/**
338 * Generates an unauthenticated access token. This is necessary to make unauthenticated requests
339 *
340 * @param {string} scope An array of scopes. see https://developer.vimeo.com/api/authentication#scopes for more
341 * @param {Function} fn A function that is called when the request is complete. If an error occured the first parameter will be that error, otherwise the first parameter will be null.
342 */
343Vimeo.prototype.generateClientCredentials = function (scope, fn) {
344 var query = {
345 grant_type : 'client_credentials',
346 }
347
348 if (scope) {
349 if (Array.isArray(scope)) {
350 query.scope = scope.join(' ');
351 } else {
352 query.scope = scope;
353 }
354 } else {
355 query.scope = 'public';
356 }
357
358 this.request({
359 method : 'POST',
360 hostname : module.exports.request_defaults.hostname,
361 path : auth_endpoints.clientCredentials,
362 query : query,
363 headers : {
364 'Content-Type' : 'application/x-www-form-urlencoded'
365 }
366 }, function (err, body, status, headers) {
367 if (err) {
368 return fn(err, null, status, headers);
369 } else {
370 fn(null, body, status, headers);
371 }
372 });
373}
374
375/**
376 * Initiate streaming uploads
377 *
378 * @param {string} path The path to the file you wish to upload
379 * @param {Function} callback A function that is called when the upload is complete, or fails.
380 */
381Vimeo.prototype.streamingUpload = function (path, video_uri, callback, progress_callback) {
382 var _self = this;
383
384 if (typeof video_uri === 'function') {
385 progress_callback = callback;
386 callback = video_uri;
387 video_uri = undefined;
388 }
389
390 var options = {
391 method : video_uri ? 'PUT' : 'POST',
392 path : video_uri ? video_uri + '/files' : '/me/videos',
393 query : {
394 type : 'streaming'
395 }
396 };
397
398 this.request(options, function (err, ticket, status, headers) {
399 if (err) {
400 return callback(err);
401 }
402
403 var file = new FileStreamer(path, ticket.upload_link_secure, progress_callback);
404
405 file.ready(function () {
406 _self.request({
407 method : 'DELETE',
408 path : ticket.complete_uri
409 }, callback);
410 });
411
412 file.error(callback);
413 file.upload();
414 });
415};