Drupal 8 - Instagram GraphQL + Twig

By kenneth, Sun, 01/10/2021 - 17:15

Authored on

Sun, 01/10/2021 - 17:15
Image

 


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: postman Instagram GET response

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: GraphQL IDE - HTTP GET

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:
Instagram GraphQL formatter

As soon as we create a new content and fill out the field with a valid Instagram Account:
Instagram GraphQL valid account

Once you save it the you can see it as shown below:
Instagram GraphQL display

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).

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.