396 lines
18 KiB
PHP
396 lines
18 KiB
PHP
<?php
|
|
class WeiboPicsBridge extends BridgeAbstract
|
|
{
|
|
const MAINTAINER = 'Akamaru, Brawl, Gemini 2.5 Pro';
|
|
const NAME = 'Weibo User Pictures';
|
|
const URI = 'https://weibo.com';
|
|
const CACHE_TIMEOUT = 3600; // 1 hour for feed data
|
|
const DESCRIPTION = 'Get the latest pictures from a Weibo user.';
|
|
|
|
const PARAMETERS = [[
|
|
'uid' => [
|
|
'name' => 'User ID',
|
|
'required' => true,
|
|
'exampleValue' => '2813217452', // <3
|
|
'title' => 'The numeric user ID from Weibo'
|
|
],
|
|
]];
|
|
|
|
private const GUEST_COOKIE_CACHE_KEY_PREFIX = 'WeiboPicsBridge_Guest_'; // Prefix for cache key
|
|
private const GUEST_COOKIE_TTL = 21600; // 6 hours for guest cookies
|
|
|
|
private $feedTitleName = '';
|
|
|
|
public function getIcon()
|
|
{
|
|
return 'https://weibo.com/favicon.ico';
|
|
}
|
|
|
|
public function getName()
|
|
{
|
|
if (empty($this->feedTitleName)) {
|
|
return parent::getName();
|
|
}
|
|
return $this->feedTitleName;
|
|
}
|
|
|
|
private function getGuestCookieCacheKey()
|
|
{
|
|
// Make cache key unique per bridge instance if needed, though guest cookies are general.
|
|
// For simplicity, using a general key.
|
|
return self::GUEST_COOKIE_CACHE_KEY_PREFIX . 'cookies';
|
|
}
|
|
|
|
private function getGuestCookiesOrLogin()
|
|
{
|
|
$cacheKey = $this->getGuestCookieCacheKey();
|
|
$cachedCookies = $this->loadCacheValue($cacheKey, self::GUEST_COOKIE_TTL);
|
|
|
|
if ($cachedCookies !== null && is_array($cachedCookies) && !empty($cachedCookies)) {
|
|
$this->logger->debug('Using cached guest cookies.');
|
|
return $cachedCookies;
|
|
}
|
|
$this->logger->debug('No valid cached guest cookies found. Attempting guest login.');
|
|
|
|
// --- Step 1: genvisitor ---
|
|
$genvisitorUrl = 'https://passport.weibo.com/visitor/genvisitor';
|
|
$fpData = [
|
|
'os' => '1',
|
|
'browser' => 'Gecko109,0,0,0',
|
|
'fonts' => 'undefined',
|
|
'screenInfo' => '1920*1080*24',
|
|
'plugins' => ''
|
|
];
|
|
$postFields = http_build_query([
|
|
'cb' => 'gen_callback',
|
|
'fp' => json_encode($fpData)
|
|
]);
|
|
|
|
$genvisitorHeaders = [
|
|
'Referer: https://passport.weibo.com/visitor/visitor?entry=miniblog&page=request',
|
|
'Content-Type: application/x-www-form-urlencoded'
|
|
];
|
|
$genvisitorCurlOptions = [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $postFields
|
|
];
|
|
|
|
/** @var Response|string $genvisitorResponseObj */
|
|
$genvisitorResponseObj = getContents($genvisitorUrl, $genvisitorHeaders, $genvisitorCurlOptions, true);
|
|
|
|
if (!$genvisitorResponseObj instanceof Response) {
|
|
$this->logger->warning('genvisitor request failed to return a Response object.');
|
|
return null;
|
|
}
|
|
|
|
|
|
$genvisitorResponseBody = $genvisitorResponseObj->getBody();
|
|
|
|
if (empty($genvisitorResponseBody)) {
|
|
$this->logger->warning('genvisitor response body is empty.');
|
|
return null;
|
|
}
|
|
|
|
if (!preg_match('/gen_callback\((.*)\);/s', $genvisitorResponseBody, $matches)) {
|
|
$this->logger->warning('Could not parse genvisitor JSONP response: ' . substr($genvisitorResponseBody, 0, 200));
|
|
return null;
|
|
}
|
|
$jsonData = json_decode($matches[1], true);
|
|
|
|
if (!isset($jsonData['data']['tid']) || empty($jsonData['data']['tid'])) {
|
|
$this->logger->warning('TID not found in genvisitor response: ' . $matches[1]);
|
|
return null;
|
|
}
|
|
|
|
$tid = $jsonData['data']['tid'];
|
|
$newTid = isset($jsonData['data']['new_tid']) && $jsonData['data']['new_tid'];
|
|
$confidence = $jsonData['data']['confidence'] ?? 100;
|
|
|
|
$this->logger->debug('genvisitor successful. TID: ' . $tid);
|
|
|
|
// --- Step 2: visitor ---
|
|
$wParam = $newTid ? '3' : '2';
|
|
$cParam = sprintf('%03d', $confidence);
|
|
$randParam = mt_rand() / mt_getrandmax();
|
|
|
|
$visitorUrl = sprintf(
|
|
'https://passport.weibo.com/visitor/visitor?a=incarnate&t=%s&w=%s&c=%s&gc=&cb=cross_domain&from=weibo&_rand=%s',
|
|
urlencode($tid),
|
|
urlencode($wParam),
|
|
urlencode($cParam),
|
|
$randParam
|
|
);
|
|
|
|
$visitorHeaders = [
|
|
];
|
|
// No specific cURL options needed for this GET request beyond headers
|
|
|
|
/** @var Response|string $visitorResponseObj */
|
|
$visitorResponseObj = getContents($visitorUrl, $visitorHeaders, [], true);
|
|
|
|
if (!$visitorResponseObj instanceof Response) {
|
|
$this->logger->warning('Visitor request failed to return a Response object.');
|
|
return null;
|
|
}
|
|
|
|
if ($visitorResponseObj->getCode() >= 300) {
|
|
$this->logger->warning('Visitor request failed with HTTP code: ' . $visitorResponseObj->getCode());
|
|
return null;
|
|
}
|
|
$this->logger->debug('Visitor request successful.');
|
|
|
|
$cookiesToStore = [];
|
|
// Extract Set-Cookie headers from visitor response
|
|
$vistorHeaders = $visitorResponseObj->getHeaders();
|
|
$visitorSetCookieHeaders = $vistorHeaders['set-cookie'] ?? [];
|
|
if (is_array($visitorSetCookieHeaders)) {
|
|
foreach ($visitorSetCookieHeaders as $headerValue) {
|
|
if (preg_match('/^([^=]+=[^;]+)/i', $headerValue, $cookieMatch)) {
|
|
$cookiesToStore[] = $cookieMatch[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check cookies potentially set by genvisitor call if any were relevant
|
|
$genvisitorHeaders = $genvisitorResponseObj->getHeaders();
|
|
$genvisitorSetCookieHeaders = $genvisitorHeaders['set-cookie'] ?? [];
|
|
if (is_array($genvisitorSetCookieHeaders)) {
|
|
foreach ($genvisitorSetCookieHeaders as $headerValue) {
|
|
if (preg_match('/^([^=]+=[^;]+)/i', $headerValue, $cookieMatch)) {
|
|
// Avoid duplicates if same cookie name was set by both
|
|
$isDuplicate = false;
|
|
$newCookieName = explode('=', $cookieMatch[1])[0];
|
|
foreach($cookiesToStore as $existingCookie) {
|
|
if (explode('=', $existingCookie)[0] === $newCookieName) {
|
|
$isDuplicate = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$isDuplicate) {
|
|
$cookiesToStore[] = $cookieMatch[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (empty($cookiesToStore)) {
|
|
$this->logger->warning('No cookies were set by the visitor login flow.');
|
|
return null;
|
|
}
|
|
|
|
$this->saveCacheValue($cacheKey, $cookiesToStore, self::GUEST_COOKIE_TTL);
|
|
$this->logger->info('Guest cookies obtained and cached: ' . implode('; ', $cookiesToStore));
|
|
return $cookiesToStore;
|
|
}
|
|
|
|
public function collectData()
|
|
{
|
|
$uid = $this->getInput('uid');
|
|
if (!is_numeric($uid)) {
|
|
returnClientError('User ID must be numeric.');
|
|
}
|
|
|
|
$guestCookiesArray = $this->getGuestCookiesOrLogin();
|
|
if ($guestCookiesArray === null) {
|
|
returnServerError('Failed to obtain Weibo guest cookies. The API might be inaccessible or login failed.');
|
|
}
|
|
|
|
$commonRequestHeaders = [
|
|
'X-Requested-With: XMLHttpRequest',
|
|
'Referer: https://weibo.com/u/' . $uid,
|
|
'Cookie: ' . implode('; ', $guestCookiesArray)
|
|
];
|
|
|
|
$imageWallUrl = 'https://weibo.com/ajax/profile/getImageWall?uid=' . $uid . '&sinceid=0&has_album=true';
|
|
|
|
$maxAttempts = 3;
|
|
$attempt = 0;
|
|
$imageWallResponseObj = null; // Initialize
|
|
|
|
do {
|
|
$attempt++;
|
|
if ($attempt > 1) {
|
|
$this->logger->debug("Retrying getImageWall for UID {$uid}, attempt {$attempt}/{$maxAttempts}.");
|
|
// Optional: consider a small delay, e.g., sleep(1);
|
|
}
|
|
/** @var Response|string|false $currentAttemptResponseObj */
|
|
$currentAttemptResponseObj = getContents($imageWallUrl, $commonRequestHeaders, [], true);
|
|
|
|
if ($currentAttemptResponseObj instanceof Response) {
|
|
$httpCode = $currentAttemptResponseObj->getCode();
|
|
if ($httpCode === 500) {
|
|
$imageWallResponseObj = $currentAttemptResponseObj; // Store the 500 response for now
|
|
if ($attempt < $maxAttempts) {
|
|
$this->logger->info("Weibo getImageWall API for UID {$uid} returned HTTP 500 on attempt {$attempt}/{$maxAttempts}. Retrying...");
|
|
continue; // Go to next attempt
|
|
}
|
|
// Max attempts reached for 500, loop will terminate, and this 500 response will be handled below
|
|
$this->logger->warning("Weibo getImageWall API for UID {$uid} still returned HTTP 500 after {$maxAttempts} attempts.");
|
|
break;
|
|
}
|
|
// Not a 500 error, so this is our final response from the loop
|
|
$imageWallResponseObj = $currentAttemptResponseObj;
|
|
break;
|
|
} elseif ($currentAttemptResponseObj === false || $currentAttemptResponseObj === null) {
|
|
// Critical getContents failure
|
|
$imageWallResponseObj = $currentAttemptResponseObj; // Store false/null
|
|
break;
|
|
} else {
|
|
// Unexpected response type from getContents
|
|
$imageWallResponseObj = $currentAttemptResponseObj; // Store unexpected type
|
|
break;
|
|
}
|
|
} while ($attempt < $maxAttempts);
|
|
|
|
$imageWallJson = null;
|
|
// Default to empty, successful-like structure for imageWallData
|
|
$imageWallData = ['ok' => 1, 'data' => ['list' => [], 'user' => []]];
|
|
|
|
if ($imageWallResponseObj instanceof Response) {
|
|
$httpCode = $imageWallResponseObj->getCode();
|
|
if ($httpCode === 500) { // This means all retries (if any) resulted in 500
|
|
$this->logger->info('Weibo getImageWall API for UID ' . $uid . ' returned HTTP 500 (after all retries). Proceeding with empty data for image wall.');
|
|
// $imageWallData is already set to an empty valid structure, $imageWallJson remains null.
|
|
} elseif ($httpCode >= 300) { // Other HTTP errors (excluding 500 which is handled above)
|
|
returnServerError('Weibo getImageWall API request failed for UID ' . $uid . '. HTTP Code: ' . $httpCode . '. Body: ' . substr($imageWallResponseObj->getBody(), 0, 200));
|
|
} else { // Successful response (2xx)
|
|
$imageWallJson = $imageWallResponseObj->getBody();
|
|
$decodedData = json_decode($imageWallJson, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
returnServerError('Failed to decode JSON from image wall for UID ' . $uid . ': ' . json_last_error_msg() . ' Response: ' . substr($imageWallJson, 0, 500));
|
|
}
|
|
|
|
if (!isset($decodedData['ok']) || $decodedData['ok'] != 1) {
|
|
$msg = $decodedData['msg'] ?? 'Unknown error from Weibo getImageWall API.';
|
|
if (stripos($msg, 'login') !== false || ($imageWallJson && stripos($imageWallJson, 'login.sina.com.cn') !== false)) {
|
|
$this->logger->warning('Weibo API redirected to login for getImageWall, UID ' . $uid . '. Guest cookies might be invalid. Clearing cache.');
|
|
$this->saveCacheValue($this->getGuestCookieCacheKey(), null, 0); // Invalidate cache
|
|
returnServerError('Weibo getImageWall API requires login for UID ' . $uid . '. Guest session failed or expired. Message: ' . $msg);
|
|
}
|
|
returnServerError('Invalid response from Weibo getImageWall API (after 2xx) for UID ' . $uid . ': ' . $msg . ' Raw: ' . substr($imageWallJson, 0, 200));
|
|
}
|
|
$imageWallData = $decodedData; // Use the successfully decoded data
|
|
}
|
|
} elseif ($imageWallResponseObj === false || $imageWallResponseObj === null) {
|
|
// getContents itself failed critically (e.g., cURL error, DNS issue)
|
|
returnServerError('Could not request image wall (getContents failed critically) for UID ' . $uid . ': ' . $imageWallUrl);
|
|
} else {
|
|
// Should not happen if getContents with true as 4th param behaves as expected (Response or false/null)
|
|
$this->logger->warning('Unexpected response type from getContents for imageWall, UID ' . $uid . ': ' . gettype($imageWallResponseObj));
|
|
returnServerError('Unexpected response from getContents for imageWall API for UID ' . $uid);
|
|
}
|
|
|
|
$postsToFetchDetails = [];
|
|
if (isset($imageWallData['data']['list']) && is_array($imageWallData['data']['list'])) {
|
|
$processedMids = [];
|
|
foreach ($imageWallData['data']['list'] as $postSummary) {
|
|
if (isset($postSummary['mid']) && !empty($postSummary['mid'])) {
|
|
if (!in_array($postSummary['mid'], $processedMids)) {
|
|
$processedMids[] = $postSummary['mid'];
|
|
$postsToFetchDetails[] = $postSummary['mid'];
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$this->logger->debug('Image wall list is not an array or not set, or empty. Response: ' . $imageWallJson);
|
|
}
|
|
|
|
if (empty($postsToFetchDetails)) {
|
|
$this->logger->debug('No posts found in image wall for UID: ' . $uid);
|
|
}
|
|
|
|
$feedTitleName = 'User ' . $uid;
|
|
if (isset($imageWallData['data']['user']['screen_name'])) {
|
|
$feedTitleName = $imageWallData['data']['user']['screen_name'];
|
|
} elseif (isset($imageWallData['data']['list'][0]['user']['screen_name'])) { // If list not empty
|
|
$feedTitleName = $imageWallData['data']['list'][0]['user']['screen_name'];
|
|
} else {
|
|
$userInfoUrl = 'https://weibo.com/ajax/profile/info?uid=' . $uid;
|
|
$userInfoJson = getContents($userInfoUrl, $commonRequestHeaders);
|
|
if ($userInfoJson) {
|
|
$userInfoData = json_decode($userInfoJson, true);
|
|
if (isset($userInfoData['data']['user']['screen_name'])) {
|
|
$feedTitleName = $userInfoData['data']['user']['screen_name'];
|
|
}
|
|
}
|
|
}
|
|
$this->feedTitleName = $feedTitleName . ' - Weibo Pictures';
|
|
|
|
foreach (array_slice($postsToFetchDetails, 0, 15) as $mid) {
|
|
$postDetailUrl = 'https://weibo.com/ajax/statuses/show?id=' . $mid;
|
|
$postJson = getContents($postDetailUrl, $commonRequestHeaders);
|
|
|
|
if ($postJson === false || $postJson === null) {
|
|
$this->logger->warning('Could not request post details for MID: ' . $mid);
|
|
continue;
|
|
}
|
|
|
|
$postData = json_decode($postJson, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
$this->logger->warning('Failed to decode JSON for post MID ' . $mid . ': ' . json_last_error_msg() . ' Raw: ' . substr($postJson, 0, 200));
|
|
continue;
|
|
}
|
|
|
|
if (!isset($postData['ok']) || $postData['ok'] != 1) {
|
|
$this->logger->warning('Post detail API error for MID ' . $mid . ': ' . ($postData['msg'] ?? 'Unknown error'));
|
|
continue;
|
|
}
|
|
|
|
$mblogid = $postData['mblogid'] ?? $mid;
|
|
$postUser = $postData['user'] ?? null;
|
|
$postUid = $postUser['idstr'] ?? $uid;
|
|
$postLink = 'https://weibo.com/' . $postUid . '/' . $mblogid;
|
|
|
|
$titleText = $postData['text_raw'] ?? 'Weibo Post ' . $mid;
|
|
$title = mb_strlen($titleText) > 70 ? mb_substr($titleText, 0, 70, 'UTF-8') . '...' : $titleText;
|
|
if (empty(trim($title))) $title = 'Weibo Image Post ' . $mid;
|
|
|
|
$htmlContent = '<p>' . nl2br(htmlspecialchars($postData['text_raw'] ?? '')) . '</p>';
|
|
$timestamp = isset($postData['created_at']) ? strtotime($postData['created_at']) : time();
|
|
|
|
$imageHtml = '';
|
|
if (isset($postData['pic_ids']) && is_array($postData['pic_ids']) && isset($postData['pic_infos']) && is_array($postData['pic_infos'])) {
|
|
foreach ($postData['pic_ids'] as $pic_id) {
|
|
if (isset($postData['pic_infos'][$pic_id]['largest']['url'])) {
|
|
$imageUrl = $postData['pic_infos'][$pic_id]['largest']['url'];
|
|
if (strpos($imageUrl, 'http:') === 0) $imageUrl = 'https:' . substr($imageUrl, 5);
|
|
$imageHtml .= '<figure><img src="' . htmlspecialchars($imageUrl) . '" /><figcaption><a href="' . htmlspecialchars($imageUrl) . '">' . htmlspecialchars($imageUrl) . '</a></figcaption></figure>';
|
|
}
|
|
}
|
|
} elseif (isset($postData['page_info']['type'])) {
|
|
$pageInfo = $postData['page_info'];
|
|
$imgUrlKey = null;
|
|
if (isset($pageInfo['page_pic']['url'])) $imgUrlKey = $pageInfo['page_pic']['url'];
|
|
elseif (isset($pageInfo['media_info']['video_poster'])) $imgUrlKey = $pageInfo['media_info']['video_poster'];
|
|
|
|
if ($imgUrlKey) {
|
|
$imageUrl = $imgUrlKey;
|
|
if (strpos($imageUrl, 'http:') === 0) $imageUrl = 'https:' . substr($imageUrl, 5);
|
|
$imageHtml .= '<figure><img src="' . htmlspecialchars($imageUrl) . '" /><figcaption>Cover Image: <a href="' . htmlspecialchars($imageUrl) . '">' . htmlspecialchars($imageUrl) . '</a></figcaption></figure>';
|
|
}
|
|
}
|
|
|
|
if (empty($imageHtml)) {
|
|
$this->logger->debug('Post MID ' . $mid . ' had no extractable images in detail view.');
|
|
continue;
|
|
}
|
|
$htmlContent .= $imageHtml;
|
|
|
|
$item = [];
|
|
$item['uri'] = $postLink;
|
|
$item['title'] = $title;
|
|
$item['content'] = $htmlContent;
|
|
$item['timestamp'] = $timestamp;
|
|
$item['uid'] = $mid;
|
|
$item['author'] = $postUser['screen_name'] ?? $feedTitleName;
|
|
|
|
$this->items[] = $item;
|
|
|
|
if (count($this->items) >= 10) break;
|
|
}
|
|
}
|
|
}
|