From e90314f18b3afc388a37ebb55bbe352e23d1e8b2 Mon Sep 17 00:00:00 2001 From: amikhaylov Date: Thu, 28 May 2026 02:16:26 +0300 Subject: [PATCH] Added VKBot --- .env | 6 +- app/Http/Controllers/VkBotController.php | 41 +++++++ app/Library/VK/Entity/VkPost.php | 114 ++++++++++++++++++ app/Library/VK/Mapper/AttachmentMapper.php | 20 +++ app/Library/VK/Mapper/PostMapper.php | 28 +++++ .../Interfaces/MappingStrategyInterface.php | 12 ++ .../Strategy/MappingStrategyFactory.php | 20 +++ .../VK/Mapper/Strategy/RepostStrategy.php | 49 ++++++++ .../VK/Mapper/Strategy/SimplePostStrategy.php | 32 +++++ app/Library/VK/Resources/WallResource.php | 36 ++++++ .../VK/Service/VkPostImportService.php | 47 ++++++++ app/Library/VK/VkApiClient.php | 38 ++++++ app/Providers/AppServiceProvider.php | 11 +- bootstrap/app.php | 5 +- config/services.php | 6 +- routes/web.php | 4 + 16 files changed, 464 insertions(+), 5 deletions(-) create mode 100644 app/Http/Controllers/VkBotController.php create mode 100644 app/Library/VK/Entity/VkPost.php create mode 100644 app/Library/VK/Mapper/AttachmentMapper.php create mode 100644 app/Library/VK/Mapper/PostMapper.php create mode 100644 app/Library/VK/Mapper/Strategy/Interfaces/MappingStrategyInterface.php create mode 100644 app/Library/VK/Mapper/Strategy/MappingStrategyFactory.php create mode 100644 app/Library/VK/Mapper/Strategy/RepostStrategy.php create mode 100644 app/Library/VK/Mapper/Strategy/SimplePostStrategy.php create mode 100644 app/Library/VK/Resources/WallResource.php create mode 100644 app/Library/VK/Service/VkPostImportService.php create mode 100644 app/Library/VK/VkApiClient.php diff --git a/.env b/.env index cee3d65..93e9f13 100755 --- a/.env +++ b/.env @@ -10,7 +10,6 @@ APP_FAKER_LOCALE=en_US APP_MAINTENANCE_DRIVER=file # APP_MAINTENANCE_STORE=database - # PHP_CLI_SERVER_WORKERS=4 BCRYPT_ROUNDS=12 @@ -62,4 +61,7 @@ AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false -VITE_APP_NAME="${APP_NAME}" +VK_API_TOKEN=a9eb7582a9eb7582a9eb7582f3aaaa27d0aa9eba9eb7582c3fc47622763de78e4f0d8ad +VK_API_VERSION=5.131 + +# VITE_APP_NAME="${APP_NAME}" diff --git a/app/Http/Controllers/VkBotController.php b/app/Http/Controllers/VkBotController.php new file mode 100644 index 0000000..6eae363 --- /dev/null +++ b/app/Http/Controllers/VkBotController.php @@ -0,0 +1,41 @@ +all(); + + // Проверяем тип запроса + if (! isset($data['type'])) { return response('ok', 200); } + + // 1. Подтверждение сервера для VK (срабатывает один раз) + if ($data['type'] === 'confirmation') { + // Замените ЭТУ_СТРОКУ на код из настроек Callback API в VK + return response('ЭТУ_СТРОКУ', 200) + ->header('Content-Type', 'text/plain'); + } + + // 2. Получение новой записи на стене (новости) + if ($data['type'] === 'wall_post_new') { + $post = $data['object']; // Здесь вся информация о посте + + // Временно запишем в лог (storage/logs/laravel.log), чтобы увидеть структуру + Log::info('Новый пост от VK:', $post); + + // TODO: Здесь будет код сохранения $post в вашу базу данных + + // VK требует всегда возвращать строку "ok" на любые события + return response('ok', 200) + ->header('Content-Type', 'text/plain'); + } + } +} diff --git a/app/Library/VK/Entity/VkPost.php b/app/Library/VK/Entity/VkPost.php new file mode 100644 index 0000000..92700be --- /dev/null +++ b/app/Library/VK/Entity/VkPost.php @@ -0,0 +1,114 @@ + $this->id, + 'post' => (int) $this->post, + 'text' => $this->text, + 'author_id' => $this->author_id, + 'owner_id' => $this->owner_id, + 'date' => $this->getDate()->toIso8601String(), + 'attachments' => $this->attachments, + ]; + } + + public function isEmpty(): bool + { + return empty($this->text); + } + + public function isPost(): bool + { + return $this->post; + } + + public function isRepost(): bool + { + return ! $this->post; + } + + public function setIsPost(): void + { + $this->post = true; + } + + public function setIsRepost(): void + { + $this->post = false; + } + + public function getId(): int + { + return $this->id; + } + + public function getText(): string + { + return $this->text; + } + + public function getAuthorId(): int + { + return $this->author_id; + } + + public function getDate(): Carbon + { + return $this->date; + } + + /** + * @return string + */ + public function getOwnerId(): int + { + return $this->owner_id; + } + + public function setId(int $id): void + { + $this->id = $id; + } + + public function setText(string $text): void + { + $this->text = $text; + } + + public function setAuthorId(int $author_id): void + { + $this->author_id = $author_id; + } + + public function setDate(string|int $date): void + { + $this->date = Carbon::parse($date); + } + + public function setOwnerId(int $owner_id): void + { + $this->owner_id = $owner_id; + } + + public function setAttachments(array $attachments = []): void + { + $this->attachments = $attachments; + } +} diff --git a/app/Library/VK/Mapper/AttachmentMapper.php b/app/Library/VK/Mapper/AttachmentMapper.php new file mode 100644 index 0000000..4fa79f6 --- /dev/null +++ b/app/Library/VK/Mapper/AttachmentMapper.php @@ -0,0 +1,20 @@ +mappingStrategyFactory->getStrategy($isRepost); + + return $strategy->map($item); + } +} diff --git a/app/Library/VK/Mapper/Strategy/Interfaces/MappingStrategyInterface.php b/app/Library/VK/Mapper/Strategy/Interfaces/MappingStrategyInterface.php new file mode 100644 index 0000000..a424f5b --- /dev/null +++ b/app/Library/VK/Mapper/Strategy/Interfaces/MappingStrategyInterface.php @@ -0,0 +1,12 @@ +attachmentMapper) + : new SimplePostStrategy($this->attachmentMapper); + } +} diff --git a/app/Library/VK/Mapper/Strategy/RepostStrategy.php b/app/Library/VK/Mapper/Strategy/RepostStrategy.php new file mode 100644 index 0000000..051e6cb --- /dev/null +++ b/app/Library/VK/Mapper/Strategy/RepostStrategy.php @@ -0,0 +1,49 @@ +setId($item['id']); + $post->setOwnerId($item['owner_id']); + $post->setAuthorId($item['from_id']); + $post->setDate($item['date']); + $post->setIsRepost(); + $post->setText($item['text'] ?? ''); + + // Переключаемся на оригинал для контента + $original = $this->getOriginalPost($item); + + $post->setText($original['text'] ?? ''); + + // Вложения берем именно из оригинала + if (! empty($original['attachments'])) { + $attachments = $this->attachmentMapper->map($original['attachments']); + $post->setAttachments($attachments); + } + + return $post; + } + + private function getOriginalPost(array $item): array + { + $copy_history_length = count($item['copy_history']); + + if($copy_history_length > 0) { + return end($item['copy_history']); + } + + return $item; + } +} diff --git a/app/Library/VK/Mapper/Strategy/SimplePostStrategy.php b/app/Library/VK/Mapper/Strategy/SimplePostStrategy.php new file mode 100644 index 0000000..3e9d5d4 --- /dev/null +++ b/app/Library/VK/Mapper/Strategy/SimplePostStrategy.php @@ -0,0 +1,32 @@ +setId($item['id']); + $post->setOwnerId($item['owner_id']); + $post->setAuthorId($item['from_id']); + $post->setDate($item['date']); + $post->setIsPost(); + $post->setText($item['text'] ?? ''); + + if (! empty($item['attachments'])) { + $attachments = $this->attachmentMapper->map($item['attachments']); + $post->setAttachments($attachments); + } + + return $post; + } +} diff --git a/app/Library/VK/Resources/WallResource.php b/app/Library/VK/Resources/WallResource.php new file mode 100644 index 0000000..ee238d0 --- /dev/null +++ b/app/Library/VK/Resources/WallResource.php @@ -0,0 +1,36 @@ +api->call('wall.get', [ + 'domain' => $domain, + 'offset' => $offset, + 'count' => $count, + 'copy_history_depth' => 10 + ]); + + return $result['response'] ?? [ 'count' => 0, 'items' => [] ]; + } + + // id = '-93243530_2113' + public function getById(string $id): array + { + $result = $this->api->call('wall.getById', + [ + 'posts' => [ $id ], + 'copy_history_depth' => 10 + ]); + + return $result['response'] ?? [ 'items' => [] ]; + } +} diff --git a/app/Library/VK/Service/VkPostImportService.php b/app/Library/VK/Service/VkPostImportService.php new file mode 100644 index 0000000..be804c7 --- /dev/null +++ b/app/Library/VK/Service/VkPostImportService.php @@ -0,0 +1,47 @@ +resource->get('ledstarband', 1, 100); + + if (empty ($result['items']) ) { + Log::info('Post import failed: no items found'); + } + + # echo "\nFound items: ".$result['count']."\n"; +// foreach ($result['items'] as $item) { +// $post = $this->mapper->map($item); +// if(! $post->isEmpty()) { +// # print_r($post->toArray()); +// } +// } + + $res = $this->resource->getById('-93243530_2113'); + foreach ($res as $item) { + $post = $this->mapper->map($item); + if(! $post->isEmpty()) { + print_r($post->toArray()); + } + } + + # print_r($res); + + } +} diff --git a/app/Library/VK/VkApiClient.php b/app/Library/VK/VkApiClient.php new file mode 100644 index 0000000..5132f6a --- /dev/null +++ b/app/Library/VK/VkApiClient.php @@ -0,0 +1,38 @@ +http->request('POST', "{$this->url}/{$method}", [ + 'form_params' => array_merge($params, [ + 'access_token' => $this->token, + 'v' => $this->version, + ]), + ]); + + if (isset($result['error'])) { + throw new Exception("VK API Error: " . $result['error']['error_msg']); + } + + return json_decode($response->getBody()->getContents(), true); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9aa3fe9..c389eb4 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,8 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Pagination\Paginator; +use App\Library\VK\VkApiClient; +use GuzzleHttp\Client as GuzzleHttpClient; class AppServiceProvider extends ServiceProvider { @@ -12,7 +14,14 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(VkApiClient::class, function ($app) { + return new VkApiClient( + new GuzzleHttpClient(), // Создаем экземпляр Guzzle + config('services.vk.token'), // Берем из config/services.php + config('services.vk.url'), // Добавляем URL api + config('services.vk.version'), // Берем из config/services.php + ); + }); } /** diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..4131f4f 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,7 +11,10 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + // Отключаем CSRF-защиту для вебхука VK + $middleware->validateCsrfTokens(except: [ + 'vk-webhook', + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/config/services.php b/config/services.php index 6a90eb8..ab548b4 100755 --- a/config/services.php +++ b/config/services.php @@ -34,5 +34,9 @@ return [ 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), ], ], - + 'vk' => [ + 'token' => env('VK_API_TOKEN'), + 'version' => env('VK_API_VERSION', '5.131'), // 5.131 — значение по умолчанию + 'url' => env('VK_API_URL', 'https://api.vk.ru/method'), + ], ]; diff --git a/routes/web.php b/routes/web.php index da5a11b..99ba95f 100755 --- a/routes/web.php +++ b/routes/web.php @@ -11,6 +11,7 @@ use App\Http\Controllers\Website\RiderController; use App\Http\Controllers\Website\StartController; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; +use App\Http\Controllers\VkBotController; Route::middleware('auth') ->group(function () { @@ -44,3 +45,6 @@ Route::get('/api/getplace/id/{id}', [ MapController::class, 'getPlace' ]); Auth::routes(); Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); + +/** VK api web hook */ +Route::post('/vk-webhook', [ VkBotController::class, 'handle']);