1 |
|
2 |
|
3 |
|
4 | 'use strict';
|
5 |
|
6 |
|
7 | const VERSION = require( '../package' ).version;
|
8 |
|
9 |
|
10 | const async = require( 'async' );
|
11 |
|
12 |
|
13 | const request = require( 'request' );
|
14 |
|
15 | function doRequest( params, callback, method, done ) {
|
16 |
|
17 | const actionName = params.action;
|
18 |
|
19 | const options = {
|
20 | method: method || 'GET',
|
21 | proxy: this.proxy || false,
|
22 | jar: this.jar,
|
23 | headers: {
|
24 | 'User-Agent': this.userAgent
|
25 | }
|
26 | };
|
27 |
|
28 |
|
29 | params = params || {};
|
30 |
|
31 |
|
32 | params.format = 'json';
|
33 |
|
34 |
|
35 | if ( method === 'UPLOAD' ) {
|
36 | options.method = 'POST';
|
37 |
|
38 | const CRLF = '\r\n',
|
39 | postBody = [],
|
40 | boundary = `nodemw${Math.random().toString().slice( 2 )}`;
|
41 |
|
42 |
|
43 | Object.keys( params ).forEach( function ( fieldName ) {
|
44 | const value = params[ fieldName ];
|
45 |
|
46 | postBody.push( `--${boundary}` );
|
47 | postBody.push( CRLF );
|
48 |
|
49 | if ( typeof value === 'string' ) {
|
50 |
|
51 | postBody.push( `Content-Disposition: form-data; name="${fieldName}"` );
|
52 | postBody.push( CRLF );
|
53 | postBody.push( CRLF );
|
54 | postBody.push( Buffer.from( value, 'utf8' ) );
|
55 | } else {
|
56 |
|
57 | postBody.push( `Content-Disposition: form-data; name="${fieldName}"; filename="foo"` );
|
58 | postBody.push( CRLF );
|
59 | postBody.push( CRLF );
|
60 | postBody.push( value );
|
61 | }
|
62 |
|
63 | postBody.push( CRLF );
|
64 | } );
|
65 |
|
66 | postBody.push( `--${boundary}--` );
|
67 |
|
68 |
|
69 | options.headers[ 'content-type' ] = `multipart/form-data; boundary=${boundary}`;
|
70 | options.body = postBody;
|
71 |
|
72 | params = {};
|
73 | }
|
74 |
|
75 |
|
76 | options.url = this.formatUrl( {
|
77 | protocol: this.protocol,
|
78 | port: this.port,
|
79 | hostname: this.server,
|
80 | pathname: this.path + '/api.php',
|
81 | query: ( options.method === 'GET' ) ? params : {}
|
82 | } );
|
83 |
|
84 |
|
85 | if ( method === 'POST' ) {
|
86 | options.form = params;
|
87 | }
|
88 |
|
89 | this.logger.debug( 'API action: %s', actionName );
|
90 | this.logger.debug( '%s <%s>', options.method, options.url );
|
91 |
|
92 | if ( options.form ) {
|
93 | this.logger.debug( 'POST fields: %s', Object.keys( options.form ).join( ', ' ) );
|
94 | }
|
95 |
|
96 | request( options, ( error, response, body ) => {
|
97 | response = response || {};
|
98 |
|
99 | if ( error ) {
|
100 | this.logger.error( 'Request to API failed: %s', error );
|
101 | callback( new Error( `Request to API failed: ${error}` ) );
|
102 | done();
|
103 | return;
|
104 | }
|
105 |
|
106 | if ( response.statusCode !== 200 ) {
|
107 | this.logger.error( 'Request to API failed: HTTP status code was %d for <%s>', response.statusCode || 'unknown', options.url );
|
108 | this.logger.debug( 'Body: %s', body );
|
109 | this.logger.error( 'Stacktrace', new Error().stack );
|
110 |
|
111 | callback( new Error( `Request to API failed: HTTP status code was ${response.statusCode}` ) );
|
112 | done();
|
113 | return;
|
114 | }
|
115 |
|
116 |
|
117 | let data,
|
118 | info,
|
119 | next;
|
120 |
|
121 | try {
|
122 | data = JSON.parse( body );
|
123 | info = data && data[ actionName ];
|
124 |
|
125 |
|
126 | next = data && data[ 'query-continue' ] && data[ 'query-continue' ][ params.list || params.prop ];
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 | if ( !next ) {
|
133 |
|
134 | next = data && data.continue;
|
135 | }
|
136 | } catch ( e ) {
|
137 | this.logger.error( 'Error parsing JSON response: %s', body );
|
138 |
|
139 | callback( new Error( 'Error parsing JSON response' ) );
|
140 | done();
|
141 | return;
|
142 | }
|
143 |
|
144 |
|
145 |
|
146 | if ( data && !data.error ) {
|
147 | if ( next ) {
|
148 | this.logger.debug( 'There\'s more data' );
|
149 | this.logger.debug( next );
|
150 | }
|
151 |
|
152 | callback( null, info, next, data );
|
153 | } else if ( data.error ) {
|
154 | this.logger.error( 'Error returned by API: %s', data.error.info );
|
155 | this.logger.error( 'Raw error', data.error );
|
156 |
|
157 | callback( new Error( `Error returned by API: ${data.error.info}` ) );
|
158 | }
|
159 | done();
|
160 | } );
|
161 | }
|
162 |
|
163 | function Api( options ) {
|
164 | this.protocol = options.protocol || 'http';
|
165 | this.port = options.port;
|
166 | this.server = options.server;
|
167 | this.path = options.path;
|
168 | this.proxy = options.proxy;
|
169 | this.jar = request.jar();
|
170 |
|
171 | this.debug = options.debug;
|
172 |
|
173 |
|
174 | const winston = require( 'winston' ),
|
175 |
|
176 | path = require( 'path' ),
|
177 | fs = require( 'fs' ),
|
178 | logDir = path.dirname( process.argv[ 1 ] ) + '/log/',
|
179 | logFile = logDir + path.basename( process.argv[ 1 ], '.js' ) + '.log',
|
180 |
|
181 | concurrency = options.concurrency || 3;
|
182 |
|
183 | this.logger = winston.createLogger();
|
184 |
|
185 |
|
186 | this.logger.add( new winston.transports.Console( {
|
187 | level: this.debug ? 'debug' : 'error'
|
188 | } ) );
|
189 |
|
190 | if ( fs.existsSync( logDir ) ) {
|
191 | this.logger.add( new winston.transports.File( {
|
192 | colorize: true,
|
193 | filename: logFile,
|
194 | json: false,
|
195 | level: this.debug ? 'debug' : 'info'
|
196 | } ) );
|
197 | }
|
198 |
|
199 |
|
200 |
|
201 | this.queue = async.queue( function ( task, callback ) {
|
202 |
|
203 | task( callback );
|
204 | }, concurrency );
|
205 |
|
206 |
|
207 | this.formatUrl = require( 'url' ).format;
|
208 |
|
209 | this.userAgent = options.userAgent || ( `nodemw/${VERSION} (node.js ${process.version}; ${process.platform} ${process.arch})` );
|
210 | this.version = VERSION;
|
211 |
|
212 |
|
213 | this.info( process.argv.join( ' ' ) );
|
214 | this.info( this.userAgent );
|
215 |
|
216 | let port = this.port ? `:${this.port}` : '';
|
217 |
|
218 | this.info( `Using <${this.protocol}://${this.server}${port}${this.path}/api.php> as API entry point` );
|
219 | this.info( '----' );
|
220 | }
|
221 |
|
222 |
|
223 | Api.prototype = {
|
224 | log() {
|
225 | this.logger.log.apply( this.logger, arguments );
|
226 | },
|
227 |
|
228 | info() {
|
229 | this.logger.info.apply( this.logger, arguments );
|
230 | },
|
231 |
|
232 | warn() {
|
233 | this.logger.warn.apply( this.logger, arguments );
|
234 | },
|
235 |
|
236 | error() {
|
237 | this.logger.error.apply( this.logger, arguments );
|
238 | },
|
239 |
|
240 |
|
241 | call( params, callback, method ) {
|
242 | this.queue.push( ( done ) => {
|
243 | doRequest.apply( this, [ params, callback, method, done ] );
|
244 | } );
|
245 | },
|
246 |
|
247 |
|
248 | fetchUrl( url, callback, encoding ) {
|
249 | encoding = encoding || 'utf-8';
|
250 |
|
251 |
|
252 | this.queue.push( ( done ) => {
|
253 | this.info( 'Fetching <%s> (as %s)...', url, encoding );
|
254 |
|
255 | const options = {
|
256 | url,
|
257 | method: 'GET',
|
258 | proxy: this.proxy || false,
|
259 | jar: this.jar,
|
260 | encoding: ( encoding === 'binary' ) ? null : encoding,
|
261 | headers: {
|
262 | 'User-Agent': this.userAgent
|
263 | }
|
264 | };
|
265 |
|
266 | request( options, ( error, response, body ) => {
|
267 | if ( !error && response.statusCode === 200 ) {
|
268 | this.info( '<%s>: fetched %s kB', url, ( body.length / 1024 ).toFixed( 2 ) );
|
269 | callback( null, body );
|
270 | } else {
|
271 | if ( !error ) {
|
272 | error = new Error( `HTTP status ${response.statusCode}` );
|
273 | }
|
274 |
|
275 | this.error( `Failed to fetch <${url}>` );
|
276 | this.error( error.message );
|
277 | callback( error, body );
|
278 | }
|
279 |
|
280 | done();
|
281 | } );
|
282 | } );
|
283 | }
|
284 | };
|
285 |
|
286 | module.exports = Api;
|