mailn
This commit is contained in:
@@ -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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user