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
+258
View File
@@ -0,0 +1,258 @@
<?php
/**
* Simple but secure authentication system
* Compatible with PHP 5.6+ (maximum compatibility)
*/
class Auth
{
private $configFile;
private $config;
private $maxAttempts = 5;
private $lockoutTime = 900;
private $passwordAlgo;
public function __construct($configFile = null)
{
if ($configFile === null) {
$this->configFile = __DIR__ . '/../auth_config.php';
} else {
$this->configFile = $configFile;
}
// Use Argon2ID if available, fallback to bcrypt
if (defined('PASSWORD_ARGON2ID')) {
$this->passwordAlgo = PASSWORD_ARGON2ID;
} else {
$this->passwordAlgo = PASSWORD_BCRYPT;
}
$this->loadConfig();
}
private function loadConfig()
{
if (file_exists($this->configFile)) {
$this->config = require $this->configFile;
} else {
$this->config = array(
'users' => array(),
'failed_attempts' => array(),
);
}
}
private function saveConfig()
{
$content = "<?php\nreturn " . var_export($this->config, true) . ";\n";
file_put_contents($this->configFile, $content);
@chmod($this->configFile, 0600);
}
public function createUser($username, $password)
{
if (isset($this->config['users'][$username])) {
return false;
}
$this->config['users'][$username] = array(
'password_hash' => $this->hashPassword($password),
'created_at' => time(),
);
$this->saveConfig();
return true;
}
public function changePassword($username, $oldPassword, $newPassword)
{
if (!$this->verifyPassword($username, $oldPassword)) {
return false;
}
$this->config['users'][$username]['password_hash'] = $this->hashPassword($newPassword);
$this->saveConfig();
return true;
}
private function hashPassword($password)
{
if ($this->passwordAlgo === PASSWORD_ARGON2ID) {
return password_hash($password, PASSWORD_ARGON2ID, array(
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 1,
));
}
return password_hash($password, PASSWORD_BCRYPT, array('cost' => 12));
}
private function verifyPassword($username, $password)
{
if (!isset($this->config['users'][$username])) {
$this->hashPassword($password);
return false;
}
return password_verify($password, $this->config['users'][$username]['password_hash']);
}
private function isLockedOut($ip)
{
if (!isset($this->config['failed_attempts'][$ip])) {
return false;
}
$attempts = $this->config['failed_attempts'][$ip];
$lockoutTime = $this->lockoutTime;
$filtered = array();
foreach ($attempts as $time) {
if ($time > time() - $lockoutTime) {
$filtered[] = $time;
}
}
$this->config['failed_attempts'][$ip] = $filtered;
return count($filtered) >= $this->maxAttempts;
}
private function recordFailedAttempt($ip)
{
if (!isset($this->config['failed_attempts'][$ip])) {
$this->config['failed_attempts'][$ip] = array();
}
$this->config['failed_attempts'][$ip][] = time();
$this->saveConfig();
}
private function clearFailedAttempts($ip)
{
unset($this->config['failed_attempts'][$ip]);
$this->saveConfig();
}
public function login($username, $password, $ip)
{
if ($this->isLockedOut($ip)) {
return array(
'success' => false,
'message' => 'Too many failed attempts. Please try again later.',
'locked' => true,
);
}
if ($this->verifyPassword($username, $password)) {
$this->clearFailedAttempts($ip);
$token = $this->generateSessionToken();
return array(
'success' => true,
'message' => 'Login successful',
'token' => $token,
'username' => $username,
);
}
$this->recordFailedAttempt($ip);
$attempts = isset($this->config['failed_attempts'][$ip]) ? $this->config['failed_attempts'][$ip] : array();
$remaining = $this->maxAttempts - count($attempts);
return array(
'success' => false,
'message' => "Invalid username or password. {$remaining} attempts remaining.",
'locked' => false,
);
}
private function generateSessionToken()
{
if (function_exists('random_bytes')) {
return bin2hex(random_bytes(32));
}
return bin2hex(openssl_random_pseudo_bytes(32));
}
public function startSession($username, $token)
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $username;
$_SESSION['token'] = $token;
$_SESSION['login_time'] = time();
$_SESSION['last_activity'] = time();
}
public function isAuthenticated($timeout = 3600)
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['authenticated'])) {
return false;
}
$lastActivity = isset($_SESSION['last_activity']) ? $_SESSION['last_activity'] : 0;
if (time() - $lastActivity > $timeout) {
$this->logout();
return false;
}
$_SESSION['last_activity'] = time();
return true;
}
public function logout()
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION = array();
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time() - 3600, '/');
}
session_destroy();
}
public function getCurrentUser()
{
if (!$this->isAuthenticated()) {
return null;
}
return isset($_SESSION['username']) ? $_SESSION['username'] : null;
}
public function hasUsers()
{
return !empty($this->config['users']);
}
public static function getClientIP()
{
$headers = array('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR');
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
$ip = $_SERVER[$header];
if (strpos($ip, ',') !== false) {
$parts = explode(',', $ip);
$ip = trim($parts[0]);
}
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return '0.0.0.0';
}
}
+395
View File
@@ -0,0 +1,395 @@
<?php
/**
* Flickr API Client - fetches photos from your Flickr account
* Compatible with PHP 7.2+
* Supports OAuth for accessing original quality photos
*/
class FlickrAPI
{
private $apiKey;
private $apiSecret;
private $userId;
private $baseUrl = 'https://api.flickr.com/services/rest/';
private $oauth = null;
public function __construct($apiKey, $apiSecret, $userId = '')
{
$this->apiKey = $apiKey;
$this->apiSecret = $apiSecret;
$this->userId = $userId;
}
/**
* Set OAuth handler for authenticated requests
*/
public function setOAuth($oauth)
{
$this->oauth = $oauth;
}
/**
* Check if OAuth is available
*/
public function hasOAuth()
{
return $this->oauth !== null && $this->oauth->isAuthorized();
}
/**
* Set user ID
*/
public function setUserId($userId)
{
$this->userId = $userId;
}
/**
* Make API request (with OAuth if available)
*
* @param string $method Flickr API method
* @param array $params Additional parameters
* @param bool $useOAuth Force OAuth for this request
* @return array Response data
*/
private function request($method, $params = [], $useOAuth = false)
{
$params = array_merge([
'method' => $method,
'api_key' => $this->apiKey,
'format' => 'json',
'nojsoncallback' => 1,
], $params);
// Use OAuth if available and requested
if (($useOAuth || $this->hasOAuth()) && $this->oauth !== null) {
return $this->requestWithOAuth($method, $params);
}
$url = $this->baseUrl . '?' . http_build_query($params);
$context = stream_context_create([
'http' => [
'timeout' => 30,
'user_agent' => 'VH_Posting_System/1.0',
],
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
throw new RuntimeException('Failed to connect to Flickr API');
}
$data = json_decode($response, true);
if ($data === null) {
throw new RuntimeException('Invalid JSON response from Flickr API');
}
if (isset($data['stat']) && $data['stat'] === 'fail') {
throw new RuntimeException('Flickr API error: ' . (isset($data['message']) ? $data['message'] : 'Unknown error'));
}
return $data;
}
/**
* Make OAuth-signed API request
*/
private function requestWithOAuth($method, $params)
{
$params['method'] = $method;
$params['format'] = 'json';
$params['nojsoncallback'] = 1;
$oauthParams = $this->oauth->signRequest('GET', $this->baseUrl, $params);
$allParams = array_merge($params, $oauthParams);
$url = $this->baseUrl . '?' . http_build_query($allParams);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
throw new RuntimeException('Failed to connect to Flickr API (OAuth)');
}
$data = json_decode($response, true);
if ($data === null) {
throw new RuntimeException('Invalid JSON response from Flickr API');
}
if (isset($data['stat']) && $data['stat'] === 'fail') {
throw new RuntimeException('Flickr API error: ' . (isset($data['message']) ? $data['message'] : 'Unknown error'));
}
return $data;
}
/**
* Get user's photostream
*
* @param int $page Page number
* @param int $perPage Photos per page (max 500)
* @return array Photos data
*/
public function getPhotos($page = 1, $perPage = 50)
{
$response = $this->request('flickr.people.getPhotos', [
'user_id' => $this->userId ? $this->userId : 'me',
'page' => $page,
'per_page' => $perPage,
'extras' => 'url_sq,url_t,url_s,url_m,url_z,url_l,url_k,url_o,description,date_upload,date_taken,owner_name,original_format',
]);
return $this->normalizePhotosResponse(isset($response['photos']) ? $response['photos'] : []);
}
/**
* Get user's photosets (albums)
*
* @param int $page Page number
* @param int $perPage Albums per page
* @return array Photosets data with pagination info
*/
public function getPhotosets($page = 1, $perPage = 50)
{
$response = $this->request('flickr.photosets.getList', [
'user_id' => $this->userId,
'page' => $page,
'per_page' => $perPage,
'primary_photo_extras' => 'url_sq,url_t,url_s,url_m',
]);
$photosets = isset($response['photosets']) ? $response['photosets'] : [];
return [
'albums' => isset($photosets['photoset']) ? $photosets['photoset'] : [],
'page' => (int)($photosets['page'] ?? $page),
'pages' => (int)($photosets['pages'] ?? 1),
'perpage' => (int)($photosets['perpage'] ?? $perPage),
'total' => (int)($photosets['total'] ?? 0),
];
}
/**
* Get photos from a specific photoset (album)
*
* @param string $photosetId Photoset ID
* @param int $page Page number
* @param int $perPage Photos per page
* @return array Photos data
*/
public function getPhotosetPhotos($photosetId, $page = 1, $perPage = 50)
{
$response = $this->request('flickr.photosets.getPhotos', [
'photoset_id' => $photosetId,
'user_id' => $this->userId,
'page' => $page,
'per_page' => $perPage,
'extras' => 'url_sq,url_t,url_s,url_m,url_z,url_l,url_k,url_o,description,date_upload,date_taken,original_format,media,path_alias,owner_name',
]);
// Get owner info from photoset response
$ownername = isset($response['photoset']['ownername']) ? $response['photoset']['ownername'] : '';
$owner = isset($response['photoset']['owner']) ? $response['photoset']['owner'] : $this->userId;
return $this->normalizePhotosResponse(isset($response['photoset']) ? $response['photoset'] : [], $ownername, $owner);
}
/**
* Get info about a specific photo
*
* @param string $photoId Photo ID
* @return array Photo info
*/
public function getPhotoInfo($photoId)
{
$response = $this->request('flickr.photos.getInfo', [
'photo_id' => $photoId,
]);
return isset($response['photo']) ? $response['photo'] : [];
}
/**
* Get available sizes for a photo (uses OAuth if available)
*
* @param string $photoId Photo ID
* @return array Available sizes
*/
public function getPhotoSizes($photoId)
{
// getSizes requires OAuth to return Original for private/restricted photos
$response = $this->request('flickr.photos.getSizes', [
'photo_id' => $photoId,
], true); // Force OAuth if available
$sizes = [];
$sizeList = isset($response['sizes']['size']) ? $response['sizes']['size'] : [];
foreach ($sizeList as $size) {
$sizes[$size['label']] = [
'url' => $size['source'],
'width' => (int)$size['width'],
'height' => (int)$size['height'],
];
}
return $sizes;
}
/**
* Get original URL for a photo (requires OAuth)
*
* @param string $photoId Photo ID
* @return string|null Original URL or null if not available
*/
public function getOriginalUrl($photoId)
{
try {
$sizes = $this->getPhotoSizes($photoId);
// Try Original first, then fall back to largest available
if (isset($sizes['Original'])) {
return $sizes['Original']['url'];
}
if (isset($sizes['Large 2048'])) {
return $sizes['Large 2048']['url'];
}
if (isset($sizes['Large 1600'])) {
return $sizes['Large 1600']['url'];
}
if (isset($sizes['Large'])) {
return $sizes['Large']['url'];
}
return null;
} catch (Exception $e) {
return null;
}
}
/**
* Search user's photos
*
* @param string $query Search query
* @param int $page Page number
* @param int $perPage Photos per page
* @return array Photos data
*/
public function searchPhotos($query, $page = 1, $perPage = 50)
{
$response = $this->request('flickr.photos.search', [
'user_id' => $this->userId ? $this->userId : 'me',
'text' => $query,
'page' => $page,
'per_page' => $perPage,
'extras' => 'url_sq,url_t,url_s,url_m,url_z,url_l,url_k,url_o,description,date_upload,date_taken,original_format',
]);
return $this->normalizePhotosResponse(isset($response['photos']) ? $response['photos'] : []);
}
/**
* Find user ID by username
*
* @param string $username Flickr username
* @return string User ID
*/
public function findUserByUsername($username)
{
$response = $this->request('flickr.people.findByUsername', [
'username' => $username,
]);
return isset($response['user']['nsid']) ? $response['user']['nsid'] : '';
}
/**
* Normalize photos response to consistent format
*/
private function normalizePhotosResponse($response, $defaultOwnerName = '', $defaultOwner = '')
{
$photos = [];
$photoList = isset($response['photo']) ? $response['photo'] : [];
foreach ($photoList as $photo) {
$farm = isset($photo['farm']) ? $photo['farm'] : '';
$server = $photo['server'];
$id = $photo['id'];
$originalSecret = isset($photo['originalsecret']) ? $photo['originalsecret'] : $photo['secret'];
$originalFormat = isset($photo['originalformat']) ? $photo['originalformat'] : 'jpg';
// Get original URL - ONLY use if API returns it
// If url_o is not returned, originals are blocked by Flickr privacy settings
$originalUrl = isset($photo['url_o']) ? $photo['url_o'] : null;
// Get large 2048 URL (url_k) - from API or construct it
// This is the best quality available when originals are blocked
$large2048Url = isset($photo['url_k']) ? $photo['url_k'] : null;
// Construct large2048 URL if not provided (usually works)
if (!$large2048Url && $server) {
$large2048Url = "https://live.staticflickr.com/{$server}/{$id}_{$photo['secret']}_k.jpg";
}
// Determine media type (photo or video)
$mediaType = isset($photo['media']) ? $photo['media'] : 'photo';
$isVideo = ($mediaType === 'video');
// Build page URL - use path_alias, ownername, or owner NSID
$pathAlias = isset($photo['pathalias']) && $photo['pathalias'] ? $photo['pathalias'] : '';
$ownerName = isset($photo['ownername']) ? $photo['ownername'] : $defaultOwnerName;
$owner = isset($photo['owner']) ? $photo['owner'] : ($defaultOwner ? $defaultOwner : $this->userId);
// Prefer path_alias, then ownername, then owner NSID (URL encoded)
$userPath = $pathAlias ? $pathAlias : ($ownerName ? $ownerName : rawurlencode($owner));
$pageUrl = "https://www.flickr.com/photos/{$userPath}/{$id}/";
$photos[] = [
'id' => $id,
'secret' => $photo['secret'],
'server' => $server,
'farm' => $farm,
'title' => isset($photo['title']) ? $photo['title'] : 'Untitled',
'description' => isset($photo['description']['_content']) ? $photo['description']['_content'] : '',
'date_upload' => isset($photo['dateupload']) ? $photo['dateupload'] : '',
'date_taken' => isset($photo['datetaken']) ? $photo['datetaken'] : '',
'original_format' => $originalFormat,
'original_secret' => $originalSecret,
'media' => $mediaType,
'is_video' => $isVideo,
'urls' => [
'square' => isset($photo['url_sq']) ? $photo['url_sq'] : null,
'thumbnail' => isset($photo['url_t']) ? $photo['url_t'] : null,
'small' => isset($photo['url_s']) ? $photo['url_s'] : null,
'medium' => isset($photo['url_m']) ? $photo['url_m'] : null,
'medium640' => isset($photo['url_z']) ? $photo['url_z'] : null,
'large' => isset($photo['url_l']) ? $photo['url_l'] : null,
'large2048' => $large2048Url,
'original' => $originalUrl,
],
'page_url' => $pageUrl,
];
}
return [
'photos' => $photos,
'page' => (int)(isset($response['page']) ? $response['page'] : 1),
'pages' => (int)(isset($response['pages']) ? $response['pages'] : 1),
'perpage' => (int)(isset($response['perpage']) ? $response['perpage'] : count($photos)),
'total' => (int)(isset($response['total']) ? $response['total'] : count($photos)),
];
}
}
+272
View File
@@ -0,0 +1,272 @@
<?php
/**
* Flickr OAuth 1.0a Authentication
* Handles authorization flow to get access tokens for API calls
*/
class FlickrOAuth
{
private $consumerKey;
private $consumerSecret;
private $requestTokenUrl = 'https://www.flickr.com/services/oauth/request_token';
private $authorizeUrl = 'https://www.flickr.com/services/oauth/authorize';
private $accessTokenUrl = 'https://www.flickr.com/services/oauth/access_token';
private $oauthToken = null;
private $oauthTokenSecret = null;
private $tokenFile;
public function __construct($consumerKey, $consumerSecret)
{
$this->consumerKey = $consumerKey;
$this->consumerSecret = $consumerSecret;
$this->tokenFile = __DIR__ . '/../data/oauth_token.json';
// Load saved tokens if exist
$this->loadTokens();
}
/**
* Check if we have valid access tokens
*/
public function isAuthorized()
{
return !empty($this->oauthToken) && !empty($this->oauthTokenSecret);
}
/**
* Get stored OAuth token
*/
public function getOAuthToken()
{
return $this->oauthToken;
}
/**
* Get stored OAuth token secret
*/
public function getOAuthTokenSecret()
{
return $this->oauthTokenSecret;
}
/**
* Step 1: Get request token and return authorization URL
*/
public function getAuthorizationUrl($callbackUrl)
{
$params = [
'oauth_callback' => $callbackUrl,
'oauth_consumer_key' => $this->consumerKey,
'oauth_nonce' => $this->generateNonce(),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
];
$params['oauth_signature'] = $this->generateSignature('GET', $this->requestTokenUrl, $params);
$url = $this->requestTokenUrl . '?' . http_build_query($params);
$response = $this->httpRequest($url);
if (!$response) {
throw new Exception('Failed to get request token');
}
parse_str($response, $data);
if (!isset($data['oauth_token']) || !isset($data['oauth_token_secret'])) {
throw new Exception('Invalid request token response: ' . $response);
}
// Store request token temporarily (needed for step 2)
$_SESSION['flickr_request_token'] = $data['oauth_token'];
$_SESSION['flickr_request_token_secret'] = $data['oauth_token_secret'];
// Return URL for user to authorize
return $this->authorizeUrl . '?oauth_token=' . $data['oauth_token'] . '&perms=read';
}
/**
* Step 2: Exchange verifier for access token
*/
public function handleCallback($oauthToken, $oauthVerifier)
{
if (!isset($_SESSION['flickr_request_token_secret'])) {
throw new Exception('Request token secret not found in session');
}
$requestTokenSecret = $_SESSION['flickr_request_token_secret'];
$params = [
'oauth_consumer_key' => $this->consumerKey,
'oauth_token' => $oauthToken,
'oauth_verifier' => $oauthVerifier,
'oauth_nonce' => $this->generateNonce(),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
];
$params['oauth_signature'] = $this->generateSignature(
'GET',
$this->accessTokenUrl,
$params,
$requestTokenSecret
);
$url = $this->accessTokenUrl . '?' . http_build_query($params);
$response = $this->httpRequest($url);
if (!$response) {
throw new Exception('Failed to get access token');
}
parse_str($response, $data);
if (!isset($data['oauth_token']) || !isset($data['oauth_token_secret'])) {
throw new Exception('Invalid access token response: ' . $response);
}
// Save access tokens
$this->oauthToken = $data['oauth_token'];
$this->oauthTokenSecret = $data['oauth_token_secret'];
$this->saveTokens($data);
// Clean up session
unset($_SESSION['flickr_request_token']);
unset($_SESSION['flickr_request_token_secret']);
return [
'oauth_token' => $this->oauthToken,
'user_nsid' => $data['user_nsid'] ?? null,
'username' => $data['username'] ?? null,
'fullname' => $data['fullname'] ?? null,
];
}
/**
* Sign an API request with OAuth
*/
public function signRequest($method, $url, $params = [])
{
$oauthParams = [
'oauth_consumer_key' => $this->consumerKey,
'oauth_token' => $this->oauthToken,
'oauth_nonce' => $this->generateNonce(),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
];
$allParams = array_merge($params, $oauthParams);
$oauthParams['oauth_signature'] = $this->generateSignature(
$method,
$url,
$allParams,
$this->oauthTokenSecret
);
return $oauthParams;
}
/**
* Generate OAuth signature
*/
private function generateSignature($method, $url, $params, $tokenSecret = '')
{
ksort($params);
$paramString = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$baseString = strtoupper($method) . '&'
. rawurlencode($url) . '&'
. rawurlencode($paramString);
$signingKey = rawurlencode($this->consumerSecret) . '&' . rawurlencode($tokenSecret);
return base64_encode(hash_hmac('sha1', $baseString, $signingKey, true));
}
/**
* Generate random nonce
*/
private function generateNonce()
{
return md5(uniqid(mt_rand(), true));
}
/**
* Make HTTP request
*/
private function httpRequest($url)
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return false;
}
return $response;
}
/**
* Save tokens to file
*/
private function saveTokens($data)
{
$dir = dirname($this->tokenFile);
if (!is_dir($dir)) {
mkdir($dir, 0700, true);
}
$tokenData = [
'oauth_token' => $data['oauth_token'],
'oauth_token_secret' => $data['oauth_token_secret'],
'user_nsid' => $data['user_nsid'] ?? null,
'username' => $data['username'] ?? null,
'created_at' => date('Y-m-d H:i:s'),
];
file_put_contents($this->tokenFile, json_encode($tokenData, JSON_PRETTY_PRINT));
chmod($this->tokenFile, 0600);
}
/**
* Load tokens from file
*/
private function loadTokens()
{
if (file_exists($this->tokenFile)) {
$data = json_decode(file_get_contents($this->tokenFile), true);
if ($data) {
$this->oauthToken = $data['oauth_token'] ?? null;
$this->oauthTokenSecret = $data['oauth_token_secret'] ?? null;
}
}
}
/**
* Clear saved tokens (logout)
*/
public function clearTokens()
{
$this->oauthToken = null;
$this->oauthTokenSecret = null;
if (file_exists($this->tokenFile)) {
unlink($this->tokenFile);
}
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
/**
* Flickr URL Parser - extracts photo info from various Flickr URL formats
* Compatible with PHP 7.2+
*/
class FlickrParser
{
/**
* Flickr image size suffixes
* https://www.flickr.com/services/api/misc.urls.html
*/
const SIZES = [
'Square' => '_s', // 75x75
'LargeSquare' => '_q', // 150x150
'Thumbnail' => '_t', // 100 on longest side
'Small' => '_m', // 240 on longest side
'Small320' => '_n', // 320 on longest side
'Medium' => '', // 500 on longest side
'Medium640' => '_z', // 640 on longest side
'Medium800' => '_c', // 800 on longest side
'Large' => '_b', // 1024 on longest side
'Large1600' => '_h', // 1600 on longest side
'Large2048' => '_k', // 2048 on longest side
'Original' => '_o', // original image
];
/**
* Parse a Flickr URL and extract photo information
*
* @param string $url Flickr URL
* @return array|null Photo info or null if not a valid Flickr URL
*/
public function parse($url)
{
$url = trim($url);
// Direct image URL
if (preg_match('/staticflickr\.com\/\d+\/(\d+)_([a-f0-9]+)(_[a-z])?\.(\w+)/i', $url, $matches)) {
return [
'photo_id' => $matches[1],
'secret' => $matches[2],
'size_suffix' => isset($matches[3]) ? $matches[3] : '',
'format' => $matches[4],
'type' => 'direct',
'original_url' => $url,
];
}
// Photo page URL
if (preg_match('/flickr\.com\/photos\/[^\/]+\/(\d+)/i', $url, $matches)) {
return [
'photo_id' => $matches[1],
'type' => 'page',
'original_url' => $url,
];
}
// Short URL (flic.kr)
if (preg_match('/flic\.kr\/p\/([a-zA-Z0-9]+)/i', $url, $matches)) {
$photoId = $this->decodeBase58($matches[1]);
return [
'photo_id' => $photoId,
'type' => 'short',
'original_url' => $url,
];
}
return null;
}
/**
* Parse multiple URLs (one per line or comma-separated)
*
* @param string $input Input text with URLs
* @return array Array of parsed photo info
*/
public function parseMultiple($input)
{
$results = [];
// Split by newlines and commas
$lines = preg_split('/[\r\n,]+/', $input);
foreach ($lines as $line) {
$url = trim($line);
if (empty($url)) continue;
$parsed = $this->parse($url);
if ($parsed) {
$results[] = $parsed;
}
}
return $results;
}
/**
* Build a direct image URL from photo info
*
* @param array $photoInfo Photo information
* @param string $size Size name from SIZES constant
* @return string Direct image URL
*/
public function buildImageUrl($photoInfo, $size = 'Large')
{
$suffix = isset(self::SIZES[$size]) ? self::SIZES[$size] : self::SIZES['Large'];
$server = isset($photoInfo['server']) ? $photoInfo['server'] : '65535';
$photoId = $photoInfo['photo_id'];
$secret = isset($photoInfo['secret']) ? $photoInfo['secret'] : (isset($photoInfo['originalsecret']) ? $photoInfo['originalsecret'] : '');
$format = isset($photoInfo['format']) ? $photoInfo['format'] : (isset($photoInfo['originalformat']) ? $photoInfo['originalformat'] : 'jpg');
// For original size, use originalsecret if available
if ($size === 'Original' && isset($photoInfo['originalsecret'])) {
$secret = $photoInfo['originalsecret'];
$format = isset($photoInfo['originalformat']) ? $photoInfo['originalformat'] : 'jpg';
}
return "https://live.staticflickr.com/{$server}/{$photoId}_{$secret}{$suffix}.{$format}";
}
/**
* Decode Flickr's base58 short URL to photo ID
*
* @param string $encoded Base58 encoded string
* @return string Photo ID
*/
private function decodeBase58($encoded)
{
$alphabet = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
$base = strlen($alphabet);
$decoded = '0';
for ($i = 0; $i < strlen($encoded); $i++) {
$pos = strpos($alphabet, $encoded[$i]);
$decoded = bcmul($decoded, (string)$base);
$decoded = bcadd($decoded, (string)$pos);
}
return $decoded;
}
/**
* Get available sizes
*
* @return array Size names
*/
public function getAvailableSizes()
{
return array_keys(self::SIZES);
}
}
+200
View File
@@ -0,0 +1,200 @@
<?php
/**
* Format Generator - converts image URLs to various posting formats
* Compatible with PHP 7.2+
*/
class FormatGenerator
{
/**
* Predefined format templates
*/
private $formats = [
'bbcode' => [
'name' => 'BBCode',
'description' => 'Standard forum BBCode',
'template' => '[img]{url}[/img]',
'separator' => "\n",
],
'bbcode_linked' => [
'name' => 'BBCode (clickable)',
'description' => 'BBCode with link to original',
'template' => '[url={original}][img]{url}[/img][/url]',
'separator' => "\n",
],
'html' => [
'name' => 'HTML',
'description' => 'HTML img tag',
'template' => '<img src="{url}" alt="{title}">',
'separator' => "\n",
],
'html_linked' => [
'name' => 'HTML (clickable)',
'description' => 'HTML img with link to original',
'template' => '<a href="{original}" target="_blank"><img src="{url}" alt="{title}"></a>',
'separator' => "\n",
],
'html_figure' => [
'name' => 'HTML Figure',
'description' => 'HTML5 figure with caption',
'template' => "<figure>\n <a href=\"{original}\" target=\"_blank\"><img src=\"{url}\" alt=\"{title}\"></a>\n <figcaption>{title}</figcaption>\n</figure>",
'separator' => "\n\n",
],
'markdown' => [
'name' => 'Markdown',
'description' => 'Markdown image syntax',
'template' => '![{title}]({url})',
'separator' => "\n",
],
'markdown_linked' => [
'name' => 'Markdown (clickable)',
'description' => 'Markdown with link to original',
'template' => '[![{title}]({url})]({original})',
'separator' => "\n",
],
'url_only' => [
'name' => 'URL only',
'description' => 'Just the image URL',
'template' => '{url}',
'separator' => "\n",
],
'url_list' => [
'name' => 'URL list (comma)',
'description' => 'Comma-separated URLs',
'template' => '{url}',
'separator' => ', ',
],
// ============ FORUM PRESETS ============
'bjdclub' => [
'name' => 'BJDClub.ru',
'description' => 'Оптимизировано для BJDClub (phpBB)',
'template' => '[url={original}][img]{url}[/img][/url]',
'separator' => "\n\n",
'category' => 'forum',
],
'babiki' => [
'name' => 'Babiki.ru',
'description' => 'Оптимизировано для Бэбиков',
'template' => '<a href="{original}" target="_blank"><img src="{url}" alt="{title}"></a>',
'separator' => "\n\n",
'category' => 'forum',
],
'babiki_simple' => [
'name' => 'Babiki.ru (простой)',
'description' => 'Только картинки для Бэбиков',
'template' => '<img src="{url}" alt="{title}">',
'separator' => "\n",
'category' => 'forum',
],
'doll_forum' => [
'name' => 'Кукольный форум',
'description' => 'Универсальный BBCode для кукольных форумов',
'template' => '[url={original}][img]{url}[/img][/url]',
'separator' => "\n",
'category' => 'forum',
],
];
/**
* Add a custom format
*
* @param string $key Format key
* @param string $name Display name
* @param string $template Template with placeholders
* @param string $separator Separator between multiple images
* @param string $description Format description
*/
public function addFormat($key, $name, $template, $separator = "\n", $description = '')
{
$this->formats[$key] = [
'name' => $name,
'description' => $description,
'template' => $template,
'separator' => $separator,
];
}
/**
* Get all available formats
*
* @return array Format definitions
*/
public function getFormats()
{
return $this->formats;
}
/**
* Generate formatted output for a single image
*
* @param string $formatKey Format key
* @param array $imageData Image data with url, original, title
* @return string Formatted output
*/
public function generate($formatKey, $imageData)
{
if (!isset($this->formats[$formatKey])) {
throw new InvalidArgumentException("Unknown format: {$formatKey}");
}
$template = $this->formats[$formatKey]['template'];
$replacements = [
'{url}' => isset($imageData['url']) ? $imageData['url'] : '',
'{original}' => isset($imageData['original']) ? $imageData['original'] : (isset($imageData['url']) ? $imageData['url'] : ''),
'{title}' => htmlspecialchars(isset($imageData['title']) ? $imageData['title'] : 'Image', ENT_QUOTES),
'{description}' => htmlspecialchars(isset($imageData['description']) ? $imageData['description'] : '', ENT_QUOTES),
'{photo_id}' => isset($imageData['photo_id']) ? $imageData['photo_id'] : '',
'{width}' => isset($imageData['width']) ? $imageData['width'] : '',
'{height}' => isset($imageData['height']) ? $imageData['height'] : '',
];
return str_replace(array_keys($replacements), array_values($replacements), $template);
}
/**
* Generate formatted output for multiple images
*
* @param string $formatKey Format key
* @param array $images Array of image data
* @return string Formatted output
*/
public function generateMultiple($formatKey, $images)
{
if (!isset($this->formats[$formatKey])) {
throw new InvalidArgumentException("Unknown format: {$formatKey}");
}
$separator = $this->formats[$formatKey]['separator'];
$outputs = [];
foreach ($images as $imageData) {
$outputs[] = $this->generate($formatKey, $imageData);
}
return implode($separator, $outputs);
}
/**
* Generate output in all formats at once
*
* @param array $images Array of image data
* @return array Keyed by format key
*/
public function generateAll($images)
{
$result = [];
foreach (array_keys($this->formats) as $formatKey) {
$result[$formatKey] = [
'name' => $this->formats[$formatKey]['name'],
'description' => $this->formats[$formatKey]['description'],
'output' => $this->generateMultiple($formatKey, $images),
];
}
return $result;
}
}
+283
View File
@@ -0,0 +1,283 @@
<?php
/**
* Telegram Bot API client for posting images and text
* Compatible with PHP 7.2+
*/
class TelegramBot
{
private $botToken;
private $baseUrl = 'https://api.telegram.org/bot';
private $defaultChannels = [];
public function __construct($botToken)
{
$this->botToken = $botToken;
}
/**
* Set default channels for posting
*
* @param array $channels Array of channel usernames or IDs
*/
public function setDefaultChannels($channels)
{
$this->defaultChannels = $channels;
}
/**
* Make API request to Telegram
*
* @param string $method API method
* @param array $params Parameters
* @return array Response
*/
private function request($method, $params = [])
{
$url = $this->baseUrl . $this->botToken . '/' . $method;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $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("Telegram API error: {$error}");
}
$data = json_decode($response, true);
if (!$data['ok']) {
throw new RuntimeException("Telegram API error: " . (isset($data['description']) ? $data['description'] : 'Unknown error'));
}
return isset($data['result']) ? $data['result'] : [];
}
/**
* Send a text message
*
* @param string $chatId Chat/Channel ID or username
* @param string $text Message text
* @param string $parseMode Parse mode (HTML, Markdown, MarkdownV2)
* @param bool $disablePreview Disable link preview
* @return array Message info
*/
public function sendMessage($chatId, $text, $parseMode = 'HTML', $disablePreview = false)
{
return $this->request('sendMessage', [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => $parseMode,
'disable_web_page_preview' => $disablePreview,
]);
}
/**
* Send a single photo
*
* @param string $chatId Chat/Channel ID
* @param string $photo Photo URL or file_id
* @param string $caption Photo caption
* @param string $parseMode Parse mode for caption
* @return array Message info
*/
public function sendPhoto($chatId, $photo, $caption = '', $parseMode = 'HTML')
{
$params = [
'chat_id' => $chatId,
'photo' => $photo,
];
if ($caption) {
$params['caption'] = $caption;
$params['parse_mode'] = $parseMode;
}
return $this->request('sendPhoto', $params);
}
/**
* Send multiple photos as media group (album)
*
* @param string $chatId Chat/Channel ID
* @param array $photos Array of photo URLs
* @param string $caption Caption for first photo
* @param string $parseMode Parse mode
* @return array Messages info
*/
public function sendMediaGroup($chatId, $photos, $caption = '', $parseMode = 'HTML')
{
$media = [];
foreach ($photos as $index => $photo) {
$item = [
'type' => 'photo',
'media' => $photo,
];
// Caption only on first photo
if ($index === 0 && $caption) {
$item['caption'] = $caption;
$item['parse_mode'] = $parseMode;
}
$media[] = $item;
}
return $this->request('sendMediaGroup', [
'chat_id' => $chatId,
'media' => json_encode($media),
]);
}
/**
* Post photos with text to a channel
* Smart method that chooses best approach based on content
*
* @param string $chatId Chat/Channel ID
* @param array $photos Array of photo URLs
* @param string $text Post text
* @param string $parseMode Parse mode
* @return array Result info
*/
public function post($chatId, $photos, $text = '', $parseMode = 'HTML')
{
$photoCount = count($photos);
// Text only
if ($photoCount === 0) {
return ['type' => 'text', 'message' => $this->sendMessage($chatId, $text, $parseMode)];
}
// Single photo
if ($photoCount === 1) {
return ['type' => 'photo', 'message' => $this->sendPhoto($chatId, $photos[0], $text, $parseMode)];
}
// Multiple photos (2-10) - use media group
if ($photoCount <= 10) {
return ['type' => 'album', 'messages' => $this->sendMediaGroup($chatId, $photos, $text, $parseMode)];
}
// More than 10 photos - split into multiple albums
$results = [];
$chunks = array_chunk($photos, 10);
foreach ($chunks as $index => $chunk) {
$caption = ($index === 0) ? $text : '';
$results[] = $this->sendMediaGroup($chatId, $chunk, $caption, $parseMode);
}
return ['type' => 'multiple_albums', 'messages' => $results];
}
/**
* Post to multiple channels at once
*
* @param array $chatIds Array of Chat/Channel IDs
* @param array $photos Array of photo URLs
* @param string $text Post text
* @param string $parseMode Parse mode
* @return array Results for each channel
*/
public function postToMultiple($chatIds, $photos, $text = '', $parseMode = 'HTML')
{
$results = [];
foreach ($chatIds as $chatId) {
try {
$results[$chatId] = [
'success' => true,
'result' => $this->post($chatId, $photos, $text, $parseMode),
];
} catch (Exception $e) {
$results[$chatId] = [
'success' => false,
'error' => $e->getMessage(),
];
}
}
return $results;
}
/**
* Get bot info
*
* @return array Bot info
*/
public function getMe()
{
return $this->request('getMe');
}
/**
* Get chat info
*
* @param string $chatId Chat/Channel ID
* @return array Chat info
*/
public function getChat($chatId)
{
return $this->request('getChat', ['chat_id' => $chatId]);
}
/**
* Validate that bot has access to a channel
*
* @param string $chatId Channel ID or username
* @return array Validation result
*/
public function validateChannel($chatId)
{
try {
$chat = $this->getChat($chatId);
return [
'valid' => true,
'title' => isset($chat['title']) ? $chat['title'] : (isset($chat['username']) ? $chat['username'] : $chatId),
'type' => $chat['type'],
];
} catch (Exception $e) {
return [
'valid' => false,
'error' => $e->getMessage(),
];
}
}
/**
* Format text for Telegram HTML mode
*
* @param string $text Plain text
* @return string Escaped HTML
*/
public static function escapeHtml($text)
{
return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* Format text for Telegram MarkdownV2 mode
*
* @param string $text Plain text
* @return string Escaped Markdown
*/
public static function escapeMarkdown($text)
{
$chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
foreach ($chars as $char) {
$text = str_replace($char, '\\' . $char, $text);
}
return $text;
}
}
+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'];
}
}