· 6 years ago · Oct 21, 2019, 08:52 PM
1<?php
2
3namespace VK\Client;
4
5
6use App\Domain\ProxyServer\Repositories\ProxyServerRepository;
7use App\Models\ProxyServer;
8use Illuminate\Support\Facades\Log;
9use VK\Exceptions\Api\ExceptionMapper;
10use VK\Exceptions\Api\VKApiFloodException;
11use VK\Exceptions\Api\VKApiTooManyException;
12use VK\Exceptions\VKApiException;
13use VK\Exceptions\VKClientException;
14use VK\TransportClient\Curl\CurlHttpClient;
15use VK\TransportClient\TransportClientResponse;
16use VK\TransportClient\TransportRequestException;
17
18class VKApiRequest {
19 private const PARAM_VERSION = 'v';
20 private const PARAM_ACCESS_TOKEN = 'access_token';
21 private const PARAM_LANG = 'lang';
22
23 private const KEY_ERROR = 'error';
24 private const KEY_RESPONSE = 'response';
25
26 protected const CONNECTION_TIMEOUT = 10;
27 protected const HTTP_STATUS_CODE_OK = 200;
28
29 private $currentPriority;
30 private $minimalAwaitTime;
31 private $blockAllExceptHighPriorityRequestsKey ;
32
33 /**
34 * @var string
35 */
36 private $host;
37
38 /**
39 * @var CurlHttpClient
40 */
41 private $http_client;
42
43 /**
44 * @var string
45 */
46 private $version;
47
48 /**
49 * @var string|null
50 */
51 private $language;
52
53 private $outoing_ip;
54
55 /**
56 * VKApiRequest constructor.
57 * @param string $api_version
58 * @param string|null $language
59 * @param string $host
60 */
61 public function __construct(string $api_version, ?string $language, string $host) {
62 $this->http_client = new CurlHttpClient(static::CONNECTION_TIMEOUT);
63 $this->version = $api_version;
64 $this->host = $host;
65 $this->language = $language;
66 }
67
68 /**
69 * Makes post request.
70 *
71 * @param string $method
72 * @param string $access_token
73 * @param array $params
74 *
75 * @return mixed
76 *
77 * @throws VKClientException
78 * @throws VKApiException
79 */
80 public function post(string $method, string $access_token, array $params = array(),int $repeatedRequest = 0) {
81 $params = $this->formatParams($params);
82 $params[static::PARAM_ACCESS_TOKEN] = $access_token;
83
84 if (!isset($params[static::PARAM_VERSION])) {
85 $params[static::PARAM_VERSION] = $this->version;
86 }
87
88 if ($this->language && !isset($params[static::PARAM_LANG])) {
89 $params[static::PARAM_LANG] = $this->language;
90 }
91
92 $url = $this->host . '/' . $method;
93
94 $key = 'VkRequestLock:'.$access_token;
95
96 $complicatedRequest = false;
97
98 switch ($method) {
99 case 'ads.updateAds':
100 case 'ads.createAds':
101 case 'ads.getTargetingStats':
102 case 'ads.importTargetContacts':
103 case 'ads.getPostsReach':
104 $complicatedRequest = true;
105 $this->currentPriority = 'High';
106 break;
107 case 'wall.editAdsStealth':
108 $this->currentPriority = 'High';
109 break;
110 default:
111 $this->currentPriority = 'Default';
112 break;
113 }
114
115 $currentMinimalAwaitTimeKey = 'VkRequest:MinimalAwaitTime:'.$access_token;
116
117 if (!$this->minimalAwaitTime = \Cache::connection()->get($currentMinimalAwaitTimeKey)) {
118 \Cache::connection()->set($currentMinimalAwaitTimeKey,env('VKONTAKTE_MINIMAL_AWAIT_TIME',500));
119 }
120
121 $currentPriorityKey = $key.':'.$this->currentPriority;
122 $complicatedKey = $key.':Complicated';
123
124 $this->blockAllExceptHighPriorityRequestsKey = 'VkRequest:BlockAllExceptHighPriorityRequests:'.\Auth::id();
125
126 $startQueueWaitTime = microtime(true);
127
128 if ($complicatedRequest) {
129 do {
130 if ($this->canSendComplicatedRequest($complicatedKey)) {
131 break;
132 } else {
133 usleep(rand(50000,150000));
134 }
135 } while (true);
136 }
137
138 do {
139 if ($this->canSendCurrentPriorityRequest($currentPriorityKey)) {
140 break;
141 } else {
142 usleep(rand(50000,150000));
143 }
144 } while (true);
145
146 $queueWaitTime = microtime(true) - $startQueueWaitTime;
147
148
149 /** @var ProxyServer $proxyServer */
150 $proxyServer = null;
151
152
153 if (env('APP_ENV') == 'production') {
154 $proxyServer = ProxyServerRepository::getAvailableProxyServer('vk');
155 }
156
157 $requestExectionStartTime = microtime(true);
158
159 try {
160 $response = $this->http_client->post($url, $params, $proxyServer);
161 } catch (TransportRequestException $transportRequestException) {
162 if ($proxyServer) {
163 $proxyServer->update([
164 'status' => ProxyServer::PROXY_SERVER_STATUS_FAILED
165 ]);
166 }
167 throw $transportRequestException;
168 } catch (\Exception $e) {
169
170 \Cache::connection()->pexpire($currentPriorityKey,1000);
171
172 if ($proxyServer) {
173 ProxyServerRepository::releaseProxyServer($proxyServer, 'vk');
174 }
175
176 \Log::driver('vk')->error('Vk Client exception', [
177 'error_user_id' => \Auth::id(),
178 'error_user_vk_id' => \Auth::getUser()->vk_id,
179 'error_user_name' => \Auth::getUser()->name,
180 'error_code' => $e->getCode(),
181 'error_message' => $e->getMessage(),
182 'error_trace' => $e->getTraceAsString(),
183 'outgoing_ip' => $proxyServer ? $proxyServer->ip . ':' . $proxyServer->port : 'null',
184 'proxy_server' => $proxyServer ? $proxyServer->toArray() : []
185 ]
186 );
187
188 if (strpos($e->getMessage(), 'SOCKS') !== false) {
189 return $this->post($method, $access_token, $params, $repeatedRequest + 1);
190 }
191
192 throw new VKClientException($e);
193 }
194
195 if ($proxyServer) {
196 ProxyServerRepository::releaseProxyServer($proxyServer, 'vk');
197 }
198
199 $requestExectionTime = microtime(true) - $requestExectionStartTime;
200
201 \Log::channel('vk')->info('Request stats', [
202 'method_name' => $method,
203 'queue_wait_time' => $queueWaitTime,
204 'request_priority' => $this->currentPriority,
205 'request_url' => $url,
206 'request_params' => $params,
207 'request_response_new' => [
208 'body' => $response->getBody(),
209 'headers' => $response->getHeaders(),
210 'http_status' => $response->getHttpStatus()
211 ],
212 'user_id' => \Auth::id(),
213 'user_vk_id' => \Auth::getUser()->vk_id,
214 'user_name' => \Auth::getUser()->name,
215 'outgoing_ip' => $proxyServer ? $proxyServer->ip . ':' . $proxyServer->port : 'null',
216 'proxy_server' => $proxyServer ? $proxyServer->toArray() : [],
217 'request_execution_time' => $requestExectionTime
218 ]);
219
220
221
222 try {
223 $response = $this->parseResponse($response);
224 } catch (VKApiFloodException $e) {
225
226 if (!$repeatedRequest) {
227 usleep($this->minimalAwaitTime);
228
229 $newMinimalAwaitTime = \Cache::connection()->incrby($currentMinimalAwaitTimeKey,10);
230
231 \Cache::connection()->expire($currentMinimalAwaitTimeKey, 60);
232
233 Log::channel('vk')->info('Flood control', [
234 'current_await_time' => $this->minimalAwaitTime,
235 'new_await_time' => $newMinimalAwaitTime,
236 'outgoing_ip' => $proxyServer ? $proxyServer->ip . ':' . $proxyServer->port : 'null',
237 'proxy_server' => $proxyServer ? $proxyServer->toArray() : []
238 ]);
239
240 } else {
241 $sleep = rand(100,1500) * $repeatedRequest;
242 \Log::channel('vk')->info('Flood control', [
243 'current_await_time' => $this->minimalAwaitTime,
244 'outgoing_ip' => $proxyServer ? $proxyServer->ip . ':' . $proxyServer->port : 'null',
245 'proxy_server' => $proxyServer ? $proxyServer->toArray() : [],
246 'sleep' => $sleep,
247 'token' => $access_token
248 ]);
249 usleep($sleep);
250 }
251
252 $this->releaseQueueKey($currentPriorityKey);
253
254 return $this->post($method,$access_token,$params,$repeatedRequest+1);
255
256 } catch (VKApiTooManyException $e) {
257 Log::channel('vk')->info('Too many requests exception', [
258 'code' => $e->getCode(),
259 'message' => $e->getMessage(),
260 'trace' => $e->getTraceAsString(),
261 'outgoing_ip' => $proxyServer ? $proxyServer->ip . ':' . $proxyServer->port : 'null',
262 'proxy_server' => $proxyServer ? $proxyServer->toArray() : []
263 ]
264 );
265
266 usleep(rand(1000,2000));
267
268 $this->releaseQueueKey($currentPriorityKey);
269
270 return $this->post($method,$access_token,$params,$repeatedRequest+1);
271 }
272
273
274
275 if ($requestExectionTime < 500 && $nextRequestCanBePerformedInMilliseconds = $this->minimalAwaitTime - $requestExectionTime) {
276 \Cache::connection()->pexpire($currentPriorityKey,intval($nextRequestCanBePerformedInMilliseconds));
277 } else {
278 \Cache::connection()->del($currentPriorityKey);
279 }
280
281 return $response;
282
283 }
284
285 /**
286 * Uploads data by its path to the given url.
287 *
288 * @param string $upload_url
289 * @param string $parameter_name
290 * @param string $path
291 *
292 * @return mixed
293 *
294 * @throws VKClientException
295 * @throws VKApiException
296 */
297 public function upload(string $upload_url, string $parameter_name, string $path) {
298 try {
299 $response = $this->http_client->upload($upload_url, $parameter_name, $path);
300 } catch (TransportRequestException $e) {
301 throw new VKClientException($e);
302 }
303
304 return $this->parseResponse($response);
305 }
306
307 private function releaseQueueKey(string $key) {
308 \Cache::connection()->del($key);
309 }
310
311 private function canSendComplicatedRequest($complicatedKey) {
312
313 if (\Cache::connection()->ttl($complicatedKey) == -1) {
314 \Cache::connection()->del($complicatedKey);
315 }
316
317 if (\Cache::connection()->set($complicatedKey,0,'EX',10,'NX')) {
318 return true;
319 } else {
320 if (\Cache::connection()->get($complicatedKey) < 5) {
321 \Cache::connection()->incr($complicatedKey);
322 return true;
323 }
324 }
325
326 return false;
327 }
328
329 private function canSendCurrentPriorityRequest($keyWithPriority) {
330 if ($this->currentPriority == 'High') {
331 return $this->canSendRequest($keyWithPriority);
332 } else if (!\Cache::connection()->get($keyWithPriority) && !\Cache::connection()->get($this->blockAllExceptHighPriorityRequestsKey)) {
333 return $this->canSendRequest($keyWithPriority);
334 } else {
335 return false;
336 }
337
338 }
339
340 private function shouldBlockAllRequestsExceptHighPriority() {
341
342 }
343
344 private function canSendRequest($key) {
345 if (\Cache::connection()->ttl($key) == -1) {
346 \Cache::connection()->del($key);
347 }
348
349 return \Cache::connection()->set($key, 1, 'PX', env('VK_REQUEST_LOCK_TIMEOUT', 600), 'NX');
350 }
351
352 /**
353 * Decodes the response and checks its status code and whether it has an Api error. Returns decoded response.
354 *
355 * @param TransportClientResponse $response
356 *
357 * @return mixed
358 *
359 * @throws VKApiException
360 * @throws VKClientException
361 */
362 private function parseResponse(TransportClientResponse $response) {
363 $this->checkHttpStatus($response);
364
365 $body = $response->getBody();
366 $decode_body = $this->decodeBody($body);
367
368 if (isset($decode_body[static::KEY_ERROR])) {
369 $error = $decode_body[static::KEY_ERROR];
370 $api_error = new VKApiError($error);
371 throw ExceptionMapper::parse($api_error);
372 }
373
374 if (isset($decode_body[static::KEY_RESPONSE])) {
375 return $decode_body[static::KEY_RESPONSE];
376 } else {
377 return $decode_body;
378 }
379 }
380
381 /**
382 * Formats given array of parameters for making the request.
383 *
384 * @param array $params
385 *
386 * @return array
387 */
388 private function formatParams(array $params) {
389 foreach ($params as $key => $value) {
390 if (is_array($value)) {
391 $params[$key] = implode(',', $value);
392 } else if (is_bool($value)) {
393 $params[$key] = $value ? 1 : 0;
394 }
395 }
396 return $params;
397 }
398
399 /**
400 * Decodes body.
401 *
402 * @param string $body
403 *
404 * @return mixed
405 */
406 protected function decodeBody(string $body) {
407 $decoded_body = json_decode($body, true);
408
409 if ($decoded_body === null || !is_array($decoded_body)) {
410 $decoded_body = [];
411 }
412
413 return $decoded_body;
414 }
415
416 /**
417 * @param TransportClientResponse $response
418 *
419 * @throws VKClientException
420 */
421 protected function checkHttpStatus(TransportClientResponse $response) {
422 if ($response->getHttpStatus() != static::HTTP_STATUS_CODE_OK) {
423 throw new VKClientException("Invalid http status: {$response->getHttpStatus()}");
424 }
425 }
426}