Fulfilling Search with Drupal on IoT Devices

Submitted by fullcolorcoder on Sat, 03/25/2017 - 05:01

There is always a gap between information and users and with IoT devices being all the rage, developers face a new set of challenges. But with the powers of Drupal and a little bit of play, anything is possible. This post is supplimental to the embeded video which covers how to connect your Drupal site to the Google Home and Amazon Alexa. I finally got this talk online, bare with me I’m a little new to doing screencasts but I’m having fun. Hopefully you find the talk helpful and can build off of these ideas with Drupal!

Preliminary Info

You’ll want to have accounts setup and grasp the overall concepts first. I highy suggest watching the videos for API.AI as it really broadens your horizons and helps you get through the documentation quicker. 

API.AI Getting Started

https://docs.api.ai/docs/get-started
https://docs.api.ai/docs/videos

Amazon Skills Getting Started

https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/getting-started-guide

The Setup

Docker4Drupal

Docker4Drupal has been amazing for me. There is a small hurdle of getting Unison going, but once you get that taken care of it should be smooth sailing. 

https://github.com/wodby/docker4drupal/ - Git project page
http://docs.docker4drupal.org/en/latest/ - Documentation 

Helpful Docker4Drupal Commands 

// Start your stack (Invokes docker-compose up)
docker-sync-stack start
// Start ALL over, if you're having issues with Unison 
docker-sync-stack clean
// Standard docker start 
docker-compose up -d
// Ctl+C works if you use the docker-sync-stack but use 
// this if it fails to stop or stops oddly too quickly
// the stack should break down and not all at once. 
docker-compose stop
// Another way to remove volumes and containers
docker-compose down
// How to get Drush kicking off 
docker-compose exec php drush status
docker-compose exec php drush ws —tail 

My Configuration Files 

docker-compose.yml

version: "2"

services:
  mariadb:
    image: wodby/drupal-mariadb:1.0.0
    environment:
      MYSQL_RANDOM_ROOT_PASSWORD: 1
      MYSQL_DATABASE: drupal
      MYSQL_USER: drupal
      MYSQL_PASSWORD: drupal
#    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci # The simple way to override the mariadb config.
#    volumes:
#      - ./mariadb-init:/docker-entrypoint-initdb.d # Place init .sql file(s) here.

  php:
    image: wodby/drupal-php:7.0-1.0.0
#    image: wodby/drupal-php:5.6-1.0.0
    environment:
      PHP_SITE_NAME: dev
      PHP_HOST_NAME: localhost:7000
#      PHP_DOCROOT: web # Relative path inside the /var/www/html/ directory.
      PHP_SENDMAIL_PATH: /usr/sbin/sendmail -t -i -S mailhog:1025
      # PHP_XDEBUG_ENABLED: 1
      # PHP_XDEBUG_AUTOSTART: 1
      # PHP_XDEBUG_REMOTE_CONNECT_BACK: 0         # This is needed to respect remote.host setting bellow
      # PHP_XDEBUG_REMOTE_HOST: "10.254.254.254"  # You will also need to 'sudo ifconfig lo0 alias 10.254.254.254'
    volumes:
#      - ./:/var/www/html
      - d4d-unison-sync:/var/www/html:rw # Replace volume to this to use docker-sync for macOS users

  nginx:
    image: wodby/drupal-nginx:1.10-1.1.0
    environment:
      NGINX_SERVER_NAME: localhost
      NGINX_UPSTREAM_NAME: php
#      NGINX_DOCROOT: web # Relative path inside the /var/www/html/ directory.
      DRUPAL_VERSION: 8 # Allowed: 7, 8.
    volumes_from:
      - php
    ports:
      - "7000:80"

  pma:
    image: phpmyadmin/phpmyadmin
    environment:
      PMA_HOST: mariadb
      PMA_USER: drupal
      PMA_PASSWORD: drupal
      PHP_UPLOAD_MAX_FILESIZE: 1G
      PHP_MAX_INPUT_VARS: 1G
    ports:
     - "7001:80"

  mailhog:
    image: mailhog/mailhog
    ports:
      - "7002:8025"

#  redis:
#    image: redis:3.2-alpine

#  memcached:
#    image: memcached:1.4-alpine

#  memcached-admin:
#    image: phynias/phpmemcachedadmin
#    ports:
#      - "8006:80"

  solr:
    image: wodby/solr:5.5-1.0.0
    environment:
      SOLR_HEAP: 1024m
    ports:
      - "7003:8983"

#  varnish:
#    image: wodby/drupal-varnish:1.0.0
#    depends_on:
#      - nginx
#    environment:
#      VARNISH_SECRET: secret
#      VARNISH_BACKEND_HOST: nginx
#      VARNISH_BACKEND_PORT: 80
#      VARNISH_MEMORY_SIZE: 256M
#      VARNISH_STORAGE_SIZE: 1024M
#    ports:
#      - "8004:6081" # HTTP Proxy
#      - "8005:6082" # Control terminal

volumes:
  d4d-unison-sync:
   external: true

docker-sync.yml

syncs:
  d4d-unison-sync:
    src: './'
    dest: '/var/www/html/'
    sync_args: '-prefer newer'
    sync_user: 'www-data'
    sync_userid: '82'
    sync_host_port: '10871'
    sync_strategy: 'unison'
    sync_excludes: ['.gitignore', '.git/', '.idea/']

Drupal 8 and Modules 

I’m running on Drupal 8.2.7 in this demo. Below are links to the modules you’ll need to download and info on enabling. 

Alexa & Alexa Demo 

https://www.drupal.org/project/alexa

Follow the instructions at https://www.drupal.org/node/2701403 when installing this module. Be sure to note there are Composer dependancies, so don’t just go enabling it without running the below command:

composer require jakubsuchy/amazon-alexa-php

Search API 

https://www.drupal.org/project/search_api

Search API Solr

https://www.drupal.org/project/search_api_solr

Make sure to run the following commands before enabling the module:

composer config repositories.drupal composer https://packages.drupal.org/8
composer require drupal/search_api_solr

Core Modules

Enable the following modules that are already part of Drupal 8 core: 

  • REST UI
  • RESTful Web Services
  • HAL
  • Serlization 

Additional Code for Node Solution 

API.AI Webhook Sample

https://github.com/api-ai/apiai-webhook-sample

Commands: 

npm install 
npm start

Ngrok 

https://ngrok.com/

Once you download it, simply run the following command to start it up while in the same directory as the script:

// These can be whatever ports you are using, this is what I used for my solution. 
./ngrok http 7000
./ngrok http 8010

Apache Solr 

You’ll need to create a core to use with Drupal. Some helpful commands: 

// Create a core - you can call it whatever you want
docker-compose exec solr solr-create-core my-core
// Reload your core
docker-compose exec solr solr-create-core my-core reload
// Delete 
docker-compose exec solr solr-create-core my-core delete

Code 

Alexa Solution 

I simply am plunging into the Alexa Demo app, at /modules/alexa/alexa_demo/src/EventSubscriber/RequestSubscriber.php 

<?php

namespace Drupal\alexa_demo\EventSubscriber;

use Drupal\alexa\AlexaEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * An event subscriber for Alexa request events.
 */
class RequestSubscriber implements EventSubscriberInterface {

  /**
   * Gets the event.
   */
  public static function getSubscribedEvents() {
    $events['alexaevent.request'][] = array('onRequest', 0);
    return $events;
  }

  /**
   * Called upon a request event.
   *
   * @param \Drupal\alexa\AlexaEvent $event
   *   The event object.
   */
  public function onRequest(AlexaEvent $event) {
    $request = $event->getRequest();
    $response = $event->getResponse();

    // Sanity Checks
    file_put_contents('ZZZ-Request.txt', print_r($request, true));
    file_put_contents('ZZZ-Response.txt', print_r($response, true));


    switch ($request->intentName) {
      case 'AMAZON.HelpIntent':
        $response->respond('You can ask anything and I will respond with "Hello I am Brads Drupal Install"');
        break;

        case 'HelloDrupal':
          $response->respond('Drupal says hi.');
        break;

        case 'SearchDrupal':
        $search_term = $request->slots['SearchTerm'];
        $url = 'https://YOURURL.ngrok.io/products-rest/' . $search_term;
        $data = array('keys' => $search_term);
        $options = array(
                'http' => array(
                'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
                'method'  => 'GET',
                'content' => http_build_query($data),
            )
        );
        $context  = stream_context_create($options);
        $drupalData = file_get_contents($url, false, $context);
        $data = json_decode($drupalData, true);

        // Sanity Check 
        file_put_contents('ZZZ-Drupal-Rest.txt', print_r($context, true));

        $response_cost = $data[0]['field_price'];
        $card_text = $data[0]['body'];

        // Final response. 
        $response->respond('There\'s a product that costs ' . $response_cost . ' dollars that you can check out, and it has ' . $search_term . ' inside.')
          ->withCard($card_text);

        break;

      default:
        $response->respond('Is your Drupal install running? You better chase after it!');
        break;
    }
  }

}

API.AI Webhook Sample

// Copyright 2016, Google, Inc.
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

process.env.DEBUG = 'actions-on-google:*';
let Assistant = require('actions-on-google').ApiAiAssistant;
let express = require('express');
let bodyParser = require('body-parser');

let app = express();
app.use(bodyParser.json({type: 'application/json'}));

// [START YourAction]
app.post('/', function (req, res) {
    const assistant = new Assistant({request: req, response: res});

    // Fulfill action business logic
    function responseHandler (assistant) {

    var product = assistant['body_']['result']['parameters']['Product'];
    var http = require("http");
    var options = {
      "method": "GET",
      "hostname": "localhost",
      "port": "7000",
      "path": "/products-rest/" +product+ "",
      "headers": {
        "cache-control": "no-cache"
      }
    };
    var req = http.request(options, function (res) {
      var chunks = [];

      res.on("data", function (chunk) {
        chunks.push(chunk);
      });

      res.on("end", function () {
        var body = Buffer.concat(chunks);
        var responseObj = JSON.parse(body);
        var price = responseObj[0]['field_price'];
        // Send the response back to the Home. 
        assistant.tell('There\'s a product that costs' +price+ 'dollars that you can check out and it has' +product+ 'inside');
      });
    });

    req.end();
  }

  assistant.handleRequest(responseHandler);
});
// [END YourAction]

if (module === require.main) {
  // [START server]
  // Start the server
  let server = app.listen(process.env.PORT || 8010, function () {
    let port = server.address().port;
    console.log('App listening on port %s', port);
  });
  // [END server]
}

module.exports = app;

My Sample Utterances 

HelloDrupal hello drupal from {City}
HelloDrupal say hi to drupal from {City}
SearchDrupal I'm looking for a product with {SearchTerm}

My Sample Intents

{
  "intents": [
    {
      "intent": "HelloDrupal",
      "slots": [
        {
          "name": "City",
          "type": "AMAZON.US_CITY"
        }
      ]
    },
    {
      "intent": "SearchDrupal",
      "slots": [
        {
          "name": "SearchTerm",
          "type": "AMAZON.Food"
        }
      ]      
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.CancelIntent"
    },  
    {
      "intent": "AMAZON.RepeatIntent"
    }
  ]
}

Final Thoughts 

Again this is meant to get you started. I’m not doing any existence checks in my code, so this isn’t production ready. But the point is to show you what’s possible and to point you in the right direction. If you have any questions or feedback feel free to comment below or email me at fullcolorcoder at the gmail place. Good luck!