404 lines
14 KiB
PHP
404 lines
14 KiB
PHP
<?php
|
||
/**
|
||
* VK API Client for posting to groups/walls
|
||
* Compatible with PHP 7.2+
|
||
*
|
||
* Требования:
|
||
* - VK Access Token с правами: wall, photos, groups
|
||
* - Получить токен: https://vk.com/dev → Create App → Get Token
|
||
*/
|
||
|
||
class VKAPI
|
||
{
|
||
private $accessToken;
|
||
private $apiVersion = '5.199';
|
||
private $baseUrl = 'https://api.vk.com/method/';
|
||
private $userId;
|
||
|
||
public function __construct($accessToken)
|
||
{
|
||
$this->accessToken = $accessToken;
|
||
}
|
||
|
||
/**
|
||
* Make API request to VK
|
||
*
|
||
* @param string $method API method
|
||
* @param array $params Parameters
|
||
* @return array Response
|
||
*/
|
||
private function request($method, $params = [])
|
||
{
|
||
$params['access_token'] = $this->accessToken;
|
||
$params['v'] = $this->apiVersion;
|
||
|
||
$url = $this->baseUrl . $method;
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => http_build_query($params),
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 60,
|
||
CURLOPT_SSL_VERIFYPEER => true,
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$error = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
if ($error) {
|
||
throw new RuntimeException("VK API connection error: {$error}");
|
||
}
|
||
|
||
$data = json_decode($response, true);
|
||
|
||
if (isset($data['error'])) {
|
||
$errorMsg = isset($data['error']['error_msg']) ? $data['error']['error_msg'] : 'Unknown error';
|
||
$errorCode = isset($data['error']['error_code']) ? $data['error']['error_code'] : 0;
|
||
throw new RuntimeException("VK API error [{$errorCode}]: {$errorMsg}");
|
||
}
|
||
|
||
return isset($data['response']) ? $data['response'] : [];
|
||
}
|
||
|
||
/**
|
||
* Get current user info
|
||
*
|
||
* @return array User info
|
||
*/
|
||
public function getMe()
|
||
{
|
||
$result = $this->request('users.get', [
|
||
'fields' => 'photo_100,screen_name'
|
||
]);
|
||
|
||
if (!empty($result[0])) {
|
||
$this->userId = $result[0]['id'];
|
||
return $result[0];
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* Get groups where user can post
|
||
*
|
||
* @param int $count Number of groups to return
|
||
* @return array Groups list
|
||
*/
|
||
public function getGroups($count = 100)
|
||
{
|
||
$result = $this->request('groups.get', [
|
||
'extended' => 1,
|
||
'filter' => 'admin,editor,moder',
|
||
'fields' => 'name,screen_name,photo_100,can_post',
|
||
'count' => $count
|
||
]);
|
||
|
||
$groups = [];
|
||
if (isset($result['items'])) {
|
||
foreach ($result['items'] as $group) {
|
||
// Only groups where posting is allowed
|
||
if (!empty($group['can_post']) || isset($group['admin_level'])) {
|
||
$groups[] = [
|
||
'id' => '-' . $group['id'], // Negative for group wall
|
||
'name' => $group['name'],
|
||
'screen_name' => isset($group['screen_name']) ? $group['screen_name'] : '',
|
||
'photo' => isset($group['photo_100']) ? $group['photo_100'] : '',
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
return $groups;
|
||
}
|
||
|
||
/**
|
||
* Upload photo to VK from URL
|
||
*
|
||
* @param int $groupId Group ID (without minus, positive number)
|
||
* @param string $photoUrl Photo URL to upload
|
||
* @return string Attachment string (photo{owner}_{id})
|
||
*/
|
||
public function uploadPhotoFromUrl($groupId, $photoUrl)
|
||
{
|
||
$groupId = abs((int)$groupId);
|
||
|
||
// Get upload server
|
||
try {
|
||
$uploadServer = $this->request('photos.getWallUploadServer', [
|
||
'group_id' => $groupId
|
||
]);
|
||
} catch (Exception $e) {
|
||
throw new RuntimeException('Не удалось получить сервер загрузки VK: ' . $e->getMessage());
|
||
}
|
||
|
||
if (!isset($uploadServer['upload_url'])) {
|
||
throw new RuntimeException('VK не вернул URL для загрузки фото');
|
||
}
|
||
|
||
// Download photo from URL with context for HTTPS
|
||
$context = stream_context_create([
|
||
'http' => [
|
||
'timeout' => 30,
|
||
'user_agent' => 'VH-Posting-System/1.0'
|
||
],
|
||
'ssl' => [
|
||
'verify_peer' => true,
|
||
'verify_peer_name' => true
|
||
]
|
||
]);
|
||
|
||
$photoData = @file_get_contents($photoUrl, false, $context);
|
||
if ($photoData === false) {
|
||
throw new RuntimeException('Не удалось скачать фото с Flickr: ' . $photoUrl);
|
||
}
|
||
|
||
// Detect mime type
|
||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||
$mimeType = $finfo->buffer($photoData) ?: 'image/jpeg';
|
||
$extension = $mimeType === 'image/png' ? 'png' : 'jpg';
|
||
|
||
// Save to temp file
|
||
$tempFile = tempnam(sys_get_temp_dir(), 'vk_photo_');
|
||
file_put_contents($tempFile, $photoData);
|
||
|
||
// Upload photo using CURLFile
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_URL => $uploadServer['upload_url'],
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => [
|
||
'photo' => new CURLFile($tempFile, $mimeType, 'photo.' . $extension)
|
||
],
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 120,
|
||
]);
|
||
|
||
$uploadResponse = curl_exec($ch);
|
||
$curlError = curl_error($ch);
|
||
curl_close($ch);
|
||
unlink($tempFile);
|
||
|
||
if ($curlError) {
|
||
throw new RuntimeException('Ошибка загрузки фото на VK: ' . $curlError);
|
||
}
|
||
|
||
$uploadData = json_decode($uploadResponse, true);
|
||
if (!$uploadData) {
|
||
throw new RuntimeException('Неверный ответ от сервера VK при загрузке');
|
||
}
|
||
|
||
if (empty($uploadData['photo']) || $uploadData['photo'] === '[]') {
|
||
$errorMsg = isset($uploadData['error']) ? $uploadData['error'] : 'пустой ответ';
|
||
throw new RuntimeException('VK не принял фото: ' . $errorMsg);
|
||
}
|
||
|
||
// Save photo
|
||
try {
|
||
$savedPhoto = $this->request('photos.saveWallPhoto', [
|
||
'group_id' => $groupId,
|
||
'photo' => $uploadData['photo'],
|
||
'server' => $uploadData['server'],
|
||
'hash' => $uploadData['hash']
|
||
]);
|
||
} catch (Exception $e) {
|
||
throw new RuntimeException('Не удалось сохранить фото в VK: ' . $e->getMessage());
|
||
}
|
||
|
||
if (empty($savedPhoto[0])) {
|
||
throw new RuntimeException('VK не вернул данные сохранённого фото');
|
||
}
|
||
|
||
$photo = $savedPhoto[0];
|
||
return 'photo' . $photo['owner_id'] . '_' . $photo['id'];
|
||
}
|
||
|
||
/**
|
||
* Post to wall/group
|
||
*
|
||
* @param int $ownerId User ID or Group ID (negative for groups)
|
||
* @param string $message Post text
|
||
* @param array $attachments Array of attachments
|
||
* @param bool $fromGroup Post from group name (only for groups)
|
||
* @return array Post info
|
||
*/
|
||
public function wallPost($ownerId, $message = '', $attachments = [], $fromGroup = true)
|
||
{
|
||
$params = [
|
||
'owner_id' => $ownerId,
|
||
'message' => $message,
|
||
];
|
||
|
||
if (!empty($attachments)) {
|
||
$params['attachments'] = implode(',', $attachments);
|
||
$params['primary_attachments_mode'] = 'grid';
|
||
}
|
||
|
||
// If posting to group, post from group name
|
||
if ($ownerId < 0 && $fromGroup) {
|
||
$params['from_group'] = 1;
|
||
}
|
||
|
||
return $this->request('wall.post', $params);
|
||
}
|
||
|
||
/**
|
||
* Post photos and text to a group
|
||
*
|
||
* @param int $groupId Group ID (will be converted to negative)
|
||
* @param array $photoUrls Array of photo URLs
|
||
* @param string $message Post text
|
||
* @return array Result
|
||
*/
|
||
public function post($groupId, $photoUrls = [], $message = '')
|
||
{
|
||
$attachments = [];
|
||
$uploadErrors = [];
|
||
$permissionError = false;
|
||
|
||
// Make sure group ID is numeric
|
||
$numericGroupId = (int)$groupId;
|
||
if ($numericGroupId > 0) {
|
||
$numericGroupId = -$numericGroupId;
|
||
}
|
||
|
||
// Try to upload each photo
|
||
foreach ($photoUrls as $url) {
|
||
try {
|
||
$attachments[] = $this->uploadPhotoFromUrl(abs($numericGroupId), $url);
|
||
} catch (Exception $e) {
|
||
$errorMsg = $e->getMessage();
|
||
$uploadErrors[] = $errorMsg;
|
||
error_log("VK photo upload failed: " . $errorMsg);
|
||
|
||
// Check if it's a permission error (error 15 = Access denied, error 27 = group auth)
|
||
if (strpos($errorMsg, 'error [15]') !== false ||
|
||
strpos($errorMsg, 'error [27]') !== false ||
|
||
strpos($errorMsg, 'Access denied') !== false ||
|
||
strpos($errorMsg, 'group auth') !== false) {
|
||
$permissionError = true;
|
||
break; // Don't try other photos if it's a permission issue
|
||
}
|
||
}
|
||
}
|
||
|
||
// If permission error, try to post with photo links in text
|
||
if ($permissionError && !empty($photoUrls)) {
|
||
$photoLinks = "\n\n📷 Фото:\n" . implode("\n", $photoUrls);
|
||
$messageWithPhotos = $message . $photoLinks;
|
||
|
||
try {
|
||
$result = $this->wallPost($numericGroupId, $messageWithPhotos, []);
|
||
$result['warning'] = 'Фото добавлены как ссылки. Community-токен не поддерживает загрузку фото - нужен пользовательский токен.';
|
||
return $result;
|
||
} catch (Exception $e) {
|
||
throw new RuntimeException('Ошибка постинга: ' . $e->getMessage() . '. Также нет прав на загрузку фото.');
|
||
}
|
||
}
|
||
|
||
// If all photos failed to upload for non-permission reasons, report the first error
|
||
if (!empty($photoUrls) && empty($attachments) && !empty($uploadErrors)) {
|
||
throw new RuntimeException('Ошибка загрузки фото: ' . $uploadErrors[0]);
|
||
}
|
||
|
||
return $this->wallPost($numericGroupId, $message, $attachments);
|
||
}
|
||
|
||
/**
|
||
* Post to multiple groups at once
|
||
*
|
||
* @param array $groupIds Array of group IDs
|
||
* @param array $photoUrls Array of photo URLs
|
||
* @param string $message Post text
|
||
* @return array Results for each group
|
||
*/
|
||
public function postToMultiple($groupIds, $photoUrls = [], $message = '')
|
||
{
|
||
$results = [];
|
||
|
||
foreach ($groupIds as $groupId) {
|
||
try {
|
||
$results[$groupId] = [
|
||
'success' => true,
|
||
'result' => $this->post($groupId, $photoUrls, $message),
|
||
];
|
||
} catch (Exception $e) {
|
||
$results[$groupId] = [
|
||
'success' => false,
|
||
'error' => $e->getMessage(),
|
||
];
|
||
}
|
||
|
||
// VK rate limit: max 3 requests per second
|
||
usleep(350000);
|
||
}
|
||
|
||
return $results;
|
||
}
|
||
|
||
/**
|
||
* Validate access token (supports both user and community tokens)
|
||
*
|
||
* @return array Validation result
|
||
*/
|
||
public function validateToken()
|
||
{
|
||
// First try user token validation
|
||
try {
|
||
$user = $this->getMe();
|
||
if (!empty($user)) {
|
||
return [
|
||
'valid' => true,
|
||
'type' => 'user',
|
||
'user_id' => $user['id'],
|
||
'user_name' => trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')),
|
||
'screen_name' => $user['screen_name'] ?? '',
|
||
];
|
||
}
|
||
} catch (Exception $e) {
|
||
// User token failed, try community token
|
||
}
|
||
|
||
// Try community token validation using groups.getById with group_id from token
|
||
try {
|
||
// For community tokens, we can get group info using groups.getById
|
||
// The token should have access to its own group
|
||
$result = $this->request('groups.getById', [
|
||
'fields' => 'name,screen_name,photo_100'
|
||
]);
|
||
|
||
if (!empty($result['groups'][0])) {
|
||
$group = $result['groups'][0];
|
||
return [
|
||
'valid' => true,
|
||
'type' => 'community',
|
||
'user_id' => '-' . $group['id'],
|
||
'user_name' => $group['name'] ?? 'Сообщество',
|
||
'screen_name' => $group['screen_name'] ?? '',
|
||
];
|
||
}
|
||
// VK API v5.199+ returns in different format
|
||
if (!empty($result[0])) {
|
||
$group = $result[0];
|
||
return [
|
||
'valid' => true,
|
||
'type' => 'community',
|
||
'user_id' => '-' . $group['id'],
|
||
'user_name' => $group['name'] ?? 'Сообщество',
|
||
'screen_name' => $group['screen_name'] ?? '',
|
||
];
|
||
}
|
||
} catch (Exception $e) {
|
||
return [
|
||
'valid' => false,
|
||
'error' => $e->getMessage(),
|
||
];
|
||
}
|
||
|
||
return ['valid' => false, 'error' => 'Invalid token'];
|
||
}
|
||
}
|