This commit is contained in:
zuevav
2026-04-30 15:14:09 +03:00
parent 08fe53fa5c
commit e5a88665cd
25 changed files with 13697 additions and 0 deletions
+403
View File
@@ -0,0 +1,403 @@
<?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'];
}
}