mailn
This commit is contained in:
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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' => '',
|
||||
'separator' => "\n",
|
||||
],
|
||||
'markdown_linked' => [
|
||||
'name' => 'Markdown (clickable)',
|
||||
'description' => 'Markdown with link to original',
|
||||
'template' => '[]({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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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