Added VKBot

This commit is contained in:
amikhaylov
2026-05-28 02:16:26 +03:00
parent 20450c4ede
commit e90314f18b
16 changed files with 464 additions and 5 deletions
+4 -2
View File
@@ -10,7 +10,6 @@ APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database # APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4 # PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
@@ -62,4 +61,7 @@ AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET= AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false 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}"
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class VkBotController extends Controller
{
public function handle(Request $request)
{
// Получаем данные от VK в формате массива
$data = $request->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');
}
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Entity;
use Carbon\Carbon;
class VkPost
{
private int $id;
private bool $post = true;
private string $text;
private int $author_id;
private int $owner_id;
private array $attachments = [];
private Carbon $date;
public function toArray(): array
{
return [
'id' => $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;
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Mapper;
class AttachmentMapper
{
public function map(array $attachments = []): array
{
$photos = [];
foreach ($attachments as $attachment) {
if( ($attachment['type'] === 'photo') && $attachment['photo']['sizes'] ) {
$photos[] = end($attachment['photo']['sizes'])['url'];
}
}
return $photos;
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Mapper;
use App\Library\VK\Entity\VkPost;
use App\Library\VK\Mapper\Strategy\Interfaces\MappingStrategyInterface;
use App\Library\VK\Mapper\Strategy\MappingStrategyFactory;
class PostMapper
{
public function __construct(
private MappingStrategyFactory $mappingStrategyFactory,
) {
}
public function map(array $item): VkPost
{
$isRepost = isset($item['copy_history']);
/** @var MappingStrategyInterface $strategy */
$strategy = $this->mappingStrategyFactory->getStrategy($isRepost);
return $strategy->map($item);
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Mapper\Strategy\Interfaces;
use App\Library\VK\Entity\VkPost;
interface MappingStrategyInterface
{
public function map(array $item): VkPost;
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Mapper\Strategy;
use App\Library\VK\Mapper\AttachmentMapper;
use App\Library\VK\Mapper\Strategy\Interfaces\MappingStrategyInterface;
class MappingStrategyFactory
{
public function __construct(private AttachmentMapper $attachmentMapper) {}
public function getStrategy(bool $isRepost): MappingStrategyInterface
{
return $isRepost
? new RepostStrategy($this->attachmentMapper)
: new SimplePostStrategy($this->attachmentMapper);
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Mapper\Strategy;
use App\Library\VK\Entity\VkPost;
use App\Library\VK\Mapper\AttachmentMapper;
use App\Library\VK\Mapper\Strategy\Interfaces\MappingStrategyInterface;
class RepostStrategy implements MappingStrategyInterface
{
public function __construct(private AttachmentMapper $attachmentMapper) {}
public function map(array $item): VkPost
{
$post = new VkPost();
$post->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;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Mapper\Strategy;
use App\Library\VK\Entity\VkPost;
use App\Library\VK\Mapper\AttachmentMapper;
use App\Library\VK\Mapper\Strategy\Interfaces\MappingStrategyInterface;
class SimplePostStrategy implements MappingStrategyInterface
{
public function __construct(private AttachmentMapper $attachmentMapper) {}
public function map(array $item): VkPost
{
$post = new VkPost();
$post->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;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Resources;
use App\Library\VK\VkApiClient;
class WallResource
{
public function __construct(private VkApiClient $api) {}
public function get(string $domain, int $count = 50, int $offset = 0): array
{
$result = $this->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' => [] ];
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Library\VK\Service;
use App\Library\VK\Mapper\PostMapper;
use App\Library\VK\Resources\WallResource;
use Illuminate\Support\Facades\Log;
class VkPostImportService
{
public function __construct(
private WallResource $resource,
private PostMapper $mapper
) {
}
public function run(): void
{
//$result = $this->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);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Library\VK;
use Exception;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
final class VkApiClient
{
public function __construct(
protected ClientInterface $http,
protected string $token,
protected string $url,
protected string $version = '5.131'
) {}
/**
* @throws GuzzleException
*/
public function call(
string $method,
array $params = []
): array {
$response = $this->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);
}
}
+10 -1
View File
@@ -4,6 +4,8 @@ namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator; use Illuminate\Pagination\Paginator;
use App\Library\VK\VkApiClient;
use GuzzleHttp\Client as GuzzleHttpClient;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@@ -12,7 +14,14 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void 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
);
});
} }
/** /**
+4 -1
View File
@@ -11,7 +11,10 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
// // Отключаем CSRF-защиту для вебхука VK
$middleware->validateCsrfTokens(except: [
'vk-webhook',
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //
+5 -1
View File
@@ -34,5 +34,9 @@ return [
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), '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'),
],
]; ];
+4
View File
@@ -11,6 +11,7 @@ use App\Http\Controllers\Website\RiderController;
use App\Http\Controllers\Website\StartController; use App\Http\Controllers\Website\StartController;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\VkBotController;
Route::middleware('auth') Route::middleware('auth')
->group(function () { ->group(function () {
@@ -44,3 +45,6 @@ Route::get('/api/getplace/id/{id}', [ MapController::class, 'getPlace' ]);
Auth::routes(); Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
/** VK api web hook */
Route::post('/vk-webhook', [ VkBotController::class, 'handle']);