DISCLAIMER: This article describes few unofficial ways to consume data from Instagram. Uses it under your own risk. I suggest to read the oficial documentation at Instagram Platform
The idea behind this article is to demonstrate how to use custom parameters in GraphQL Twig
Few months ago I had problems to retrieve information from an Instagram account, after I debugged what was going on I found this warning: The remaining Instagram Legacy API permission ("Basic Permission") was disabled on June 29, 2020.
While I was researching about how to retrieve the feeds again I ended up in this thread: Is this www.instagram.com/username/?__a=1 is officially and allowed? where it looks like it's possible to retrieve the same feeds by consiming the Instagram page and appending `__a=1` as a query string.
After I tested then I noticed it worked fine:
Since I was researching about GraphQL module and its possible implementation, then I realized here I could create an alternative way to retrieve the feeds by combining the GraphQL Twig and GraphQL JSON modules.
I decided to create a new `@FieldFormatter` plugin where I can have a twig template and using GraphQL I would consume the URL by using an HTTP GET resquest. To verify I was able to do so, then I installed the three contributed modules I mentioned above.
Once those modules were installed, I jumped into the GraphiQL interface at `/graphql/explorer` to check if I was able to consume the JSON data, guess what, It was possible:
I noticed that I was able to send the "url" as a parameter to the query in order to retrieve the JSON data I was looking for. So I was ready to create the new custom module.
instagram_graphql
instagram_graphql.info.yml
I configured the new custom module and I was sure the dependencies was defined there.
name: Instagram GraphQL
type: module
description: 'Expose Instagram plugin formatter by GraphQL.'
package: KEBOCA
core: 8.x
dependencies:
- graphql_twig
- graphql_json
instagram_graphql.module
The must challeging piece here was to find out how to send variables into Twig template to be recognized by GraphQL in runtime. I didn't find documentation about how to do it, then I started to debug the code and I found "GraphQLTemplateTrait::display" is getting the context arguments by using "graphql_arguments" key from the theme definition.
Then TO MAKE IT WORKS we need to include "graphql_arguments" as a valid argument into our variable for the new theme definition:
<?php
/**
* @file Contains Instagram GraphQL module.
*/
/**
* Implements hook_theme().
*/
function instagram_graphql_theme($existing, $type, $theme, $path) {
return [
'instagram_graphql' => [
'variables' => [
'graphql_arguments' => [],
],
],
];
}
templates/instagram-graphql.html.twig
Let's summary what's happening here:
- I wrote the same query I build into the GraphQL Explorer
- To make "$url" a valid parameter for GraphQL then I have to map it inside the "#graphql_arguments" key where I am using the drupal theme I made early (Details on next section).
- Next I retrieved the result from GraphQL by extracting them from "graphql.route.request.json.media"
- Walk-through the items to display an image tag plus a description.
- The "sharedData" variable is another alternative to retrieve the same values.
{#graphql
query ($url: String!) {
route(path: $url) {
... on ExternalUrl {
request {
json {
... on JsonObject {
media:path(steps: ["graphql", "user", "edge_owner_to_timeline_media", "edges"]) {
... on JsonList {
items {
... on JsonObject {
owner:path(steps: ["node", "owner", "username"]) {
... on JsonLeaf {
value
}
},
url:path(steps:["node", "display_url"]) {
... on JsonLeaf {
value
}
},
caption:path(steps:["node", "accessibility_caption"]) {
... on JsonLeaf {
value
}
},
description:path(steps:["node", "edge_media_to_caption", "edges", "0", "node", "text"]) {
... on JsonLeaf {
value
}
}
}
}
}
}
}
}
}
}
}
}
#}
{% set media = graphql.route.request.json.media %}
<div{{ content_attributes.addClass('content') }}>
{% for item in media.items %}
<blockquote>
<pre>{{ dump(sharedData[loop.index]) }}</pre>
<img src="{{ item.url.value }}" alt="{{ item.caption.value }}"
class="instagram-graphql instagram-{{ item.owner.value }}">
<p>{{ item.description.value }}</p>
</blockquote>
{% endfor %}
</div>
src/Plugin/Field/FieldFormatter/InstagramGraphQLFormatter.php
To complete the task I created the formatter plugin to allow any string field type to display the GraphQL template, it defines the template on each item. However the magic happens here because I need to create the "#graphql_arguments" key as an array where I can inject the "url" value to be used on GraphQL Twig.
Besides I am including another variable named: "sharedData" which's another alternative to consume the same URL but by using raw PHP instead (if something goes wrong, we need a plan B).
<?php
namespace Drupal\instagram_graphql\Plugin\Field\FieldFormatter;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Plugin implementation of the 'Instagram GraphQL' formatter.
*
* @FieldFormatter(
* id = "instagram_graphql_formatter",
* label = @Translation("Instagram GraphQL"),
* field_types = {
* "string"
* }
* )
*/
class InstagramGraphQLFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$element = [];
foreach ($items as $delta => $item) {
$element[$delta] = [
'#theme' => 'instagram_graphql',
'#graphql_arguments' => [
'url' => "https://www.instagram.com/{$item->value}/?__a=1",
'sharedData' => $this->getSharedData($item->value),
],
];
}
return $element;
}
/**
* Another fancy way to extract the data from Instagram.
*
* @param string $username
*
* @return mixed
*/
protected function getSharedData($username) {
$url = sprintf("https://www.instagram.com/$username");
$content = file_get_contents($url);
$content = explode("window._sharedData = ", $content)[1];
$content = explode(";</script>", $content)[0];
$data = json_decode($content, true);
$rawData = NestedArray::getValue($data, [
'entry_data',
'ProfilePage',
0,
'graphql',
'user',
'edge_owner_to_timeline_media',
'edges',
]);
return array_map(function($item) {
return [
'owner' => NestedArray::getValue($item, [
'node',
'owner',
'username',
]),
'url' => NestedArray::getValue($item, [
'node',
'display_url',
]),
'caption' => NestedArray::getValue($item, [
'node',
'accessibility_caption',
]),
'description' => NestedArray::getValue($item, [
'node',
'edge_media_to_caption',
'edges',
0,
'node',
'text',
]),
];
}, $rawData ?? []);
}
}
It should be enough to retrieve the feeds from Instagram while we mix different technologies: GraphQL, Twig and PHP. At the end, we can configure a text field to use our new formatter:
As soon as we create a new content and fill out the field with a valid Instagram Account:
Once you save it the you can see it as shown below:
As an example there are only two records display in the screenshot, but you may notice the "dump" with details from "sharedData" variable first, then the image tag and a description. From here, we can give the style we need to our feeds.
Hope, it helps someone else out there in the wild!
Happy coding!
PD: I created a repository with everything together. Here you may see the custom module here. To the grand finale I added an eastern egg on the last commit (Plan C by using Javascript).
Comments