Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: json

  • FacetWP, JSON API, and WP_Query Searches

    FacetWP, JSON API, and WP_Query Searches

    One of the ways that FacetWP works is that it adds <!--fwp-loop--> to the source of your outputted page when it detects you used a search query and it can’t find it’s usual classes. This is so that it’s various features like refreshes and so on. It’s a good thing.

    At least it is until you’re trying to use a WP_Query based search to find titles “like” something, and you find your JSON output prefixed…

    Mostly Harmless

    Most of the time when you see <!--fwp-loop--> in the source code, you don’t care. It doesn’t impact anything and it helps Facet work. This is especially important when you have a ‘weird’ theme or plugins that mess with output.

    The issue is that this is how Facet decides if you need that output:

    $is_main_query = ( $query->is_archive || $query->is_search || ( $query->is_main_query() && ! $query->is_singular ) );
    

    Most of the time, that makes perfect sense. It just happens that I’m calling search in a place where that output is a bad idea. Like this:

    {“id”:6294,”name”:”Alex”,”shows”:”Witches of East End”,”url”:”https:\/\/tv.lezpress.dev\/character\/alex\/”,”died”:”alive”}

    Whoops.

    Annoying, But Not Impossible, To Fix

    After bashing my head in for a while, I grep’d the code for Facet, found where it was being set, and then read the help document on facetwp_is_main_query which told me that I could filter the function.

    In this case, I needed to set the value to false to get it to stop outputting, so I used this:

    add_filter( 'facetwp_is_main_query', function( $is_main_query, $query ) { return false; }, 10, 2 );
    

    Be careful where you put that by the way. If you put it on all pages, you’ll break your Facets. I put it in the function that generates the JSON output which limits it heavily, just as I want it to.

  • A Fully Functional Alexa Skill

    A Fully Functional Alexa Skill

    I’ve been talking a lot (a lot) about my Amazon Alexa skill.

    First of all, it’s done. It’s published and working. You can check it out on Amazon.com.

    Secondly, a lot of people are like me and need a real, concrete, working example. So I’ve decided to post the entirety of my Amazon Alexa Skill.

    This file, which is actually found on a site in /lwtv-plugin/rest-api/alexa-skills.php consists of two endpoints: the flash briefing and the skill. The skill is called “Bury Your Queers” and while I suspect that part of the code is the least ‘useful’ to people, it’s also good to see the real code.

    Here, then, is the original approved and certified WordPress code for an Alexa Skill:

    The Code

    <?php
    /*
    Description: REST-API - Alexa Skills
    
    For Amazon Alexa Skills
    
    Version: 1.0
    Author: Mika Epstein
    */
    
    if ( ! defined('WPINC' ) ) die;
    
    /**
     * class LWTV_Alexa_Skills
     *
     * The basic constructor class that will set up our JSON API.
     */
    class LWTV_Alexa_Skills {
    
    	/**
    	 * Constructor
    	 */
    	public function __construct() {
    		add_action( 'rest_api_init', array( $this, 'rest_api_init') );
    	}
    
    	/**
    	 * Rest API init
    	 *
    	 * Creates callbacks
    	 *   - /lwtv/v1/flash-briefing
    	 */
    	public function rest_api_init() {
    
    		// Skills
    		register_rest_route( 'lwtv/v1', '/alexa-skills/briefing/', array(
    			'methods' => 'GET',
    			'callback' => array( $this, 'flash_briefing_rest_api_callback' ),
    		) );
    
    		// Skills
    		register_rest_route( 'lwtv/v1', '/alexa-skills/byq/', array(
    			'methods' => [ 'GET', 'POST' ],
    			'callback' => array( $this, 'bury_your_queers_rest_api_callback' ),
    		) );
    
    
    	}
    
    	/**
    	 * Rest API Callback for Flash Briefing
    	 */
    	public function flash_briefing_rest_api_callback( $data ) {
    		$response = $this->flash_briefing();
    		return $response;
    	}
    
    	/**
    	 * Rest API Callback for Bury Your Queers
    	 * This accepts POST data
    	 */
    	public function bury_your_queers_rest_api_callback( WP_REST_Request $request ) {
    
    		$type   = ( isset( $request['request']['type'] ) )? $request['request']['type'] : false;
    		$intent = ( isset( $request['request']['intent']['name'] ) )? $request['request']['intent']['name'] : false;
    		$date   = ( isset( $request['request']['intent']['slots']['Date']['value'] ) )? $request['request']['intent']['slots']['Date']['value'] : false;
    
    		$validate_alexa = $this->alexa_validate_request( $request );
    
    		if ( $validate_alexa['success'] != 1 ) {
    			$error = new WP_REST_Response( array( 'message' => $validate_alexa['message'], 'data' => array( 'status' => 400 ) ) );
    			$error->set_status( 400 );
    			return $error;
    		}
    
    		$response = $this->bury_your_queers( $type, $intent, $date );
    		return $response;
    	}
    
    
    	function alexa_validate_request( $request ) {
    
    		$chain_url = $request->get_header( 'signaturecertchainurl' );
    		$timestamp = $request['request']['timestamp'];
    		$signature = $request->get_header( 'signature' );
    
    	    // Validate that it even came from Amazon ...
    	    if ( !isset( $chain_url ) )
    	    	return array( 'success' => 0, 'message' => 'This request did not come from Amazon.' );
    
    	    // Validate proper format of Amazon provided certificate chain url
    	    $valid_uri = $this->alexa_valid_key_chain_uri( $chain_url );
    	    if ( $valid_uri != 1 )
    	    	return array( 'success' => 0, 'message' => $valid_uri );
    
    	    // Validate certificate signature
    	    $valid_cert = $this->alexa_valid_cert( $request, $chain_url, $signature );
    	    if ( $valid_cert != 1 )
    	    	return array ( 'success' => 0, 'message' => $valid_cert );
    
    	    // Validate time stamp
    		if (time() - strtotime( $timestamp ) > 60)
    			return array ( 'success' => 0, 'message' => 'Timestamp validation failure. Current time: ' . time() . ' vs. Timestamp: ' . $timestamp );
    
    	    return array( 'success' => 1, 'message' => 'Success' );
    	}
    
    	/*
    		Validate certificate chain URL
    	*/
    	function alexa_valid_key_chain_uri( $keychainUri ){
    
    	    $uriParts = parse_url( $keychainUri );
    
    	    if (strcasecmp( $uriParts['host'], 's3.amazonaws.com' ) != 0 )
    	        return ( 'The host for the Certificate provided in the header is invalid' );
    
    	    if (strpos( $uriParts['path'], '/echo.api/' ) !== 0 )
    	        return ( 'The URL path for the Certificate provided in the header is invalid' );
    
    	    if (strcasecmp( $uriParts['scheme'], 'https' ) != 0 )
    	        return ( 'The URL is using an unsupported scheme. Should be https' );
    
    	    if (array_key_exists( 'port', $uriParts ) && $uriParts['port'] != '443' )
    	        return ( 'The URL is using an unsupported https port' );
    
    	    return 1;
    	}
    
    	/*
    	    Validate that the certificate and signature are valid
    	*/
    	function alexa_valid_cert( $request, $chain_url, $signature ) {
    
    		$md5pem     = get_temp_dir() . md5( $chain_url ) . '.pem';
    	    $echoDomain = 'echo-api.amazon.com';
    
    	    // If we haven't received a certificate with this URL before,
    	    // store it as a cached copy
    	    if ( !file_exists( $md5pem ) ) {
    		    file_put_contents( $md5pem, file_get_contents( $chain_url ) );
    		}
    
    	    $pem = file_get_contents( $md5pem );
    
    	    // Validate certificate chain and signature
    	    $ssl_check = openssl_verify( $request->get_body() , base64_decode( $signature ), $pem, 'sha1' );
    
    	    if ($ssl_check != 1 ) {
    		    return( openssl_error_string() );
    		}
    
    	    // Parse certificate for validations below
    	    $parsedCertificate = openssl_x509_parse( $pem );
    	    if ( !$parsedCertificate ) return( 'x509 parsing failed' );
    
    	    // Check that the domain echo-api.amazon.com is present in
    	    // the Subject Alternative Names (SANs) section of the signing certificate
    	    if(strpos( $parsedCertificate['extensions']['subjectAltName'], $echoDomain) === false) {
    	        return( 'subjectAltName Check Failed' );
    	    }
    
    	    // Check that the signing certificate has not expired
    	    // (examine both the Not Before and Not After dates)
    	    $validFrom = $parsedCertificate['validFrom_time_t'];
    	    $validTo   = $parsedCertificate['validTo_time_t'];
    	    $time      = time();
    
    	    if ( !( $validFrom <= $time && $time <= $validTo ) ) {
    	        return( 'certificate expiration check failed' );
    	    }
    
    	    return 1;
    	}
    
    	/**
    	 * Generate the Flash Briefing output
    	 *
    	 * @access public
    	 * @return void
    	 */
    	public function flash_briefing() {
    
    		$query = new WP_Query( array( 'numberposts' => '10' ) );
    		if ( $query->have_posts() ) {
    			while ( $query->have_posts() ) {
    				$query->the_post();
    
    				$response = array(
    					'uid'            => get_the_permalink(),
    					'updateDate'     => get_post_modified_time( 'Y-m-d\TH:i:s.\0\Z' ),
    					'titleText'      => get_the_title(),
    					'mainText'       => get_the_title() . '. ' . get_the_excerpt(),
    					'redirectionUrl' => home_url(),
    				);
    
    				$responses[] = $response;
    			}
    			wp_reset_postdata();
    		}
    
    		if ( count( $responses ) === 1 ) {
    			$responses = $responses[0];
    		}
    
    		return $responses;
    
    	}
    
    	/**
    	 * Generate Bury Your Queers
    	 *
    	 * @access public
    	 * @return void
    	 */
    	public function bury_your_queers( $type = false, $intent = false, $date = false ) {
    
    		$whodied    = '';
    		$endsession = true;
    		$timestamp  = ( strtotime( $date ) == false )? false : strtotime( $date ) ;
    		$helptext   = 'You can find out who died on specific dates by asking me questions like "who died" or "who died today" or "who died on March 3rd" or even "How many died in 2017." If no one died then, I\'ll let you know.';
    
    		if ( $type == 'LaunchRequest' ) {
    			$whodied = 'Welcome to the LezWatch TV Bury Your Queers skill. ' . $helptext;
    			$endsession = false;
    		} else {
    			if ( $intent == 'AMAZON.HelpIntent' ) {
    				$whodied = 'This is the Bury Your Queers skill by LezWatch TV, home of the world\'s greatest database of queer female on TV. ' . $helptext;
    				$endsession = false;
    			} elseif ( $intent == 'AMAZON.StopIntent' || $intent == 'AMAZON.CancelIntent' ) {
    				// Do nothing
    			} elseif ( $intent == 'HowMany' ) {
    				if ( $date == false || $timestamp == false ) {
    					$data     = LWTV_Stats_JSON::statistics( 'death', 'simple' );
    					$whodied  = 'A total of '. $data['characters']['dead'] .' queer female characters have died on TV.';
    				} elseif ( !preg_match( '/^[0-9]{4}$/' , $date ) ) {
    					$whodied    = 'I\'m sorry. I don\'t know how to calculate deaths in anything but years right now. ' . $helptext;
    					$endsession = false;
    				} else {
    					$data     = LWTV_Stats_JSON::statistics( 'death', 'years' );
    					$count    = $data[$date]['count'];
    					$how_many = 'No queer female characters died on TV in ' . $date . '.';
    					if ( $count > 0 ) {
    						$how_many = $count .' queer female ' . _n( 'character', 'characters', $count ) . ' died on TV in ' . $date . '.';
    					}
    					$whodied  = $how_many;
    				}
    			} elseif ( $intent == 'WhoDied' ) {
    				if ( $date == false || $timestamp == false ) {
    					$data    = LWTV_BYQ_JSON::last_death();
    					$name    = $data['name'];
    					$whodied = 'The last queer female to die was '. $name .' on '. date( 'F j, Y', $data['died'] ) .'.';
    				} elseif ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])$/' , $date ) ) {
    					$whodied    = 'I\'m sorry. I don\'t know how to calculate deaths in anything but days right now. ' . $helptext;
    					$endsession = false;
    				} else {
    					$this_day = date('m-d', $timestamp );
    					$data     = LWTV_BYQ_JSON::on_this_day( $this_day );
    					$count    = ( key( $data ) == 'none' )? 0 : count( $data ) ;
    					$how_many = 'No queer females died';
    					$the_dead = '';
    					if ( $count > 0 ) {
    						$how_many  = $count . ' queer female ' . _n( 'character', 'characters', $count ) . ' died';
    						$deadcount = 1;
    						foreach ( $data as $dead_character ) {
    							if ( $deadcount == $count && $count !== 1 ) $the_dead .= 'And ';
    							$the_dead .= $dead_character['name'] . ' in ' . $dead_character['died'] . '. ';
    							$deadcount++;
    						}
    					}
    					$whodied = $how_many . ' on '. date('F jS', $timestamp ) . '. ' . $the_dead;
    				}
    			} else {
    				// We have a weird request...
    				$whodied = 'I\'m sorry, I don\'t understand that request. Please ask me something else.';
    				$endsession = false;
    			}
    		}
    		$response = array(
    			'version'  => '1.0',
    			'response' => array (
    				'outputSpeech' => array (
    					'type' => 'PlainText',
    					'text' => $whodied,
    				),
    				'shouldEndSession' => $endsession,
    			)
    		);
    
    		return $response;
    
    	}
    
    }
    new LWTV_Alexa_Skills();
    

    Some Explanations

    The functions bury_your_queers and bury_your_queers_rest_api_callback are the important ones. The flash briefing is there because I was tired of Amazon being picky about embedded media in RSS feeds.

    The way bury_your_queers_rest_api_callback works is it takes the request data to generate the type of request, the intent, and the date information. Then it passes the full request data to alexa_validate_request which is the part you’ll really want.

    That function, alexa_validate_request, is what’s validating that the request came from Amazon, that it’s got a legit certificate from Amazon, and that the request was made in the last 60 seconds. While all those checks kick back an error, the development tools from Amazon will not show you them. Yet. I’m hoping they will in the future so we can more easily debug, but it was a lot of blind debugging. Not my favorite.

    Some Custom Code

    In the bury_your_queers function, I make some calls to other code not included:

    • LWTV_BYQ_JSON::last_death()
    • LWTV_BYQ_JSON::on_this_day( $this_day )

    Those both reference another rest API class in a different file. What’s important here is not what the data is, but that I’m calling those functions and getting an array back, and using that to fill in my reply. For example, here we have the call for ‘last death’:

    $data    = LWTV_BYQ_JSON::last_death();
    $name    = $data['name'];
    $whodied = 'The last queer female to die was '. $name .' on '. date( 'F j, Y', $data['died'] ) .'.';
    

    From this you can infer that the array kicked back has key for name and died. And in fact, if you look at the JSON output, you’ll see if has that and a bit more. I’m just extracting what is required. The same is true of the other function, LWTV_BYQ_JSON::on_this_day, to which I’m passing a parameter of a date.

  • POST Isn’t Just For Posting

    POST Isn’t Just For Posting

    I’m taking a moment here to explain something that confused the hell out of me when I was getting into the JSON API. In short, I was confused about what POST meant in the JSON API.

    POST vs GET

    The official documentation mentions this:

    Want to get your site’s posts? Simply send a GET request to /wp-json/wp/v2/posts. Update user with ID 4? Send a POST request to /wp-json/wp/v2/users/4. Get all posts with the search term “awesome”? GET /wp-json/wp/v2/posts?search=awesome. It’s that easy.

    From that I inferred that if I was updated content on my site, I would use a POST call. Otherwise, I’m always going to be using GET to get data. Makes sense. If you want to get the data on a specific post, you do a get for /wp-json/wp/v2/posts/1234 and get that post’s data. Store the JSON output as a parameter in your plugin, let’s say, and Bob’s your father’s brother’s husband.

    I was wrong.

    Alexa Posts

    When I started working with Alexa I was confounded. It told me it sent a POST request to my site. I stared at that for a while. I’d been assuming that when I asked Alexa something, I’d be able to tell it to do a GET request from /wp-json/MYAPP/v1/MYSKILL/parameter. After all, I’m not posting data.

    But then I thought about it a little bit more. A straightforward GET request gets data from a URL without interaction. A POST posts data to a site, and you decide what to do with it. Most of the time when we think of a POST action happening, we think of updating data.

    POST doesn’t have to mean update

    A POST is just sending data to your JSON API. It posts to your site.

    That’s why passing the WP_REST_Request $request data to your function gives you magical access to the request data. And from that we can grab all the data Alexa requests send to your site, which lets us parse the data and make decisions on our replies.

    Now like I said before, what you do with the POST is up to you. But that explains a lot about why Amazon is so picky about making sure a request came legit from them. Especially since you can order stuff from Amazon…

    “Hey Alexa, can you tell TV shows to stop killing off queer characters?”

  • Custom Alexa Skills and WordPress

    Custom Alexa Skills and WordPress

    Before you start, please note that there is no Amazon Alexa SDK for PHP. And this is a big problem.

    An SDK is a software development kit that helps standardize the development of apps for the software. Using an SDK is basically using a standard library that everyone can access and call and not reinvent the wheel all the bloody time. And Amazon, for whatever reason, has decided that they’d rather push their Lambda hosting, which yes they charge you for, instead of clearly and cleanly document PHP code. Node.js? No problem. PHP? You’re on your own.

    Rant aside, I have now a custom Amazon Skill, self hosted, and powered by WordPress.

    Amazon’s Requirements

    On Monday I showed you how to build a very generic skill. It had no options, and it was actually missing a critical piece. You see, Amazon has six basic requirements to be an app:

    1. The service must be Internet-accessible.
    2. The service must adhere to the Alexa Skills Kit interface.
    3. The service must support HTTP over SSL/TLS, leveraging an Amazon-trusted certificate.
    4. The service must accept requests on port 443.
    5. The service must present a certificate with a subject alternate name that matches the domain name of the endpoint.
    6. The service must validate that incoming requests are coming from Alexa.

    The first five are pretty normal. If it’s not internet accessible, its not going to work. Same with the adherence to the skills kit interface. But that last one was surprisingly difficult and annoying. Mostly because of that lack of a standardized PHP SDK.

    Basically there isn’t a standard way to validate that incoming requests are coming from Alexa, but boy howdy, are there requirements.

    Validating Requests for Alexa

    While it says the requirement is to validate the requests, that’s only one aspect of the game. The three basic parts are these:

    • Verifying that the Request was Sent by Alexa
    • Checking the Signature of the Request
    • Checking the Timestamp of the Request

    And none of those are really well documented for PHP. Thanks.

    The Code

    In Monday’s post, I framed out the majority of the code that will be used. The change will be in this section:

    public function last_post_rest_api_callback( $data ) {
    	$response = $this->last_post();
    	return $response;
    }
    

    It now shows this:

    public function bury_your_queers_rest_api_callback( WP_REST_Request $request ) {
    	$date = ( isset( $request['request']['intent']['slots']['Date']['value'] ) )? $request['request']['intent']['slots']['Date']['value'] : false;
    	$validate_alexa = $this->alexa_validate_request( $request );
    	if ( $validate_alexa['success'] !== 1 ) {
    		$error = new WP_REST_Response( array( 'message' => $validate_alexa['message'], 'data' => array( 'status' => 400 ) ) );
    		$error->set_status( 400 );
    		return $error;
    	}
    	$response = $this->last_post( $date );
    	return $response;
    }
    

    This makes two changes. First it’s grabbing the date from the weirdly stored JSON POST from Alexa and passing it to my last_post function. That code I’m skipping since taking the date, parsing it, and changing your output from last_post is beyond the score. No, I’m going to concentrate on the alexa_validate_request function.

    You should take note of the success check if ( $validate_alexa['success'] !== 1 ) however. You must use a rest response with a 400 because Amazon is very picky.

    alexa_validate_request

    The brunt of the validation is to check if the URL came from Amazon, if the URL is on the certificate chain, if the certificate is legit, and finally if the request was made in the last 60 seconds. Which is a lot to look for.

    In order to write this function, I forked Rich Bowen’s Validate Echo request via PHP code for WordPress. This takes into account some WordPress code that isn’t otherwise available:

    function alexa_validate_request( $request ) {
    	$url            = $request->get_header( 'signaturecertchainurl' );
    	$timestamp      = $request['request']['timestamp'];
    	$signature      = $request->get_header( 'signature' );
    
    	// Validate that it even came from Amazon ...
    	if ( !isset( $url ) )
    		return array( 'success' => 0, 'message' => 'This request did not come from Amazon.' );
    
    	// Validate proper format of Amazon provided certificate chain url
    	$valid_uri = $this->alexa_valid_key_chain_uri( $url );
    	if ( $valid_uri != 1 )
    	    	return array( 'success' => 0, 'message' => $valid_uri );
    
    	// Validate certificate signature
    	$valid_cert = $this->alexa_valid_cert( $request, $signature, $url );
    	if ( $valid_cert != 1 )
    	    	return array ( 'success' => 0, 'message' => $valid_cert );
    
    	// Validate time stamp
    	if (time() - strtotime( $timestamp ) > 60)
    		return array ( 'success' => 0, 'message' => 'Timestamp validation failure. Current time: ' . time() . ' vs. Timestamp: ' . $timestamp );
    
    	// If there was no error, it's a success!
    	return array( 'success' => 1, 'message' => 'Success' );
    }
    

    Within that function, I reference two more: alexa_valid_key_chain_uri and alexa_valid_cert which parse the chain and validate the certificate.

    function alexa_valid_key_chain_uri( $keychainUri ){
    
        $uriParts = parse_url($keychainUri);
    
        if (strcasecmp( $uriParts['host'], 's3.amazonaws.com' ) != 0 )
            return ( 'The host for the Certificate provided in the header is invalid' );
    
        if (strpos( $uriParts['path'], '/echo.api/' ) !== 0 )
            return ( 'The URL path for the Certificate provided in the header is invalid' );
    
        if (strcasecmp( $uriParts['scheme'], 'https' ) != 0 )
            return ( 'The URL is using an unsupported scheme. Should be https' );
    
        if (array_key_exists( 'port', $uriParts ) && $uriParts['port'] != '443' )
            return ( 'The URL is using an unsupported https port' );
    
        return 1;
    }
    
    /*
        Validate that the certificate and signature are valid
    */
    function alexa_valid_cert( $request, $signature, $url ) {
    
        $md5pem     = get_temp_dir() . md5( $url ) . '.pem';
        $echoDomain = 'echo-api.amazon.com';
    
        // If we haven't received a certificate with this URL before,
        // store it as a cached copy
        if ( !file_exists( $md5pem ) ) file_put_contents( $md5pem, file_get_contents( $url ) );
    
        // Validate certificate chain and signature
        $pem = file_get_contents( $md5pem );
        $ssl_check = openssl_verify( $request->get_body(), base64_decode( $signature ), $pem, 'sha1' );
        if ($ssl_check != 1 ) return( openssl_error_string() );
    
        // Parse certificate for validations below
        $parsedCertificate = openssl_x509_parse( $pem );
        if ( !$parsedCertificate ) return( 'x509 parsing failed' );
    
        // Check that the domain echo-api.amazon.com is present in
        // the Subject Alternative Names (SANs) section of the signing certificate
        if(strpos( $parsedCertificate['extensions']['subjectAltName'], $echoDomain) === false) {
            return( 'subjectAltName Check Failed' );
        }
    
        // Check that the signing certificate has not expired
        // (examine both the Not Before and Not After dates)
        $validFrom = $parsedCertificate['validFrom_time_t'];
        $validTo   = $parsedCertificate['validTo_time_t'];
        $time      = time();
        if (!($validFrom <= $time && $time <= $validTo)) {
            return( 'certificate expiration check failed' );
        }
    
        return 1;
    }
    

    A Word About Testing…

    The problem with all this weird code is that the only way to test is to use Amazon’s testing platform and that doesn’t actually throw back errors. The testing environment is fun, because you can type in ‘when was last post’ and it prepends “Alexa, ask HalfElf…” for you. And it shows you exactly what JSON it’s passing to your API and what your API retuned.

    But…

    In the event your API throws an error, you don’t get to see what the error was. No, you get a message saying that the API returned an invalid output.

    Basically the Amazon API has no actual debugging if you’re trying to debug the connection requirements.

    There may have been a lot of swearing involved on my end.

  • Hello World: WordPress, the Rest API, and Alexa

    Hello World: WordPress, the Rest API, and Alexa

    I have a big issue with Amazon’s ‘documentation.’ Trying to learn how to do anything is akin to sniffing your cat’s butt to find out where the dog is. It’s written from a mindset I don’t share, it rarely has practical and functional examples, and the font is maddeningly small.

    How I learn best is by creating a “Hello World” type app. Even if it’s just copy and pasting, by doing that, my brain is able to follow the pathways and understand the logic steps.

    If you’re like me and have been swearing at Amazon just trying to make a simple ‘Hello World’ app for your Echo, here we go.

    The Outcome

    To summarize what we want here, is we would like to be able to do is turn to our Echos and say this:

    Hey Alexa, ask HalfElf what the last post is.

    And we want Alexa to reply:

    The last post on HalfElf was [Post Name]

    This is really simple on purpose. While eventually we want to be able to ask for a post on a specific date, we’re not there yet. You’ve got to run before you can walk.

    The Design

    Designing your API requires forethought. In a previous post, I named my flash briefing URL /MYSITE/v1/alexa-skills/briefing and, in keeping with that, this one will be named /MYSITE/v1/alexa-skills/last-post/

    You’ll need to hang on to your URL – https://example.com/wp-json/MYSITE/v1/alexa-skills/last-post/ – as you will need to put this into your Amazon Skill. This will be a custom skill and you’ll need to have the intent look like this:

    {
      "intents": [
        {
          "slots": [
            {
              "name": "Date",
              "type": "AMAZON.DATE"
            }
          ],
          "intent": "HalfElf"
        }
      ]
    }
    

    Remember, the goal is eventually to be able to use that date slot. We’re not right now, but be prepared, as Mr. Lehrer would say.

    With that in mind, the sample utterances look like this:

    HalfElf last post
    HalfElf post on {Date}
    HalfElf today's post
    HalfElf what the last post is
    

    You’ll notice I’m trying to think of every which way someone might ask the question. You need to do this. Alexa is very picky.

    The Rest API Code

    Once you’ve built out your Amazon Skill (and yes, that really is the easy part), you have to have the response. This is built off the same model I used before, and can be slipped in and shared.

    class MYSITE_Alexa_Skills {
    
    	public function __construct() {
    		add_action( 'rest_api_init', array( $this, 'rest_api_init') );
    	}
    
    	public function rest_api_init() {
    
    		register_rest_route( 'MYSITE/v1', '/alexa-skills/last-post/', array(
    			'methods' => [ 'GET', 'POST' ],
    			'callback' => array( $this, 'last_post_rest_api_callback' ),
    		) );
    	}
    
    	public function last_post_rest_api_callback( $data ) {
    		$response = $this->last_post();
    		return $response;
    	}
    
    	public function last_post() {
    
    		$query = new WP_Query( array( 'numberposts' => '1' ) );
    		if ( $query->have_posts() ) {
    			while ( $query->have_posts() ) {
    				$query->the_post();
    
    				$lastpost = 'The last post on HalfElf was .' get_the_title();
    			}
    			wp_reset_postdata();
    		}
    
    		$response = array(
    			'version'  => '1.0',
    			'response' => array (
    				'outputSpeech' => array (
    					'type' => 'PlainText',
    					'text' => $last_post,
    				),
    				'shouldEndSession' => true,
    			)
    		);
    
    		return $response;
    	}
    
    }
    new MYSITE_Alexa_Skills();
    

    As I said, it’s pretty simple. The output looks like this:

    {"version":"1.0","response":{"outputSpeech":{"type":"PlainText","text":"The last post on HalfElf was [POSTNAME]"},"shouldEndSession":true}}
    

    This is not the most efficient way to grab one post, but for the purposes of this example, it does get your head around the basic idea.

    Next Up?

    There are two issues with this code. First of all, it doesn’t meet Amazon’s requirements. Secondly, it doesn’t accept parameters. The first issue is much bigger, because as it turns out, Amazon requires you to check if the requests are coming from Amazon, are valid, and aren’t a bot-net attack. This is actually very smart, but very annoying, since they don’t make it easy to figure out how to do all that.

    But that’s next.

    If you hear screaming from California, it’s just me.

  • Flash Briefing JSON Feed

    Flash Briefing JSON Feed

    The other day I mentioned a ‘solution’ to my problem of video enclosures would also be to use a JSON feed. As much as I’d like to tell you just to use JSON feed, you can’t because their specs don’t match Amazon’s.

    The creation of a JSON Feed that does match their specs is somewhat peculiar, but still straightforward. I went with making a JSON API output instead of making true feed, since frankly I don’t think Amazon’s all that consistent with their own spec, and I’ll need to tweak it later. I’d like to do so without breaking everything else.

    The Code

    class MYSITE_Alexa_Skills {
    
    	/**
    	 * Constructor
    	 */
    	public function __construct() {
    		add_action( 'rest_api_init', array( $this, 'rest_api_init') );
    	}
    
    	/**
    	 * Rest API init
    	 *
    	 * Creates callbacks
    	 *   - /MYSITE/v1/alexa-skills/briefing
    	 */
    	public function rest_api_init() {
    
    		// Skills
    		register_rest_route( 'MYSITE/v1', '/alexa-skills/briefing/', array(
    			'methods' => 'GET',
    			'callback' => array( $this, 'flash_briefing_rest_api_callback' ),
    		) );
    	}
    
    	/**
    	 * Rest API Callback for Flash Briefing
    	 */
    	public function flash_briefing_rest_api_callback( $data ) {
    		$response = $this->flash_briefing();
    		return $response;
    	}
    
    	/**
    	 * Generate the Flash Briefing output
    	 */
    	public function flash_briefing() {
    		$query = new WP_Query( array( 'numberposts' => '10' ) );
    		if ( $query->have_posts() ) {
    			while ( $query->have_posts() ) {
    				$query->the_post();
    
    				$response = array(
    					'uid'            => get_the_permalink(),
    					'updateDate'     => get_post_modified_time( 'Y-m-d\TH:i:s.\0\Z' ),
    					'titleText'      => get_the_title(),
    					'mainText'       => get_the_excerpt(),
    					'redirectionUrl' => home_url(),
    				);
    
    				$responses[] = $response;
    			}
    			wp_reset_postdata();
    		}
    
    		if ( count( $responses ) === 1 ) $responses = $responses[0];
    
    		return $responses;
    	}
    }
    new MYSITE_Alexa_Skills();
    

    Some Notes…

    This is built out with the assumption I will later be adding more information and skills to this site. That’s why the class is named for the skills in general and has the rest route set up for sub-routines already. If that’s not on your to-do, you can simplify.

    I also made the point to strip out the possibility of a StreamURL, which I don’t plan to use at all on this site. If you do, I recommend having a look at VoiceWP’s briefing.php file which does a nice job with handling that.