<?php
declare(strict_types=1);

namespace Location;

use Location\Distance\DistanceInterface;
use Location\Formatter\Polygon\FormatterInterface;

/**
 * Polygon Implementation
 *
 * @author Paul Vidal <paul.vidal.lujan@gmail.com>
 * @author Marcus Jaschen <mjaschen@gmail.com>
 */
class Polygon implements GeometryInterface
{
    use GetBoundsTrait;

    /**
     * @var Coordinate[]
     */
    protected $points = [];

    /**
     * @param Coordinate $point
     *
     * @return void
     */
    public function addPoint(Coordinate $point)
    {
        $this->points[] = $point;
    }

    /**
     * @return Coordinate[]
     */
    public function getPoints(): array
    {
        return $this->points;
    }

    /**
     * Return all polygon point's latitudes.
     *
     * @return float[]
     */
    public function getLats(): array
    {
        $lats = [];

        foreach ($this->points as $point) {
            /** @var Coordinate $point */
            $lats[] = $point->getLat();
        }

        return $lats;
    }

    /**
     * Return all polygon point's longitudes.
     *
     * @return float[]
     */
    public function getLngs(): array
    {
        $lngs = [];

        foreach ($this->points as $point) {
            /** @var Coordinate $point */
            $lngs[] = $point->getLng();
        }

        return $lngs;
    }

    /**
     * @return int
     */
    public function getNumberOfPoints(): int
    {
        return count($this->points);
    }

    /**
     * @param FormatterInterface $formatter
     *
     * @return string
     */
    public function format(FormatterInterface $formatter): string
    {
        return $formatter->format($this);
    }

    /**
     * @return Line[]
     */
    public function getSegments(): array
    {
        $length   = count($this->points);
        $segments = [];

        if ($length <= 1) {
            return $segments;
        }

        for ($i = 1; $i < $length; $i++) {
            $segments[] = new Line($this->points[$i - 1], $this->points[$i]);
        }

        // to close the polygon we have to add the final segment between
        // the last point and the first point
        $segments[] = new Line($this->points[$i - 1], $this->points[0]);

        return $segments;
    }

    /**
     * Determine if given geometry is contained inside the polygon. This is
     * assumed to be true, if each point of the geometry is inside the polygon.
     *
     * Edge cases:
     *
     * - it's not detected when a line between two points is outside the polygon
     * - @see contains() for more restrictions
     *
     * @param GeometryInterface $geometry
     *
     * @return boolean
     */
    public function containsGeometry(GeometryInterface $geometry): bool
    {
        $geometryInPolygon = true;

        foreach ($geometry->getPoints() as $point) {
            $geometryInPolygon = $geometryInPolygon && $this->contains($point);
        }

        return $geometryInPolygon;
    }

    /**
     * Determine if given point is contained inside the polygon. Uses the PNPOLY
     * algorithm by W. Randolph Franklin. Therfore some edge cases may not give the
     * expected results, e. g. if the point resides on the polygon boundary.
     *
     * @see http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
     *
     * For special cases this calculation leads to wrong results:
     *
     * - if the polygons spans over the longitude boundaries at 180/-180 degrees
     *
     * @param Coordinate $point
     *
     * @return bool
     */
    public function contains(Coordinate $point): bool
    {
        $numberOfPoints = $this->getNumberOfPoints();
        $polygonLats    = $this->getLats();
        $polygonLngs    = $this->getLngs();

        $polygonContainsPoint = false;

        for ($node = 0, $altNode = ($numberOfPoints - 1); $node < $numberOfPoints; $altNode = $node ++) {
            if (($polygonLngs[$node] > $point->getLng() !== ($polygonLngs[$altNode] > $point->getLng()))
                && ($point->getLat() < ($polygonLats[$altNode] - $polygonLats[$node])
                                       * ($point->getLng() - $polygonLngs[$node])
                                       / ($polygonLngs[$altNode] - $polygonLngs[$node])
                                       + $polygonLats[$node]
                )
            ) {
                $polygonContainsPoint = ! $polygonContainsPoint;
            }
        }

        return $polygonContainsPoint;
    }

    /**
     * Calculates the polygon perimeter.
     *
     * @param DistanceInterface $calculator instance of distance calculation class
     *
     * @return float
     */
    public function getPerimeter(DistanceInterface $calculator): float
    {
        $perimeter = 0.0;

        if (count($this->points) < 2) {
            return $perimeter;
        }

        foreach ($this->getSegments() as $segment) {
            $perimeter += $segment->getLength($calculator);
        }

        return $perimeter;
    }

    /**
     * Calculates the polygon area.
     *
     * This algorithm gives inaccurate results as it ignores
     * ellipsoid parameters other than to arithmetic mean radius.
     * The error should be < 1 % for small areas.
     *
     * @return float
     */
    public function getArea(): float
    {
        $area = 0;

        if ($this->getNumberOfPoints() <= 2) {
            return $area;
        }

        $referencePoint = $this->points[0];
        $radius         = $referencePoint->getEllipsoid()->getArithmeticMeanRadius();
        $segments       = $this->getSegments();

        foreach ($segments as $segment) {
            /** @var Coordinate $point1 */
            $point1 = $segment->getPoint1();
            /** @var Coordinate $point2 */
            $point2 = $segment->getPoint2();

            $x1 = deg2rad($point1->getLng() - $referencePoint->getLng()) * cos(deg2rad($point1->getLat()));
            $y1 = deg2rad($point1->getLat() - $referencePoint->getLat());

            $x2 = deg2rad($point2->getLng() - $referencePoint->getLng()) * cos(deg2rad($point2->getLat()));
            $y2 = deg2rad($point2->getLat() - $referencePoint->getLat());

            $area += ($x2 * $y1 - $x1 * $y2);
        }

        $area *= 0.5 * $radius ** 2;

        return (float)abs($area);
    }

    /**
     * Create a new polygon with reversed order of points, i. e. reversed
     * polygon direction.
     *
     * @return Polygon
     */
    public function getReverse(): Polygon
    {
        $reversed = new static();

        foreach (array_reverse($this->points) as $point) {
            $reversed->addPoint($point);
        }

        return $reversed;
    }
}


<?php

namespace Clue\React\Socks;

use React\Socket\ServerInterface;
use React\Promise\PromiseInterface;
use React\Socket\ConnectorInterface;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
use React\EventLoop\LoopInterface;
use \UnexpectedValueException;
use \InvalidArgumentException;
use \Exception;
use React\Promise\Timer\TimeoutException;

final class Server
{
    // the following error codes are only used for SOCKS5 only
    /** @internal */
    const ERROR_GENERAL = 0x01;
    /** @internal */
    const ERROR_NOT_ALLOWED_BY_RULESET = 0x02;
    /** @internal */
    const ERROR_NETWORK_UNREACHABLE = 0x03;
    /** @internal */
    const ERROR_HOST_UNREACHABLE = 0x04;
    /** @internal */
    const ERROR_CONNECTION_REFUSED = 0x05;
    /** @internal */
    const ERROR_TTL = 0x06;
    /** @internal */
    const ERROR_COMMAND_UNSUPPORTED = 0x07;
    /** @internal */
    const ERROR_ADDRESS_UNSUPPORTED = 0x08;

    private $loop;

    private $connector;

    /**
     * @var null|callable
     */
    private $auth;

    /**
     * @param LoopInterface           $loop
     * @param null|ConnectorInterface $connector
     * @param null|array|callable     $auth
     */
    public function __construct(LoopInterface $loop, ConnectorInterface $connector = null, $auth = null)
    {
        if ($connector === null) {
            $connector = new Connector($loop);
        }

        if (\is_array($auth)) {
            // wrap authentication array in authentication callback
            $this->auth = function ($username, $password) use ($auth) {
                return \React\Promise\resolve(
                    isset($auth[$username]) && (string)$auth[$username] === $password
                );
            };
        } elseif (\is_callable($auth)) {
            // wrap authentication callback in order to cast its return value to a promise
            $this->auth = function($username, $password, $remote) use ($auth) {
                return  \React\Promise\resolve(
                    \call_user_func($auth, $username, $password, $remote)
                );
            };
        } elseif ($auth !== null) {
            throw new \InvalidArgumentException('Invalid authenticator given');
        }

        $this->loop = $loop;
        $this->connector = $connector;
    }

    /**
     * @param ServerInterface $socket
     * @return void
     */
    public function listen(ServerInterface $socket)
    {
        $that = $this;
        $socket->on('connection', function ($connection) use ($that) {
            $that->onConnection($connection);
        });
    }

    /** @internal */
    public function onConnection(ConnectionInterface $connection)
    {
        $that = $this;
        $handling = $this->handleSocks($connection)->then(null, function () use ($connection, $that) {
            // SOCKS failed => close connection
            $that->endConnection($connection);
        });

        $connection->on('close', function () use ($handling) {
            $handling->cancel();
        });
    }

    /**
     * [internal] gracefully shutdown connection by flushing all remaining data and closing stream
     *
     * @internal
     */
    public function endConnection(ConnectionInterface $stream)
    {
        $tid = true;
        $loop = $this->loop;

        // cancel below timer in case connection is closed in time
        $stream->once('close', function () use (&$tid, $loop) {
            // close event called before the timer was set up, so everything is okay
            if ($tid === true) {
                // make sure to not start a useless timer
                $tid = false;
            } else {
                $loop->cancelTimer($tid);
            }
        });

        // shut down connection by pausing input data, flushing outgoing buffer and then exit
        $stream->pause();
        $stream->end();

        // check if connection is not already closed
        if ($tid === true) {
            // fall back to forcefully close connection in 3 seconds if buffer can not be flushed
            $tid = $loop->addTimer(3.0, array($stream,'close'));
        }
    }

    private function handleSocks(ConnectionInterface $stream)
    {
        $reader = new StreamReader();
        $stream->on('data', array($reader, 'write'));

        $that = $this;
        $auth = $this->auth;

        return $reader->readByte()->then(function ($version) use ($stream, $that, $auth, $reader){
            if ($version === 0x04) {
                if ($auth !== null) {
                    throw new UnexpectedValueException('SOCKS4 not allowed because authentication is required');
                }
                return $that->handleSocks4($stream, $reader);
            } else if ($version === 0x05) {
                return $that->handleSocks5($stream, $auth, $reader);
            }
            throw new UnexpectedValueException('Unexpected/unknown version number');
        });
    }

    /** @internal */
    public function handleSocks4(ConnectionInterface $stream, StreamReader $reader)
    {
        $remote = $stream->getRemoteAddress();
        if ($remote !== null) {
            // remove transport scheme and prefix socks4:// instead
            $secure = strpos($remote, 'tls://') === 0;
            if (($pos = strpos($remote, '://')) !== false) {
                $remote = substr($remote, $pos + 3);
            }
            $remote = 'socks4' . ($secure ? 's' : '') . '://' . $remote;
        }

        $that = $this;
        return $reader->readByteAssert(0x01)->then(function () use ($reader) {
            return $reader->readBinary(array(
                'port'   => 'n',
                'ipLong' => 'N',
                'null'   => 'C'
            ));
        })->then(function ($data) use ($reader, $remote) {
            if ($data['null'] !== 0x00) {
                throw new Exception('Not a null byte');
            }
            if ($data['ipLong'] === 0) {
                throw new Exception('Invalid IP');
            }
            if ($data['port'] === 0) {
                throw new Exception('Invalid port');
            }
            if ($data['ipLong'] < 256) {
                // invalid IP => probably a SOCKS4a request which appends the hostname
                return $reader->readStringNull()->then(function ($string) use ($data, $remote){
                    return array($string, $data['port'], $remote);
                });
            } else {
                $ip = long2ip($data['ipLong']);
                return array($ip, $data['port'], $remote);
            }
        })->then(function ($target) use ($stream, $that) {
            return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){
                $stream->write(pack('C8', 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));

                return $remote;
            }, function($error) use ($stream){
                $stream->end(pack('C8', 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));

                throw $error;
            });
        }, function($error) {
            throw new UnexpectedValueException('SOCKS4 protocol error',0,$error);
        });
    }

    /** @internal */
    public function handleSocks5(ConnectionInterface $stream, $auth, StreamReader $reader)
    {
        $remote = $stream->getRemoteAddress();
        if ($remote !== null) {
            // remove transport scheme and prefix socks5:// instead
            $secure = strpos($remote, 'tls://') === 0;
            if (($pos = strpos($remote, '://')) !== false) {
                $remote = substr($remote, $pos + 3);
            }
            $remote = 'socks' . ($secure ? 's' : '') . '://' . $remote;
        }

        $that = $this;
        return $reader->readByte()->then(function ($num) use ($reader) {
            // $num different authentication mechanisms offered
            return $reader->readLength($num);
        })->then(function ($methods) use ($reader, $stream, $auth, &$remote) {
            if ($auth === null && strpos($methods,"\x00") !== false) {
                // accept "no authentication"
                $stream->write(pack('C2', 0x05, 0x00));

                return 0x00;
            } else if ($auth !== null && strpos($methods,"\x02") !== false) {
                // username/password authentication (RFC 1929) sub negotiation
                $stream->write(pack('C2', 0x05, 0x02));
                return $reader->readByteAssert(0x01)->then(function () use ($reader) {
                    return $reader->readByte();
                })->then(function ($length) use ($reader) {
                    return $reader->readLength($length);
                })->then(function ($username) use ($reader, $auth, $stream, &$remote) {
                    return $reader->readByte()->then(function ($length) use ($reader) {
                        return $reader->readLength($length);
                    })->then(function ($password) use ($username, $auth, $stream, &$remote) {
                        // username and password given => authenticate

                        // prefix username/password to remote URI
                        if ($remote !== null) {
                            $remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote);
                        }

                        return $auth($username, $password, $remote)->then(function ($authenticated) use ($stream) {
                            if ($authenticated) {
                                // accept auth
                                $stream->write(pack('C2', 0x01, 0x00));
                            } else {
                                // reject auth => send any code but 0x00
                                $stream->end(pack('C2', 0x01, 0xFF));
                                throw new UnexpectedValueException('Authentication denied');
                            }
                        }, function ($e) use ($stream) {
                            // reject failed authentication => send any code but 0x00
                            $stream->end(pack('C2', 0x01, 0xFF));
                            throw new UnexpectedValueException('Authentication error', 0, $e);
                        });
                    });
                });
            } else {
                // reject all offered authentication methods
                $stream->write(pack('C2', 0x05, 0xFF));
                throw new UnexpectedValueException('No acceptable authentication mechanism found');
            }
        })->then(function ($method) use ($reader) {
            return $reader->readBinary(array(
                'version' => 'C',
                'command' => 'C',
                'null'    => 'C',
                'type'    => 'C'
            ));
        })->then(function ($data) use ($reader) {
            if ($data['version'] !== 0x05) {
                throw new UnexpectedValueException('Invalid SOCKS version');
            }
            if ($data['command'] !== 0x01) {
                throw new UnexpectedValueException('Only CONNECT requests supported', Server::ERROR_COMMAND_UNSUPPORTED);
            }
//             if ($data['null'] !== 0x00) {
//                 throw new UnexpectedValueException('Reserved byte has to be NULL');
//             }
            if ($data['type'] === 0x03) {
                // target hostname string
                return $reader->readByte()->then(function ($len) use ($reader) {
                    return $reader->readLength($len);
                });
            } else if ($data['type'] === 0x01) {
                // target IPv4
                return $reader->readLength(4)->then(function ($addr) {
                    return inet_ntop($addr);
                });
            } else if ($data['type'] === 0x04) {
                // target IPv6
                return $reader->readLength(16)->then(function ($addr) {
                    return inet_ntop($addr);
                });
            } else {
                throw new UnexpectedValueException('Invalid address type', Server::ERROR_ADDRESS_UNSUPPORTED);
            }
        })->then(function ($host) use ($reader, &$remote) {
            return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) {
                return array($host, $data['port'], $remote);
            });
        })->then(function ($target) use ($that, $stream) {
            return $that->connectTarget($stream, $target);
        }, function($error) use ($stream) {
            throw new UnexpectedValueException('SOCKS5 protocol error', $error->getCode(), $error);
        })->then(function (ConnectionInterface $remote) use ($stream) {
            $stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0));

            return $remote;
        }, function(Exception $error) use ($stream){
            $stream->write(pack('C4Nn', 0x05, $error->getCode() === 0 ? Server::ERROR_GENERAL : $error->getCode(), 0x00, 0x01, 0, 0));

            throw $error;
        });
    }

    /** @internal */
    public function connectTarget(ConnectionInterface $stream, array $target)
    {
        $uri = $target[0];
        if (strpos($uri, ':') !== false) {
            $uri = '[' . $uri . ']';
        }
        $uri .= ':' . $target[1];

        // validate URI so a string hostname can not pass excessive URI parts
        $parts = parse_url('tcp://' . $uri);
        if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) {
            return \React\Promise\reject(new InvalidArgumentException('Invalid target URI given'));
        }

        if (isset($target[2])) {
            $uri .= '?source=' . rawurlencode($target[2]);
        }

        $that = $this;
        $connecting = $this->connector->connect($uri);

        $stream->on('close', function () use ($connecting) {
            $connecting->cancel();
        });

        return $connecting->then(function (ConnectionInterface $remote) use ($stream, $that) {
            $stream->pipe($remote, array('end'=>false));
            $remote->pipe($stream, array('end'=>false));

            // remote end closes connection => stop reading from local end, try to flush buffer to local and disconnect local
            $remote->on('end', function() use ($stream, $that) {
                $that->endConnection($stream);
            });

            // local end closes connection => stop reading from remote end, try to flush buffer to remote and disconnect remote
            $stream->on('end', function() use ($remote, $that) {
                $that->endConnection($remote);
            });

            // set bigger buffer size of 100k to improve performance
            $stream->bufferSize = $remote->bufferSize = 100 * 1024 * 1024;

            return $remote;
        }, function(Exception $error) {
            // default to general/unknown error
            $code = Server::ERROR_GENERAL;

            // map common socket error conditions to limited list of SOCKS error codes
            if ((defined('SOCKET_EACCES') && $error->getCode() === SOCKET_EACCES) || $error->getCode() === 13) {
                $code = Server::ERROR_NOT_ALLOWED_BY_RULESET;
            } elseif ((defined('SOCKET_EHOSTUNREACH') && $error->getCode() === SOCKET_EHOSTUNREACH) || $error->getCode() === 113) {
                $code = Server::ERROR_HOST_UNREACHABLE;
            } elseif ((defined('SOCKET_ENETUNREACH') && $error->getCode() === SOCKET_ENETUNREACH) || $error->getCode() === 101) {
                $code = Server::ERROR_NETWORK_UNREACHABLE;
            } elseif ((defined('SOCKET_ECONNREFUSED') && $error->getCode() === SOCKET_ECONNREFUSED) || $error->getCode() === 111 || $error->getMessage() === 'Connection refused') {
                // Socket component does not currently assign an error code for this, so we have to resort to checking the exception message
                $code = Server::ERROR_CONNECTION_REFUSED;
            } elseif ((defined('SOCKET_ETIMEDOUT') && $error->getCode() === SOCKET_ETIMEDOUT) || $error->getCode() === 110 || $error instanceof TimeoutException) {
                // Socket component does not currently assign an error code for this, but we can rely on the TimeoutException
                $code = Server::ERROR_TTL;
            }

            throw new UnexpectedValueException('Unable to connect to remote target', $code, $error);
        });
    }
}


<?php

namespace Clue\React\Socks;

use React\Promise;
use React\Promise\PromiseInterface;
use React\Promise\Deferred;
use React\Socket\ConnectionInterface;
use React\Socket\ConnectorInterface;
use React\Socket\FixedUriConnector;
use \Exception;
use \InvalidArgumentException;
use RuntimeException;

final class Client implements ConnectorInterface
{
    /**
     *
     * @var ConnectorInterface
     */
    private $connector;

    private $socksUri;

    private $protocolVersion = 5;

    private $auth = null;

    public function __construct($socksUri, ConnectorInterface $connector)
    {
        // support `sockss://` scheme for SOCKS over TLS
        // support `socks+unix://` scheme for Unix domain socket (UDS) paths
        if (preg_match('/^(socks(?:5|4)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) {
            // rewrite URI to parse SOCKS scheme, authentication and dummy host
            $socksUri = $match[1] . '://' . $match[3] . 'localhost';

            // connector uses appropriate transport scheme and explicit host given
            $connector = new FixedUriConnector(
                ($match[2] === 's' ? 'tls://' : 'unix://') . $match[4],
                $connector
            );
        }

        // assume default scheme if none is given
        if (strpos($socksUri, '://') === false) {
            $socksUri = 'socks://' . $socksUri;
        }

        // parse URI into individual parts
        $parts = parse_url($socksUri);
        if (!$parts || !isset($parts['scheme'], $parts['host'])) {
            throw new \InvalidArgumentException('Invalid SOCKS server URI "' . $socksUri . '"');
        }

        // assume default port
        if (!isset($parts['port'])) {
            $parts['port'] = 1080;
        }

        // user or password in URI => SOCKS5 authentication
        if (isset($parts['user']) || isset($parts['pass'])) {
            if ($parts['scheme'] !== 'socks' && $parts['scheme'] !== 'socks5') {
                // fail if any other protocol version given explicitly
                throw new InvalidArgumentException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication');
            }
            $parts += array('user' => '', 'pass' => '');
            $this->setAuth(rawurldecode($parts['user']), rawurldecode($parts['pass']));
        }

        // check for valid protocol version from URI scheme
        $this->setProtocolVersionFromScheme($parts['scheme']);

        $this->socksUri = $parts['host'] . ':' . $parts['port'];
        $this->connector = $connector;
    }

    private function setProtocolVersionFromScheme($scheme)
    {
        if ($scheme === 'socks' || $scheme === 'socks5') {
            $this->protocolVersion = 5;
        } elseif ($scheme === 'socks4') {
            $this->protocolVersion = 4;
        } else {
            throw new InvalidArgumentException('Invalid protocol version given "' . $scheme . '://"');
        }
    }

    /**
     * set login data for username/password authentication method (RFC1929)
     *
     * @param string $username
     * @param string $password
     * @link http://tools.ietf.org/html/rfc1929
     */
    private function setAuth($username, $password)
    {
        if (strlen($username) > 255 || strlen($password) > 255) {
            throw new InvalidArgumentException('Both username and password MUST NOT exceed a length of 255 bytes each');
        }
        $this->auth = pack('C2', 0x01, strlen($username)) . $username . pack('C', strlen($password)) . $password;
    }

    /**
     * Establish a TCP/IP connection to the given target URI through the SOCKS server
     *
     * Many higher-level networking protocols build on top of TCP. It you're dealing
     * with one such client implementation,  it probably uses/accepts an instance
     * implementing ReactPHP's `ConnectorInterface` (and usually its default `Connector`
     * instance). In this case you can also pass this `Connector` instance instead
     * to make this client implementation SOCKS-aware. That's it.
     *
     * @param string $uri
     * @return PromiseInterface Promise<ConnectionInterface,Exception>
     */
    public function connect($uri)
    {
        if (strpos($uri, '://') === false) {
            $uri = 'tcp://' . $uri;
        }

        $parts = parse_url($uri);
        if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
            return Promise\reject(new InvalidArgumentException('Invalid target URI specified'));
        }

        $host = trim($parts['host'], '[]');
        $port = $parts['port'];

        if (strlen($host) > 255 || $port > 65535 || $port < 0 || (string)$port !== (string)(int)$port) {
            return Promise\reject(new InvalidArgumentException('Invalid target specified'));
        }

        // construct URI to SOCKS server to connect to
        $socksUri = $this->socksUri;

        // append path from URI if given
        if (isset($parts['path'])) {
            $socksUri .= $parts['path'];
        }

        // parse query args
        $args = array();
        if (isset($parts['query'])) {
            parse_str($parts['query'], $args);
        }

        // append hostname from URI to query string unless explicitly given
        if (!isset($args['hostname'])) {
            $args['hostname'] = $host;
        }

        // append query string
        $socksUri .= '?' . http_build_query($args, '', '&');

        // append fragment from URI if given
        if (isset($parts['fragment'])) {
            $socksUri .= '#' . $parts['fragment'];
        }

        // start TCP/IP connection to SOCKS server
        $connecting = $this->connector->connect($socksUri);

        $deferred = new Deferred(function ($_, $reject) use ($uri, $connecting) {
            $reject(new RuntimeException(
                'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)',
                defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
            ));

            // either close active connection or cancel pending connection attempt
            $connecting->then(function (ConnectionInterface $stream) {
                $stream->close();
            });
            $connecting->cancel();
        });

        // handle SOCKS protocol once connection is ready
        // resolve plain connection once SOCKS protocol is completed
        $that = $this;
        $connecting->then(
            function (ConnectionInterface $stream) use ($that, $host, $port, $deferred, $uri) {
                $that->handleConnectedSocks($stream, $host, $port, $deferred, $uri);
            },
            function (Exception $e) use ($uri, $deferred) {
                $deferred->reject($e = new RuntimeException(
                    'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)',
                    defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111,
                    $e
                ));

                // avoid garbage references by replacing all closures in call stack.
                // what a lovely piece of code!
                $r = new \ReflectionProperty('Exception', 'trace');
                $r->setAccessible(true);
                $trace = $r->getValue($e);
                foreach ($trace as &$one) {
                    foreach ($one['args'] as &$arg) {
                        if ($arg instanceof \Closure) {
                            $arg = 'Object(' . get_class($arg) . ')';
                        }
                    }
                }
                $r->setValue($e, $trace);
            }
        );

        return $deferred->promise();
    }

    /**
     * Internal helper used to handle the communication with the SOCKS server
     *
     * @param ConnectionInterface $stream
     * @param string              $host
     * @param int                 $port
     * @param Deferred            $deferred
     * @param string              $uri
     * @return void
     * @internal
     */
    public function handleConnectedSocks(ConnectionInterface $stream, $host, $port, Deferred $deferred, $uri)
    {
        $reader = new StreamReader();
        $stream->on('data', array($reader, 'write'));

        $stream->on('error', $onError = function (Exception $e) use ($deferred, $uri) {
            $deferred->reject(new RuntimeException(
                'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)',
                defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e)
            );
        });

        $stream->on('close', $onClose = function () use ($deferred, $uri) {
            $deferred->reject(new RuntimeException(
                'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response from proxy (ECONNRESET)',
                defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104)
            );
        });

        if ($this->protocolVersion === 5) {
            $promise = $this->handleSocks5($stream, $host, $port, $reader, $uri);
        } else {
            $promise = $this->handleSocks4($stream, $host, $port, $reader, $uri);
        }

        $promise->then(function () use ($deferred, $stream, $reader, $onError, $onClose) {
            $stream->removeListener('data', array($reader, 'write'));
            $stream->removeListener('error', $onError);
            $stream->removeListener('close', $onClose);

            $deferred->resolve($stream);
        }, function (Exception $error) use ($deferred, $stream, $uri) {
            // pass custom RuntimeException through as-is, otherwise wrap in protocol error
            if (!$error instanceof RuntimeException) {
                $error = new RuntimeException(
                    'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)',
                    defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71,
                    $error
                );
            }

            $deferred->reject($error);
            $stream->close();
        });
    }

    private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri)
    {
        // do not resolve hostname. only try to convert to IP
        $ip = ip2long($host);

        // send IP or (0.0.0.1) if invalid
        $data = pack('C2nNC', 0x04, 0x01, $port, $ip === false ? 1 : $ip, 0x00);

        if ($ip === false) {
            // host is not a valid IP => send along hostname (SOCKS4a)
            $data .= $host . pack('C', 0x00);
        }

        $stream->write($data);

        return $reader->readBinary(array(
            'null'   => 'C',
            'status' => 'C',
            'port'   => 'n',
            'ip'     => 'N'
        ))->then(function ($data) use ($uri) {
            if ($data['null'] !== 0x00) {
                throw new Exception('Invalid SOCKS response');
            }
            if ($data['status'] !== 0x5a) {
                throw new RuntimeException(
                    'Connection to ' . $uri . ' failed because proxy refused connection with error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)',
                    defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
                );
            }
        });
    }

    private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri)
    {
        // protocol version 5
        $data = pack('C', 0x05);

        $auth = $this->auth;
        if ($auth === null) {
            // one method, no authentication
            $data .= pack('C2', 0x01, 0x00);
        } else {
            // two methods, username/password and no authentication
            $data .= pack('C3', 0x02, 0x02, 0x00);
        }
        $stream->write($data);

        $that = $this;

        return $reader->readBinary(array(
            'version' => 'C',
            'method'  => 'C'
        ))->then(function ($data) use ($auth, $stream, $reader, $uri) {
            if ($data['version'] !== 0x05) {
                throw new Exception('Version/Protocol mismatch');
            }

            if ($data['method'] === 0x02 && $auth !== null) {
                // username/password authentication requested and provided
                $stream->write($auth);

                return $reader->readBinary(array(
                    'version' => 'C',
                    'status'  => 'C'
                ))->then(function ($data) use ($uri) {
                    if ($data['version'] !== 0x01 || $data['status'] !== 0x00) {
                        throw new RuntimeException(
                            'Connection to ' . $uri . ' failed because proxy denied access with given authentication details (EACCES)',
                            defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
                        );
                    }
                });
            } else if ($data['method'] !== 0x00) {
                // any other method than "no authentication"
                throw new RuntimeException(
                    'Connection to ' . $uri . ' failed because proxy denied access due to unsupported authentication method (EACCES)',
                    defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
                );
            }
        })->then(function () use ($stream, $reader, $host, $port) {
            // do not resolve hostname. only try to convert to (binary/packed) IP
            $ip = @inet_pton($host);

            $data = pack('C3', 0x05, 0x01, 0x00);
            if ($ip === false) {
                // not an IP, send as hostname
                $data .= pack('C2', 0x03, strlen($host)) . $host;
            } else {
                // send as IPv4 / IPv6
                $data .= pack('C', (strpos($host, ':') === false) ? 0x01 : 0x04) . $ip;
            }
            $data .= pack('n', $port);

            $stream->write($data);

            return $reader->readBinary(array(
                'version' => 'C',
                'status'  => 'C',
                'null'    => 'C',
                'type'    => 'C'
            ));
        })->then(function ($data) use ($reader, $uri) {
            if ($data['version'] !== 0x05 || $data['null'] !== 0x00) {
                throw new Exception('Invalid SOCKS response');
            }
            if ($data['status'] !== 0x00) {
                // map limited list of SOCKS error codes to common socket error conditions
                // @link https://tools.ietf.org/html/rfc1928#section-6
                if ($data['status'] === Server::ERROR_GENERAL) {
                    throw new RuntimeException(
                        'Connection to ' . $uri . ' failed because proxy refused connection with general server failure (ECONNREFUSED)',
                        defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
                    );
                } elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) {
                    throw new RuntimeException(
                        'Connection to ' . $uri . ' failed because proxy denied access due to ruleset (EACCES)',
                        defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
                    );
                } elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) {
                    throw new RuntimeException(
                        'Connection to ' . $uri . ' failed because proxy reported network unreachable (ENETUNREACH)',
                        defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101
                    );
                } elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) {
                    throw new RuntimeException(
                        'Connection to ' . $uri . ' failed because proxy reported host unreachable (EHOSTUNREACH)',
                        defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113
                    );
                } elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) {
                    throw new RuntimeException(
                        'Connection to ' . $uri . ' failed because proxy reported connection refused (ECONNREFUSED)',
                        defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
                    );
                } elseif ($data['status'] === Server::ERROR_TTL) {
                    throw new RuntimeException(
                        'Connection to ' . $uri . ' failed because proxy reported TTL/timeout expired (ETIMEDOUT)',
                        defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110
                    );
                } elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) {
                    throw new RuntimeException(
                        'Connection to ' . $uri . ' failed because proxy does not support the CONNECT command (EPROTO)',
                        defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71
                    );
                } elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) {
                    throw new RuntimeException(
                        'Connection to ' . $uri . ' failed because proxy does not support this address type (EPROTO)',
                        defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71
                    );
                }

                throw new RuntimeException(
                    'Connection to ' . $uri . ' failed because proxy server refused connection with unknown error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)',
                    defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
                );
            }
            if ($data['type'] === 0x01) {
                // IPv4 address => skip IP and port
                return $reader->readLength(6);
            } elseif ($data['type'] === 0x03) {
                // domain name => read domain name length
                return $reader->readBinary(array(
                    'length' => 'C'
                ))->then(function ($data) use ($reader) {
                    // skip domain name and port
                    return $reader->readLength($data['length'] + 2);
                });
            } elseif ($data['type'] === 0x04) {
                // IPv6 address => skip IP and port
                return $reader->readLength(18);
            } else {
                throw new Exception('Invalid SOCKS reponse: Invalid address type');
            }
        });
    }
}


<?php

namespace Dialogflow\Action;

use Dialogflow\Action\Interfaces\LinkOutSuggestionInterface;
use Dialogflow\Action\Interfaces\QuestionInterface;
use Dialogflow\Action\Interfaces\ResponseInterface;
use Dialogflow\Action\Interfaces\SuggestionInterface;
use Dialogflow\Action\Responses\SimpleResponse;
use Dialogflow\RichMessage\Payload;

class Conversation
{
    /** @var null|string */
    protected $id;

    /** @var bool */
    protected $expectUserResponse = true;

    /** @var bool */
    protected $sandbox = false;

    /** @var Dialogflow\Action\Surface */
    protected $surface;

    /** @var null|Dialogflow\Action\AvailableSurfaces */
    protected $availableSurfaces;

    /** @var null|Dialogflow\Action\User */
    protected $user;

    /** @var null|Dialogflow\Action\Device */
    protected $device;

    /** @var null|Dialogflow\Action\Arguments */
    protected $arguments;

    /** @var array */
    protected $messages = [];

    /**
     * Constructor for Conversation object.
     *
     * @param array $payload original payload from google
     */
    public function __construct($payload)
    {
        if (isset($payload['conversation']['conversationId'])) {
            $this->id = $payload['conversation']['conversationId'];
        }

        if (isset($payload['isInSandbox'])) {
            $this->sandbox = $payload['isInSandbox'];
        }

        $this->surface = new Surface($payload['surface']);

        if (isset($payload['availableSurfaces'])) {
            $this->availableSurfaces = new AvailableSurfaces($payload['availableSurfaces']);
        }

        if (isset($payload['inputs'][0]['arguments'])) {
            $this->arguments = new Arguments($payload['inputs'][0]['arguments']);
        }

        if (isset($payload['user'])) {
            $this->user = new User($payload['user']);
        }

        if (isset($payload['device'])) {
            $this->device = new Device($payload['device']);
        }
    }

    /**
     * Add a message.
     *
     * @param string|Dialogflow\Action\Interfaces\ResponseInterface|Dialogflow\Action\Interfaces\QuestionInterface $message
     *
     * @return Conversation
     */
    public function add($message)
    {
        if (is_string($message)) {
            $this->messages[] = new SimpleResponse($message);
        } elseif ($message instanceof ResponseInterface) {
            $this->messages[] = $message;
        } elseif ($message instanceof QuestionInterface) {
            $this->messages[] = $message;
        }

        return $this;
    }

    /**
     * Asks to collect user's input.
     * Follow [the guidelines](https://developers.google.com/actions/policies/general-policies#user_experience) when prompting the user for a response.
     *
     * @param string|Dialogflow\Action\Interfaces\ResponseInterface|Dialogflow\Action\Interfaces\QuestionInterface $message
     *
     * @return Conversation
     */
    public function ask($message)
    {
        $this->expectUserResponse = true;

        $this->add($message);

        return $this;
    }

    /**
     * Have Assistant render the speech response and close the mic.
     *
     * @param string|Dialogflow\Action\Interfaces\ResponseInterface|Dialogflow\Action\Interfaces\QuestionInterface $message
     *
     * @return Conversation
     */
    public function close($message)
    {
        $this->expectUserResponse = false;

        $this->add($message);

        return $this;
    }

    /**
     * @return Dialogflow\Action\Surface
     */
    public function getSurface()
    {
        return $this->surface;
    }

    /**
     * @return Dialogflow\Action\AvailableSurfaces
     */
    public function getAvailableSurfaces()
    {
        return $this->availableSurfaces;
    }

    /**
     * @return Dialogflow\Action\User
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * @return null|Dialogflow\Action\Device
     */
    public function getDevice()
    {
        return $this->device;
    }

    /**
     * @return Dialogflow\Action\Arguments
     */
    public function getArguments()
    {
        return $this->arguments;
    }

    /**
     * Render response as array.
     *
     * @return array
     */
    public function render()
    {
        $out = [];

        $out['expectUserResponse'] = $this->expectUserResponse;

        $items = [];
        $suggestions = [];
        $linkOutSuggestion = null;
        foreach ($this->messages as $message) {
            if ($message instanceof ResponseInterface) {
                $item = $message->renderRichResponseItem();

                if ($item) {
                    $items[] = $item;
                }
            }

            if ($message instanceof SuggestionInterface) {
                $rendered = $message->renderRichResponseSuggestion();

                if (is_array($rendered)) {
                    foreach ($rendered as $suggestion) {
                        $suggestions[] = ['title' => $suggestion];
                    }
                } elseif (is_string($rendered)) {
                    $suggestions[] = ['title' => $rendered];
                } else {
                    // do nothing
                }
            }

            if ($message instanceof LinkOutSuggestionInterface) {
                $linkOutSuggestion = $message->renderRichResponseLinkOutSuggestion();
            }

            if ($message instanceof QuestionInterface) {
                if ($item = $message->renderRichResponseItem()) {
                    $items[] = $item;
                }
            }
        }

        $out['richResponse']['items'] = $items;

        if (count($suggestions) > 0) {
            $out['richResponse']['suggestions'] = $suggestions;
        }

        if ($linkOutSuggestion) {
            $out['richResponse']['linkOutSuggestion'] = $linkOutSuggestion;
        }

        $systemIntent = null;
        foreach ($this->messages as $message) {
            if ($message instanceof QuestionInterface) {
                $systemIntent = $message->renderSystemIntent();
            }
        }

        if ($systemIntent) {
            $out['systemIntent'] = $systemIntent;
        }

        return $out;
    }
}


<?php

namespace Dialogflow\RichMessage;

class Card extends RichMessage
{
    /**
     * Enum for Dialogflow v1 text message object
     * https://dialogflow.com/docs/reference/agent/message-objects.
     */
    const v1MessageObjectCard = 1;

    /** @var string */
    protected $title;

    /** @var string */
    protected $text;

    /** @var string */
    protected $imageUrl;

    /** @var array */
    protected $buttons = [];

    /**
     * Create a new Card instance.
     *
     * @return Dialogflow\Response\Card
     */
    public static function create()
    {
        return new self();
    }

    /**
     * Set the title for a Card.
     *
     * @param string $title
     */
    public function title($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Set the text for a Card.
     *
     * @param string $text
     */
    public function text($text)
    {
        $this->text = $text;

        return $this;
    }

    /**
     * Set the image for a Card.
     *
     * @param string $image image URL
     */
    public function image($imageUrl)
    {
        $this->imageUrl = $imageUrl;

        return $this;
    }

    /**
     * Set the button for a Card.
     *
     * @param string $buttonText button text
     * @param string $buttonUrl  button link URL
     */
    public function button($buttonText, $buttonUrl)
    {
        $this->buttons[] = [
            'buttonText' => $buttonText,
            'buttonUrl' =>  $buttonUrl,
        ];

        return $this;
    }

    /**
     * Render response as array for API V1.
     *
     * @return array
     */
    protected function renderV1()
    {
        if ('google' == $this->requestSource) {
            $out = [
                'type'     => 'basic_card',
                'platform' => $this->requestSource,
                'title'    => $this->title,
            ];

            if (! $this->text && ! $this->imageUrl) {
                $out['formattedText'] = ' '; // AoG requires text or image in card
            }

            if ($this->text) {
                $out['formattedText'] = $this->text;
            }

            if ($this->imageUrl) {
                $out['image'] = [
                    'url'               => $this->imageUrl,
                    'accessibilityText' => 'accessibility text',
                ];
            }

            if ($this->buttons) {
                $out['buttons'] = [];
                foreach ($this->buttons as $button) {
                    $out['buttons'][] =
                        [
                            'title'         => $button['buttonText'],
                            'openUrlAction' => [
                                'url' => $button['buttonUrl'],
                            ],
                        ];
                }
            }

            return $out;
        } else {
            $out = [
                'type'  => self::v1MessageObjectCard,
                'title' => $this->title,
            ];

            if ($this->text) {
                $out['subtitle'] = $this->text;
            }
            if ($this->imageUrl) {
                $out['imageUrl'] = $this->imageUrl;
            }

            // this is required in the response even if there are no buttons for some reason
            if ('slack' == $this->requestSource) {
                $out['buttons'] = [];
            }

            if ($this->buttons) {
                $out['buttons'] = [];
                foreach ($this->buttons as $button) {
                    $out['buttons'][] =
                        [
                            'text'     => $button['buttonText'],
                            'postback' => $button['buttonUrl'],
                        ];
                }
            }

            $out['platform'] = $this->requestSource;

            return $out;
        }
    }

    /**
     * Render response as array for API V2.
     *
     * @return array
     */
    protected function renderV2()
    {
        if ('google' == $this->requestSource) {
            $out = [
                'basicCard' => [
                    'title' => $this->title,
                ],
                'platform' => $this->v2PlatformMap[$this->requestSource],
            ];

            if ($this->text) {
                $out['basicCard']['formattedText'] = $this->text;
            }

            if ($this->imageUrl) {
                $out['basicCard']['image'] = [
                    'imageUri'          => $this->imageUrl,
                    'accessibilityText' => 'accessibility text',
                ];
            }

            if ($this->buttons) {
                $out['basicCard']['buttons'] = [];
                foreach ($this->buttons as $button) {
                    $out['basicCard']['buttons'][] =
                        [
                            'title'         => $button['buttonText'],
                            'openUriAction' => [
                                'uri' => $button['buttonUrl'],
                            ],
                        ];
                }
            }

            return $out;
        } else {
            $out = [
                'card' => [
                    'title' => $this->title,
                ],
                'platform' => $this->v2PlatformMap[$this->requestSource],
            ];

            if ($this->text) {
                $out['card']['subtitle'] = $this->text;
            }

            if ($this->imageUrl) {
                $out['card']['imageUri'] = $this->imageUrl;
            }

            if ($this->buttons) {
                $out['card']['buttons'] = [];
                foreach ($this->buttons as $button) {
                    $out['card']['buttons'][] =
                        [
                            'text'         => $button['buttonText'],
                            'postback' => $button['buttonUrl'],
                        ];
                }
            }

            return $out;
        }
    }
}


<?php

namespace Dialogflow\Action;

use Carbon\Carbon;
use Dialogflow\Action\Types\Location;

class Arguments
{
    /** @var array */
    protected $arguments = [];

    protected $mapArgumentName = [
        'CONFIRMATION' => 'getConfirmation',
        'PERMISSION'   => 'getPermission',
        'OPTION'       => 'getOption',
        'DATETIME'     => 'getDateTime',
        'PLACE'        => 'getPlace',
    ];

    /**
     * @param array $data request array
     */
    public function __construct($data)
    {
        $arguments = [];
        foreach ($data as $argumentData) {
            $arguments[] = $argumentData;
        }

        $this->arguments = $arguments;
    }

    /**
     * Get argument by name.
     *
     * @param string $name
     *
     * @return null|array|bool
     */
    public function get($name)
    {
        foreach ($this->arguments as $argument) {
            if ($argument['name'] == $name) {
                if (isset($this->mapArgumentName[$name])) {
                    return $this->{$this->mapArgumentName[$name]}($argument);
                } else {
                    return $argument;
                }
            }
        }
    }

    /**
     * Get confirmation argument.
     *
     * @param array $argument
     *
     * @return bool
     */
    private function getConfirmation($argument)
    {
        return $argument['boolValue'];
    }

    /**
     * Get permission argument.
     *
     * @param array $argument
     *
     * @return bool
     */
    private function getPermission($argument)
    {
        if (isset($argument['boolValue'])) {
            return $argument['boolValue'];
        } elseif (isset($argument['textValue'])) {
            return 'true' == $argument['textValue'];
        } else {
            return false;
        }
    }

    /**
     * Get option argument.
     *
     * @param array $argument
     *
     * @return string
     */
    private function getOption($argument)
    {
        return $argument['textValue'];
    }

    /**
     * Get datetime argument.
     *
     * @param array $argument
     *
     * @return Carbon\Carbon
     */
    private function getDateTime($argument)
    {
        $datetimeValue = $argument['datetimeValue'];

        $year = $datetimeValue['date']['year'];
        $month = $datetimeValue['date']['month'];
        $day = $datetimeValue['date']['day'];

        $hours = $datetimeValue['time']['hours'];
        $minutes = isset($datetimeValue['time']['minutes']) ? $datetimeValue['time']['minutes'] : 0;

        return Carbon::create($year, $month, $day, $hours, $minutes, 0);
    }

    /**
     * Get place argument.
     *
     * @param array $argument
     *
     * @return Dialogflow\Action\Types\Location
     */
    private function getPlace($argument)
    {
        return new Location($argument['placeValue']);
    }
}


<?php

namespace Dialogflow\RichMessage;

class Text extends RichMessage
{
    /**
     * Enum for Dialogflow v1 text message object
     * https://dialogflow.com/docs/reference/agent/message-objects.
     */
    const v1MessageObjectText = 0;

    /** @var string */
    protected $text;

    /** @var string */
    protected $ssml;

    /**
     * Create a new Text instance.
     *
     * @return Dialogflow\Response\Text
     */
    public static function create()
    {
        return new self();
    }

    /**
     * Set the text for a Text.
     *
     * @param string $text containing the text response content
     */
    public function text($text)
    {
        $this->text = $text;

        return $this;
    }

    /**
     * Set the SSML for a Text.
     *
     * @param string $text containing the SSML response content
     */
    public function ssml($ssml)
    {
        $this->ssml = $ssml;

        return $this;
    }

    /**
     * Render response as array for API V1.
     *
     * @return array
     */
    protected function renderV1()
    {
        if ('google' == $this->requestSource) {
            return [
                'type'         => 'simple_response',
                'platform'     => $this->requestSource,
                'textToSpeech' => $this->text,
                'displayText'  => $this->text,
            ];
        } else {
            $out = [
                'type'   => self::v1MessageObjectText,
                'speech' => $this->text,
            ];

            if (in_array($this->requestSource, $this->supportedRichMessagePlatforms)) {
                $out['platform'] = $this->requestSource;
            }

            return $out;
        }
    }

    /**
     * Render response as array for API V2.
     *
     * @return array
     */
    protected function renderV2()
    {
        if ('google' == $this->requestSource) {
            $out = [
                'platform'        => 'ACTIONS_ON_GOOGLE',
                'simpleResponses' => [
                    'simpleResponses' => [],
                ],
            ];

            if ($this->ssml) {
                $out['simpleResponses']['simpleResponses'][0]['ssml'] = $this->ssml;
            } else {
                $out['simpleResponses']['simpleResponses'][0]['textToSpeech'] = $this->text;
            }

            $out['simpleResponses']['simpleResponses'][0]['displayText'] = $this->text;

            return $out;
        } else {
            $out = [
                'text' => [
                    'text' => [$this->text],
                ],
            ];

            if (in_array($this->requestSource, $this->supportedRichMessagePlatforms)) {
                $out['platform'] = $this->v2PlatformMap[$this->requestSource];
            }

            return $out;
        }
    }
}


<?php

namespace Dialogflow\RichMessage;

use RuntimeException;

abstract class RichMessage
{
    const API_V1 = 1;
    const API_V2 = 2;

    protected $agentVersion;
    protected $requestSource;
    protected $fallbackText;

    protected $v2PlatformMap = [
        'unspecified'   => 'PLATFORM_UNSPECIFIED',
        'facebook'      => 'FACEBOOK',
        'slack'         => 'SLACK',
        'slack_testbot' => 'SLACK',
        'telegram'      => 'TELEGRAM',
        'kik'           => 'KIK',
        'skype'         => 'SKYPE',
        'line'          => 'LINE',
        'viber'         => 'VIBER',
        'google'        => 'ACTIONS_ON_GOOGLE',
    ];

    protected $supportedRichMessagePlatforms = [
        'facebook', 'slack', 'telegram', 'kik', 'skype', 'line', 'viber', 'google',
    ];

    /** @var array */
    protected $payload;

    /**
     * Check if request source support rich message.
     *
     * @return bool
     */
    public function doesSupportRichMessage()
    {
        return in_array($this->requestSource, $this->supportedRichMessagePlatforms);
    }

    /**
     * Set the fallback text if a request source doesn't support rich messages.
     */
    public function setFallbackText($text)
    {
        $this->fallbackText = $text;

        return $this;
    }

    /**
     * Alias of setFallbackText() to fit more inline with text(), button(), image(), etc.
     */
    public function fallbackText($text)
    {
        return $this->setFallbackText($text);
    }

    /**
     * Get the fallback text.
     *
     * @return  string
     */
    public function getFallbackText()
    {
        return $this->fallbackText;
    }

    protected function setAgentVersion($agentVersion)
    {
        if (self::API_V1 != $agentVersion && self::API_V2 != $agentVersion) {
            throw new RuntimeException('Invalid agent version');
        }

        $this->agentVersion = $agentVersion;

        return $this;
    }

    protected function setRequestSource($requestSource)
    {
        if (null == $requestSource) {
            $requestSource = 'unspecified';
        }

        $this->requestSource = $requestSource;

        return $this;
    }

    /**
     * Render response as array.
     *
     * @return array
     */
    public function render()
    {
        if (self::API_V1 == $this->agentVersion) {
            return $this->renderV1();
        } elseif (self::API_V2 == $this->agentVersion) {
            return $this->renderV2();
        } else {
            throw new RuntimeException('Invalid agent version');
        }
    }

    /**
     * Render response as array for API V1.
     *
     * @return array
     */
    protected function renderV1()
    {
    }

    /**
     * Render response as array for API V2.
     *
     * @return array
     */
    protected function renderV2()
    {
    }
}


<?php

namespace Dialogflow\RichMessage;

class Suggestion extends RichMessage
{
    /**
     * Enum for Dialogflow v1 text message object
     * https://dialogflow.com/docs/reference/agent/message-objects.
     */
    const v1MessageObjectSuggestions = 2;

    /** @var array */
    protected $replies = [];

    /**
     * Create a new Suggestion instance.
     *
     * @param string|array|null $reply
     *
     * @return Dialogflow\Response\Suggestion
     */
    public static function create($reply = null)
    {
        $suggestion = new self();

        if ($reply) {
            $suggestion->reply($reply);
        }

        return $suggestion;
    }

    /**
     * Set the reply for a Suggestion.
     *
     * @param string|array $reply
     */
    public function reply($reply)
    {
        if (is_string($reply)) {
            $this->replies = [$reply];
        } elseif (is_array($reply)) {
            $this->replies = $reply;
        }

        return $this;
    }

    /**
     * Render response as array for API V1.
     *
     * @return array
     */
    protected function renderV1()
    {
        if ('google' == $this->requestSource) {
            $out = [
                'suggestions' => [],
                'type'        => 'suggestion_chips',
                'platform'    => $this->requestSource,
            ];

            foreach ($this->replies as $i => $reply) {
                $out['suggestions'][$i]['title'] = $reply;
            }

            return $out;
        } else {
            $out = [
                'type' => self::v1MessageObjectSuggestions,
            ];

            $out['replies'] = $this->replies;

            $out['platform'] = $this->requestSource;

            return $out;
        }
    }

    /**
     * Render response as array for API V2.
     *
     * @return array
     */
    protected function renderV2()
    {
        if ('google' == $this->requestSource) {
            $out = [
                'suggestions' => [
                    'suggestions' => [],
                ],
            ];

            foreach ($this->replies as $i => $reply) {
                $out['suggestions']['suggestions'][$i]['title'] = $reply;
            }

            $out['platform'] = $this->v2PlatformMap[$this->requestSource];

            return $out;
        } else {
            $out = [
                'quickReplies' => [
                    'quickReplies' => $this->replies,
                ],
            ];

            $out['platform'] = $this->v2PlatformMap[$this->requestSource];

            return $out;
        }
    }
}


<?php

namespace Dialogflow\Action\Questions;

use Dialogflow\Action\Interfaces\QuestionInterface;
use Dialogflow\Action\Questions\Carousel\Option;

class Carousel implements QuestionInterface
{
    /** @var string */
    protected $imageDisplayOptions;

    /**
     * Create a new Carousel instance.
     *
     * @return Dialogflow\Action\Questions\Carousel
     */
    public static function create()
    {
        return new self();
    }

    /**
     * Type of image display option. Possible value: DEFAULT, WHITE and CROPPED.
     *
     * @param string $imageDisplayOptions
     *
     * @return Dialogflow\Action\Questions\Carousel
     */
    public function imageDisplayOptions($imageDisplayOptions)
    {
        $this->imageDisplayOptions = $imageDisplayOptions;

        return $this;
    }

    /**
     * Add Carousel option.
     *
     * @param \Dialogflow\Action\Questions\Carousel\Option $option
     *
     * @return Dialogflow\Action\Questions\Carousel
     */
    public function addOption(Option $option)
    {
        $this->options[] = $option;

        return $this;
    }

    /**
     * Render a single Rich Response item as array.
     *
     * @return null|array
     */
    public function renderRichResponseItem()
    {
    }

    /**
     * Render System Intent as array.
     *
     * @return null|array
     */
    public function renderSystemIntent()
    {
        $out = [];
        $items = [];

        foreach ($this->options as $option) {
            $items[] = $option->render();
        }

        $out['intent'] = 'actions.intent.OPTION';
        $out['data'] = [
            '@type'          => 'type.googleapis.com/google.actions.v2.OptionValueSpec',
            'carouselSelect' => [
                'imageDisplayOptions' => $this->imageDisplayOptions,
                'items'               => $items,
            ],
        ];

        return $out;
    }
}


<?php

namespace Dialogflow\Action\Responses;

class MediaObject
{
    /** @var string */
    protected $url;

    /** @var null|string */
    protected $name;

    /** @var null|string */
    protected $description;

    /** @var null|string */
    protected $icon;

    /** @var null|string */
    protected $image;

    /**
     * Create a new MediaObject instance.
     *
     * @param string $url Media URL
     *
     * @return Dialogflow\Action\Responses\MediaObject
     */
    public function __construct($url)
    {
        $this->url = $url;
    }

    /**
     * Create a new MediaObject instance.
     *
     * @param string $url Media URL
     *
     * @return Dialogflow\Action\Responses\MediaObject
     */
    public static function create($url)
    {
        return new self($url);
    }

    /**
     * Set the Name of the MediaObject.
     *
     * @param string $name
     *
     * @return Dialogflow\Action\Responses\MediaObject
     */
    public function name($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Set the description of the MediaObject.
     *
     * @param string $description
     *
     * @return Dialogflow\Action\Responses\MediaObject
     */
    public function description($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Set the icon of the MediaObject.
     *
     * @param string $icon
     *
     * @return Dialogflow\Action\Responses\MediaObject
     */
    public function icon($icon)
    {
        $this->icon = $icon;

        return $this;
    }

    /**
     * Set the large image of the MediaObject.
     *
     * @param string $image
     *
     * @return Dialogflow\Action\Responses\MediaObject
     */
    public function image($image)
    {
        $this->image = $image;

        return $this;
    }

    /**
     * Render as an array.
     *
     * @return null|array
     */
    public function render()
    {
        $mediaObject = [];
        $mediaObject['contentUrl'] = $this->url;

        if ($this->name) {
            $mediaObject['name'] = $this->name;
        }

        if ($this->description) {
            $mediaObject['description'] = $this->description;
        }

        if ($this->icon) {
            $mediaObject['icon'] = ['url' => $this->icon];
        }

        if ($this->image) {
            $mediaObject['largeImage'] = ['url' => $this->image];
        }

        return $mediaObject;
    }
}


<?php

namespace Dialogflow\Action\Responses;

use Dialogflow\Action\Interfaces\ResponseInterface;

class MediaResponse implements ResponseInterface
{
    /** @var array */
    protected $mediaObjects = [];

    /**
     * Create a new MediaResponse instance.
     *
     * @param null|Dialogflow\Action\Responses\MediaObject $mediaObject Media objects
     *
     * @return Dialogflow\Action\Responses\MediaResponse
     */
    public function __construct($mediaObject = null)
    {
        if ($mediaObject instanceof MediaObject) {
            $this->mediaObjects[] = $mediaObject;
        }
    }

    /**
     * Create a new MediaResponse instance.
     *
     * @param null|Dialogflow\Action\Responses\MediaObject $mediaObject Media objects
     *
     * @return Dialogflow\Action\Responses\MediaResponse
     */
    public static function create($mediaObject = null)
    {
        return new self($mediaObject);
    }

    /**
     * Add MediaObject.
     *
     * @param Dialogflow\Action\Responses\MediaObject $mediaObject
     *
     * @return Dialogflow\Action\Responses\MediaResponse
     */
    public function add($mediaObject)
    {
        $this->mediaObjects[] = $mediaObject;

        return $this;
    }

    /**
     * Render a single Rich Response item as array.
     *
     * @return null|array
     */
    public function renderRichResponseItem()
    {
        $out = [];
        $mediaResponse = [];

        $mediaResponse['mediaType'] = 'AUDIO';

        $mediaObjects = [];
        foreach ($this->mediaObjects as $mediaObject) {
            $mediaObjects[] = $mediaObject->render();
        }

        if (count($mediaObjects) > 0) {
            $mediaResponse['mediaObjects'] = $mediaObjects;
        }

        $out['mediaResponse'] = $mediaResponse;

        return $out;
    }
}


<?php

namespace Dialogflow\Action\Responses;

use Dialogflow\Action\Interfaces\ResponseInterface;

class SimpleResponse implements ResponseInterface
{
    /** @var null|string */
    protected $displayText;

    /** @var null|string */
    protected $ssml;

    /** @var null|string */
    protected $textToSpeech;

    /**
     * Create a new Simple Response instance.
     *
     * @param string|array $options (optional) options
     *
     * @return Dialogflow\Action\Responses\SimpleResponse
     */
    public function __construct($options = null)
    {
        if (is_string($options)) {
            $this->textToSpeech = $options;
        } elseif (is_array($options)) {
            $this->displayText = isset($options['displayText']) ? $options['displayText'] : null;
            $this->ssml = isset($options['ssml']) ? $options['ssml'] : null;
            $this->textToSpeech = isset($options['textToSpeech']) ? $options['textToSpeech'] : null;
        }
    }

    /**
     * Create a new instance.
     *
     *
     * @param string|array $options (optional) options
     *
     * @return Dialogflow\Action\Responses\SimpleResponse
     */
    public static function create($options = null)
    {
        return new self($options);
    }

    /**
     * Set display text.
     *
     * @param string $displayText
     *
     * @return Dialogflow\Action\Responses\SimpleResponse
     */
    public function displayText($displayText)
    {
        $this->displayText = $displayText;

        return $this;
    }

    /**
     * Set ssml.
     *
     * @param string $ssml
     *
     * @return Dialogflow\Action\Responses\SimpleResponse
     */
    public function ssml($ssml)
    {
        $this->ssml = $ssml;

        return $this;
    }

    /**
     * Set text to speech.
     *
     * @param string $textToSpeech
     *
     * @return Dialogflow\Action\Responses\SimpleResponse
     */
    public function textToSpeech($textToSpeech)
    {
        $this->textToSpeech = $textToSpeech;

        return $this;
    }

    /**
     * Render a single Rich Response item as array.
     *
     * @return null|array
     */
    public function renderRichResponseItem()
    {
        $out = [];
        $simpleResponse = [];

        if ($this->ssml) {
            $simpleResponse['ssml'] = $this->ssml;
        }

        if ($this->displayText) {
            $simpleResponse['displayText'] = $this->displayText;
        }

        if ($this->textToSpeech) {
            $simpleResponse['textToSpeech'] = $this->textToSpeech;
        }

        $out['simpleResponse'] = $simpleResponse;

        return $out;
    }
}


<?php

namespace Dialogflow;

use Dialogflow\Action\Conversation;
use Dialogflow\RichMessage\Payload;
use Dialogflow\RichMessage\RichMessage;
use Dialogflow\RichMessage\Text;
use RuntimeException;

class WebhookClient extends RichMessage
{
    /** @var string */
    protected $agentVersion;

    /** @var string */
    protected $intent;

    /** @var string|null */
    protected $action;

    /** @var string */
    protected $session;

    /** @var array */
    protected $parameters = [];

    /** @var array|null */
    protected $contexts;

    /** @var string */
    protected $requestSource;

    /** @var array|null */
    protected $originalRequest;

    /** @var string */
    protected $query;

    /** @var string */
    protected $locale;

    /** @var array */
    protected $messages = [];

    /** @var string */
    protected $text;

    /** @var array */
    protected $outgoingContexts = [];

    /**
     * Constructor for WebhookClient object.
     *
     * @param array $data request data payload from Dialogflow
     */
    public function __construct($data)
    {
        if (isset($data['result'])) {
            $this->parseRequestV1($data);
        } elseif (isset($data['queryResult'])) {
            $this->parseRequestV2($data);
        } else {
            throw new RuntimeException('Invalid Dialogflow request');
        }
    }

    /**
     * @param array $data
     *
     * @return WebhookClient
     */
    public static function fromData($data)
    {
        return new self($data);
    }

    private function parseRequestV1($data)
    {
        $this->agentVersion = 1;

        $this->intent = $data['result']['metadata']['intentName'];
        $this->action = (isset($data['result']['action'])) ? $data['result']['action'] : null;
        $this->session = $data['sessionId'];
        $this->parameters = $data['result']['parameters'];

        if (isset($data['result']['contexts'])) {
            $this->contexts = [];
            foreach ($data['result']['contexts'] as $arrContext) {
                $this->contexts[] = new Context($arrContext['name'], $arrContext['lifespan'], $arrContext['parameters']);
            }
        }

        if (isset($data['originalRequest'])) {
            $originalRequest = $data['originalRequest'];

            if (isset($originalRequest['data'])) {
                // Rename 'data' attr to 'payload' to be consistent with v2
                if (isset($originalRequest['data'])) {
                    $originalRequest['payload'] = $originalRequest['data'];
                    unset($originalRequest['data']);
                }
                $this->originalRequest = $originalRequest;
            }

            if (isset($originalRequest['source'])) {
                $this->requestSource = $originalRequest['source'];
            } elseif (isset($originalRequest['payload']['source'])) {
                $this->requestSource = $originalRequest['data']['source'];
            }
        }

        if (! $this->requestSource && isset($data['result']['source'])) {
            $this->requestSource = $data['result']['source'];
        }

        $this->query = $data['result']['resolvedQuery'];
        $this->locale = $data['lang'];
    }

    private function parseRequestV2($data)
    {
        $this->agentVersion = 2;

        $this->intent = $data['queryResult']['intent']['displayName'];
        $this->action = (isset($data['queryResult']['action'])) ? $data['queryResult']['action'] : null;
        $this->session = $data['session'];
        $this->parameters = $data['queryResult']['parameters'];

        if (isset($data['queryResult']['outputContexts'])) {
            $this->contexts = [];
            foreach ($data['queryResult']['outputContexts'] as $arrContext) {
                $name = substr($arrContext['name'], strlen($this->session) + strlen('/contexts/'));
                $lifespan = (isset($arrContext['lifespanCount'])) ? $arrContext['lifespanCount'] : 0;
                $parameters = (isset($arrContext['parameters'])) ? $arrContext['parameters'] : [];
                $this->contexts[] = new Context($name, $lifespan, $parameters);
            }
        }

        if (isset($data['originalDetectIntentRequest'])) {
            $this->originalRequest = $data['originalDetectIntentRequest'];

            if (isset($this->originalRequest['source'])) {
                $this->requestSource = $this->originalRequest['source'];
            } elseif (isset($this->originalRequest['payload']['source'])) {
                $this->requestSource = $this->originalRequest['payload']['source'];
            }
        }

        $this->query = $data['queryResult']['queryText'];
        $this->locale = $data['queryResult']['languageCode'];
    }

    /**
     * The agent version (v1 or v2) based on Dialogflow webhook request.
     * Reference: https://dialogflow.com/docs/reference/v2-comparison.
     *
     * @return string
     */
    public function getAgentVersion()
    {
        return $this->agentVersion;
    }

    /**
     * Get intent name.
     * Reference: https://dialogflow.com/docs/intents.
     *
     * @return string
     */
    public function getIntent()
    {
        return $this->intent;
    }

    /**
     * Get action name.
     * Reference: https://dialogflow.com/docs/actions-and-parameters.
     *
     * @return string
     */
    public function getAction()
    {
        return $this->action;
    }

    /**
     * Get session id.
     * Reference: https://dialogflow.com/docs/reference/api-v2/rest/v2beta1/WebhookRequest#FIELDS.session.
     *
     * @return string
     */
    public function getSession()
    {
        return $this->session;
    }

    /**
     * Get parameters.
     * Reference: https://dialogflow.com/docs/actions-and-parameters.
     *
     * @return array
     */
    public function getParameters()
    {
        return $this->parameters;
    }

    /**
     * Get contexts.
     * Reference: https://dialogflow.com/docs/actions-and-parameters.
     *
     * @return array|null
     */
    public function getContexts()
    {
        return $this->contexts;
    }

    /**
     * Convenience method to get a Dialogflow context by name.
     * Reference: https://dialogflow.com/docs/contexts.
     *
     * @param string $name context name
     *
     * @return null|Context
     */
    public function getContext($name)
    {
        if (is_array($this->contexts)) {
            foreach ($this->contexts as $context) {
                if ($context->getName() == $name) {
                    return $context;
                }
            }
        }
    }

    /**
     * Get request source.
     * Reference: https://dialogflow.com/docs/reference/agent/query#query_parameters_and_json_fields.
     *
     * @return string
     */
    public function getRequestSource()
    {
        return $this->requestSource;
    }

    /**
     * Dialogflow original request object from detectIntent/query or platform integration (Google Assistant, Slack, etc.) in the request or null if no value.
     * Reference: https://dialogflow.com/docs/reference/agent/query#query_parameters_and_json_fields.
     *
     * @return array|null
     */
    public function getOriginalRequest()
    {
        return $this->originalRequest;
    }

    /**
     * Original user query as indicated by Dialogflow or null if no value.
     *
     * @return string
     */
    public function getQuery()
    {
        return $this->query;
    }

    /**
     * Original request language code (i.e. "en").
     *
     * @return string
     */
    public function getLocale()
    {
        return $this->locale;
    }

    /**
     * Response to incoming request.
     *
     * @param string|\Dialogflow\Richmessage|\Dialogflow\Action\Conversation $message
     *
     * @return \Dialogflow\WebhookClient
     */
    public function reply($message)
    {
        if (is_string($message)) {
            $this->messages[] = Text::create()
                ->text($message)
                ->setAgentVersion($this->agentVersion)
                ->setRequestSource($this->requestSource);

            if (! $this->doesSupportRichMessage()) {
                $this->text = $message;
            }
        } elseif ($message instanceof RichMessage) {
            if (! $this->doesSupportRichMessage()) {
                $this->text = $message->getFallbackText();
            }

            $message->setAgentVersion($this->agentVersion)
                ->setRequestSource($this->requestSource);

            $this->messages[] = $message;
        } elseif ($message instanceof Conversation) {
            $this->messages[] = Payload::create($message->render())
                ->setAgentVersion($this->agentVersion)
                ->setRequestSource($this->requestSource);
        }

        return $this;
    }

    /**
     * Get all Dialogflow outgoing contexts.
     * Reference: https://dialogflow.com/docs/contexts.
     *
     * @return array
     */
    public function getOutgoingContexts()
    {
        return $this->outgoingContexts;
    }

    /**
     * Get a Dialogflow outgoing context.
     * Reference: https://dialogflow.com/docs/contexts.
     *
     * @param string $name context name
     *
     * @return null|Context
     */
    public function getOutgoingContext($name)
    {
        foreach ($this->outgoingContexts as $outgoingContext) {
            if ($outgoingContext->getName() == $name) {
                return $outgoingContext;
            }
        }
    }

    /**
     * Set a new Dialogflow outgoing context.
     * Reference: https://dialogflow.com/docs/contexts.
     *
     * @param string|array|\Dialogflow\Context $context
     *
     * @return \Dialogflow\WebhookClient
     */
    public function setOutgoingContext($context)
    {
        if (is_string($context)) {
            $outgoingContext = new Context($context);
        } elseif (is_array($context)) {
            if (! isset($context['name'])) {
                throw new RuntimeException('Context must have a name');
            }

            $name = $context['name'];

            $lifespan = 1;
            if (isset($context['lifespan'])) {
                $lifespan = is_numeric($context['lifespan']) ? $context['lifespan'] : null;
            }

            $parameters = [];
            if (isset($context['parameters'])) {
                $parameters = is_array($context['parameters']) ? $context['parameters'] : null;
            }

            $outgoingContext = new Context($name, $lifespan, $parameters);
        } elseif ($context instanceof Context) {
            $outgoingContext = $context;
        } else {
            throw new RuntimeException('Context must be provided');
        }

        $this->outgoingContexts[] = $outgoingContext;

        return $this;
    }

    /**
     * Clear an existing outgoing context.
     * Reference: https://dialogflow.com/docs/contexts.
     *
     * @param string $contextName
     *
     * @return \Dialogflow\WebhookClient
     */
    public function clearOutgoingContext($contextName)
    {
        foreach ($this->outgoingContexts as $i => $context) {
            if ($context->getName() == $contextName) {
                unset($this->outgoingContexts[$i]);
            }
        }

        return $this;
    }

    /**
     * Clear all existing outgoing contexts.
     * Reference: https://dialogflow.com/docs/contexts.
     *
     * @return \Dialogflow\WebhookClient
     */
    public function clearOutgoingContexts()
    {
        $this->outgoingContexts = [];

        return $this;
    }

    /**
     * Replace all Dialogflow outgoing contexts.
     * Reference: https://dialogflow.com/docs/contexts.
     *
     * @param array $contexts
     *
     * @return \Dialogflow\WebhookClient
     */
    public function setOutgoingContexts($contexts)
    {
        $this->outgoingContexts = $contexts;

        return $this;
    }

    /**
     * Get Actions on Google DialogflowConversation object.
     *
     * @return null|\Dialogflow\Action\Conversation
     */
    public function getActionConversation()
    {
        if ('google' == $this->requestSource) {
            return new Conversation($this->originalRequest['payload']);
        } else {
            return;
        }
    }

    /**
     * Render response as array for API V1.
     *
     * @return array
     */
    protected function renderV1()
    {
        $out = ['messages' => []];

        $messages = [];

        foreach ($this->messages as $message) {
            if ($message instanceof Payload) {
                $out['data'] = $message->render();
            } else {
                $messages[] = $message->render();
            }
        }

        $out['messages'] = $messages;

        if ($this->text) {
            $out['speech'] = $this->text;
        }

        $outgoingContexts = [];
        foreach ($this->outgoingContexts as $outgoingContext) {
            $outContexts = ['name' => $outgoingContext->getName()];

            if ($outgoingContext->getLifespan()) {
                $outContexts['lifespan'] = $outgoingContext->getLifespan();
            }

            if ($outgoingContext->getParameters()) {
                $outContexts['parameters'] = $outgoingContext->getParameters();
            }

            $outgoingContexts[] = $outContexts;
        }
        $out['contextOut'] = $outgoingContexts;

        return $out;
    }

    /**
     * Render response as array for API V2.
     *
     * @param string $session session
     *
     * @return array
     */
    protected function renderV2()
    {
        $out = [];

        $messages = [];

        foreach ($this->messages as $message) {
            if ($message instanceof Payload) {
                $out['payload'] = $message->render();
            } else {
                $messages[] = $message->render();
            }
        }

        if (count($messages)) {
            $out['fulfillmentMessages'] = $messages;
        }

        if ($this->text) {
            $out['fulfillmentText'] = $this->text;
        }

        $outgoingContexts = [];
        foreach ($this->outgoingContexts as $outgoingContext) {
            $outContexts = [
                'name' => $this->session.'/contexts/'.$outgoingContext->getName(),
            ];

            if ($outgoingContext->getLifespan()) {
                $outContexts['lifespanCount'] = $outgoingContext->getLifespan();
            }

            if ($outgoingContext->getParameters()) {
                $outContexts['parameters'] = $outgoingContext->getParameters();
            }

            $outgoingContexts[] = $outContexts;
        }
        $out['outputContexts'] = $outgoingContexts;

        return $out;
    }
}


<?php

namespace Dialogflow\Action\Questions;

use Dialogflow\Action\Interfaces\QuestionInterface;

class DateTime implements QuestionInterface
{
    /** @var string */
    protected $requestDateTimeText;

    /** @var string */
    protected $requestDateText;

    /** @var string */
    protected $requestTimeText;

    /**
     * Constructor for DateTime object.
     *
     * @param string $requestDateTimeText initial question
     * @param string $requestDateText     follow up question about the exact date
     * @param string $requestTimeText     follow up question about the exact time
     *
     * @return Dialogflow\Action\Questions\DateTime
     */
    public function __construct($requestDateTimeText, $requestDateText, $requestTimeText)
    {
        $this->requestDateTimeText = $requestDateTimeText;
        $this->requestDateText = $requestDateText;
        $this->requestTimeText = $requestTimeText;
    }

    /**
     * Render a single Rich Response item as array.
     *
     * @return null|array
     */
    public function renderRichResponseItem()
    {
        $out = [];

        $out['simpleResponse'] = ['textToSpeech' => 'PLACEHOLDER'];

        return $out;
    }

    /**
     * Render System Intent as array.
     *
     * @return null|array
     */
    public function renderSystemIntent()
    {
        $out = [];

        $out['intent'] = 'actions.intent.DATETIME';
        $out['data'] = [
            '@type'      => 'type.googleapis.com/google.actions.v2.DateTimeValueSpec',
            'dialogSpec' => [
                'requestDatetimeText' => $this->requestDateTimeText,
                'requestDateText'     => $this->requestDateText,
                'requestTimeText'     => $this->requestTimeText,
            ],
        ];

        return $out;
    }
}


<?php

namespace Dialogflow\Action\Responses;

use Dialogflow\Action\Interfaces\ResponseInterface;

class BasicCard implements ResponseInterface
{
    /** @var string */
    protected $title;

    /** @var string */
    protected $formattedText;

    /** @var string */
    protected $imageUrl;

    /** @var string */
    protected $accessibilityText;

    /** @var array */
    protected $buttons = [];

    /**
     * Create a new Basic Card instance.
     *
     * @return Dialogflow\Action\Responses\BasicCard
     */
    public static function create()
    {
        return new self();
    }

    /**
     * Set the title for a Card.
     *
     * @param string $title
     *
     * @return Dialogflow\Action\Responses\BasicCard
     */
    public function title($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Set the formattedText for a Card.
     *
     * @param string $formattedText
     *
     * @return Dialogflow\Action\Responses\BasicCard
     */
    public function formattedText($formattedText)
    {
        $this->formattedText = $formattedText;

        return $this;
    }

    /**
     * Set the image for a Card.
     *
     * @param string $imageUrl          image URL
     * @param string $accessibilityText (optional) accessibility text of the image
     *
     * @return Dialogflow\Action\Responses\BasicCard
     */
    public function image($imageUrl, $accessibilityText = null)
    {
        $this->imageUrl = $imageUrl;
        $this->accessibilityText = $accessibilityText;

        return $this;
    }

    /**
     * Set the button for a Card.
     *
     * @param string $buttonText button text
     * @param string $buttonUrl  button link URL
     *
     * @return Dialogflow\Action\Responses\BasicCard
     */
    public function button($buttonText, $buttonUrl)
    {
        $this->buttons[] = [
            'buttonText' => $buttonText,
            'buttonUrl' =>  $buttonUrl,
        ];

        return $this;
    }

    /**
     * Render a single Rich Response item as array.
     *
     * @return null|array
     */
    public function renderRichResponseItem()
    {
        $out = [];
        $basicCard = [];

        if ($this->title) {
            $basicCard['title'] = $this->title;
        }

        if ($this->formattedText) {
            $basicCard['formattedText'] = $this->formattedText;
        }

        if ($this->imageUrl) {
            $basicCard['image'] = [
                'url'               => $this->imageUrl,
                'accessibilityText' => ($this->accessibilityText) ? $this->accessibilityText : 'accessibility text',
            ];
        }

        if ($this->buttons) {
            foreach ($this->buttons as $button) {
                $basicCard['buttons'][] =
                    [
                        'title'         => $button['buttonText'],
                        'openUrlAction' => [
                            'url' => $button['buttonUrl'],
                        ],
                    ];
            }
        }

        $out['basicCard'] = $basicCard;

        return $out;
    }
}


<?php

namespace Dialogflow\Action\Responses\BrowseCarousel;

class Option
{
    /** @var string */
    protected $title;

    /** @var string */
    protected $description;

    /** @var string */
    protected $footer;

    /** @var string */
    protected $openUrlAction;

    /** @var string */
    protected $imageUrl;

    /** @var string */
    protected $accessibilityText;

    /**
     * Create a new Option instance.
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel\Option
     */
    public static function create()
    {
        return new self();
    }

    /**
     * Option title.
     *
     * @param string $title
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel\Option
     */
    public function title($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Option description.
     *
     * @param string $description
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel\Option
     */
    public function description($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Hero image for the carousel item. Optional.
     *
     * @param string $footer
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel\Option
     */
    public function footer($footer)
    {
        $this->footer = $footer;

        return $this;
    }

    /**
     * URL of the document associated with the carousel item.
     * The document can contain HTML content or, if \"url_type_hint\" is set to
     * AMP_CONTENT, AMP content.
     * Required.
     *
     * @param string $openUrlAction
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel\Option
     */
    public function url($openUrlAction)
    {
        $this->openUrlAction = $openUrlAction;

        return $this;
    }

    /**
     * Set the image for Browsing Carousel option.
     *
     * @param string $imageUrl          image URL
     * @param string $accessibilityText (optional) accessibility text of the image
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel\Option
     */
    public function image($imageUrl, $accessibilityText = null)
    {
        $this->imageUrl = $imageUrl;
        $this->accessibilityText = $accessibilityText;

        return $this;
    }

    /**
     * Render response as array.
     *
     * @return array
     */
    public function render()
    {
        $out = [];
        $optionInfo = [];

        if ($this->title) {
            $out['title'] = $this->title;
        }

        if ($this->description) {
            $out['description'] = $this->description;
        }

        if ($this->footer) {
            $out['footer'] = $this->footer;
        }

        if ($this->imageUrl) {
            $out['image'] = [
                'url'               => $this->imageUrl,
                'accessibilityText' => ($this->accessibilityText) ? $this->accessibilityText : 'accessibility text',
            ];
        }

        $out['openUrlAction'] = [
            'url' => $this->openUrlAction,
        ];

        return $out;
    }
}


<?php

namespace Dialogflow\Action\Responses;

use Dialogflow\Action\Interfaces\ResponseInterface;
use Dialogflow\Action\Responses\BrowseCarousel\Option;

class BrowseCarousel implements ResponseInterface
{
    /** @var string */
    protected $imageDisplayOptions;

    /** @var array */
    protected $options;

    /**
     * Create a new BrowseCarousel instance.
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel
     */
    public static function create()
    {
        return new self();
    }

    /**
     * Type of image display option. Possible value: DEFAULT, WHITE and CROPPED.
     *
     * @param string $imageDisplayOptions
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel
     */
    public function imageDisplayOptions($imageDisplayOptions)
    {
        $this->imageDisplayOptions = $imageDisplayOptions;

        return $this;
    }

    /**
     * Add BrowseCarousel option.
     *
     * @param \Dialogflow\Action\Responses\BrowseCarousel\Option $option
     *
     * @return Dialogflow\Action\Responses\BrowseCarousel
     */
    public function addOption(Option $option)
    {
        $this->options[] = $option;

        return $this;
    }

    /**
     * Render a single Rich Response item as array.
     *
     * @return null|array
     */
    public function renderRichResponseItem()
    {
        $out = [];
        $carouselBrowse = [];

        if ($this->imageDisplayOptions) {
            $carouselBrowse['imageDisplayOptions'] = $this->imageDisplayOptions;
        }

        $items = [];
        foreach ($this->options as $option) {
            $items[] = $option->render();
        }

        $carouselBrowse['items'] = $items;

        $out['carouselBrowse'] = $carouselBrowse;

        return $out;
    }
}