diff --git a/CHANGELOG.md b/CHANGELOG.md index 7654217..66ba513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Release Notes for Instagram Feed Plugin +## 2.2.0 - 2024-01-04 + +> {note} Instagram has changed the data structure. If you no longer get a feed, this update should fix the problem. + +### Changed + +- Adaptation to the recent changes in the data structure of Instagram. + ## 2.1.0 - 2022-10-21 ### Added diff --git a/composer.json b/composer.json index 71c731f..0c76e3a 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "codemonauts/craft-instagram-feed", "description": "Craft CMS plugin to receive Instagram feed data as variable in templates.", - "version": "2.1.0", + "version": "2.2.0", "type": "craft-plugin", "keywords": [ "craft", diff --git a/src/services/InstagramService.php b/src/services/InstagramService.php index e243c4f..f7bdd0c 100644 --- a/src/services/InstagramService.php +++ b/src/services/InstagramService.php @@ -9,6 +9,7 @@ use craft\helpers\FileHelper; use Exception; use GuzzleHttp\Client; +use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\GuzzleException; use Throwable; use yii\base\InvalidConfigException; @@ -106,14 +107,12 @@ public function getFeed(string $accountOrTag = null): array * Fetches the feed from the public Instagram profile page. * * @param string $account The account name to fetch. - * * @return array * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \craft\errors\SiteNotFoundException */ private function getInstagramAccountData(string $account): array { - $html = $this->fetchInstagramPage($account . '/'); + $html = $this->fetchInstagramAccount($account); if (null === $html) { Craft::error('Instagram profile data could not be fetched.', 'instagramfeed'); @@ -121,16 +120,9 @@ private function getInstagramAccountData(string $account): array return []; } - if ($this->canUseProxy()) { - $obj = $this->parseProxyResponse($html); - if (false === $obj) { - return []; - } - } else { - $obj = $this->parseInstagramResponse($html); - if (empty($obj)) { - return []; - } + $obj = $this->parseProxyResponse($html); + if (false === $obj) { + return []; } return $this->extractMedia($obj); @@ -140,17 +132,12 @@ private function getInstagramAccountData(string $account): array * Fetches the feed from the public Instagram tag page. * * @param string $tag The tag name to fetch. - * * @return array * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \craft\errors\SiteNotFoundException */ private function getInstagramTagData(string $tag): array { - $tag = substr($tag, 1); - - $path = sprintf('explore/tags/%s/', $tag); - $html = $this->fetchInstagramPage($path); + $html = $this->fetchInstagramTag($tag); if (null === $html) { Craft::error('Instagram tag data could not be fetched.', 'instagramfeed'); @@ -158,35 +145,27 @@ private function getInstagramTagData(string $tag): array return []; } - if ($this->canUseProxy()) { - $obj = $this->parseProxyResponse($html); - if (false === $obj) { - return []; - } - } else { - $obj = $this->parseInstagramResponse($html); - if (empty($obj)) { - return []; - } + $obj = $this->parseProxyResponse($html); + if (false === $obj) { + return []; } return $this->extractMedia($obj); } /** - * Fetches the page from a given URL - * - * @param string $path The path to fetch + * Fetches an account from Instagram. * + * @param string $account The account name to fetch. * @return string|null * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \craft\errors\SiteNotFoundException */ - private function fetchInstagramPage(string $path): ?string + private function fetchInstagramAccount(string $account): ?string { - $url = 'https://www.instagram.com/' . $path; - - $client = new Client(); + $cookies = new CookieJar(); + $client = new Client([ + 'cookies' => $cookies, + ]); $guzzleOptions = [ 'timeout' => InstagramFeed::$settings->timeout, @@ -196,16 +175,24 @@ private function fetchInstagramPage(string $path): ?string ], ]; - if ($this->canUseProxy()) { - $url = 'https://igproxy.codemonauts.com/' . $path; - $referer = Craft::$app->getSites()->getCurrentSite()->getBaseUrl(); - $guzzleOptions['headers']['Authorization'] = InstagramFeed::$settings->proxyKey; - $guzzleOptions['headers']['Referer'] = $referer; - $guzzleOptions['headers']['X-Plugin-Version'] = InstagramFeed::$plugin->getVersion(); - } - try { - $response = $client->get($url, $guzzleOptions); + if ($this->canUseProxy()) { + $url = sprintf('https://igproxy.codemonauts.com/%s/', $account); + $referer = Craft::$app->getSites()->getCurrentSite()->getBaseUrl(); + $guzzleOptions['headers']['Authorization'] = InstagramFeed::$settings->proxyKey; + $guzzleOptions['headers']['Referer'] = $referer; + $guzzleOptions['headers']['X-Plugin-Version'] = InstagramFeed::$plugin->getVersion(); + $response = $client->get($url, $guzzleOptions); + } else { + $url = sprintf('https://www.instagram.com/%s/', $account); + $client->get($url, $guzzleOptions); + $guzzleOptions['headers']['Referer'] = $url; + $guzzleOptions['headers']['X-IG-App-ID'] = '936619743392459'; + $guzzleOptions['headers']['X-Requested-With'] = 'XMLHttpRequest'; + $guzzleOptions['headers']['Origin'] = 'https://www.instagram.com'; + $url = sprintf('https://www.instagram.com/api/v1/users/web_profile_info/?username=%s', $account); + $response = $client->get($url, $guzzleOptions); + } } catch (Exception $e) { Craft::error('Error fetching page: ' . $e->getMessage(), 'instagramfeed'); @@ -216,51 +203,70 @@ private function fetchInstagramPage(string $path): ?string } /** - * Function to parse the response body from the proxy. + * Fetches a tag from Instagram. * - * @param string $response - * - * @return mixed + * @param string $tag The tag to fetch. + * @return string|null + * @throws \GuzzleHttp\Exception\GuzzleException */ - private function parseProxyResponse(string $response): mixed + private function fetchInstagramTag(string $tag): ?string { - if (InstagramFeed::$settings->dump) { - $this->dumpResponse($response); + $tag = substr($tag, 1); + + $cookies = new CookieJar(); + $client = new Client([ + 'cookies' => $cookies, + ]); + + $guzzleOptions = [ + 'timeout' => InstagramFeed::$settings->timeout, + 'headers' => [ + 'Accept-Language' => 'en-US;q=0.9,en;q=0.8', + 'User-Agent' => self::DEFAULT_USER_AGENT, + ], + ]; + + try { + if ($this->canUseProxy()) { + $url = sprintf('https://igproxy.codemonauts.com/explore/tags/%s/', $tag); + $referer = Craft::$app->getSites()->getCurrentSite()->getBaseUrl(); + $guzzleOptions['headers']['Authorization'] = InstagramFeed::$settings->proxyKey; + $guzzleOptions['headers']['Referer'] = $referer; + $guzzleOptions['headers']['X-Plugin-Version'] = InstagramFeed::$plugin->getVersion(); + $response = $client->get($url, $guzzleOptions); + } else { + $url = sprintf('https://www.instagram.com/explore/tags/%s/', $tag); + $client->get($url, $guzzleOptions); + $guzzleOptions['headers']['Referer'] = $url; + $guzzleOptions['headers']['X-IG-App-ID'] = '936619743392459'; + $guzzleOptions['headers']['X-Requested-With'] = 'XMLHttpRequest'; + $guzzleOptions['headers']['Origin'] = 'https://www.instagram.com'; + $url = sprintf('https://www.instagram.com/api/v1/tags/logged_out_web_info/?tag_name=%s', $tag); + $response = $client->get($url, $guzzleOptions); + } + } catch (Exception $e) { + Craft::error('Error fetching page: ' . $e->getMessage(), 'instagramfeed'); + + return null; } - return json_decode($response, true); + return (string)$response->getBody(); } /** - * Function to parse the response body from Instagram + * Function to parse the response body from the proxy. * - * @param string $response Response body from Instagram + * @param string $response * - * @return array + * @return mixed */ - private function parseInstagramResponse(string $response): array + private function parseProxyResponse(string $response): mixed { - $arr = explode('window._sharedData = ', $response); - - if (!isset($arr[1])) { - // Check if Instagram returned a statement and not a valid page - $statement = json_decode($response, false); - if (isset($statement->errors)) { - Craft::error('Instagram responsed with an error: ' . implode(' ', $statement->errors->error), 'instagramfeed'); - } else { - Craft::error('Unknown response from Instagram. Please check debug output in devMode.', 'instagramfeed'); - } - - $this->dumpResponse($response); - return []; - } - if (InstagramFeed::$settings->dump) { $this->dumpResponse($response); } - $arr = explode(';', $arr[1]); - return json_decode($arr[0], true); + return json_decode($response, true); } /**