<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\HTTP;

use Config\UserAgents;

/**
 * Abstraction for an HTTP user agent
 */
class UserAgent
{
	/**
	 * Current user-agent
	 *
	 * @var string
	 */
	protected $agent = null;

	/**
	 * Flag for if the user-agent belongs to a browser
	 *
	 * @var boolean
	 */
	protected $isBrowser = false;

	/**
	 * Flag for if the user-agent is a robot
	 *
	 * @var boolean
	 */
	protected $isRobot = false;

	/**
	 * Flag for if the user-agent is a mobile browser
	 *
	 * @var boolean
	 */
	protected $isMobile = false;

	/**
	 * Holds the config file contents.
	 *
	 * @var \Config\UserAgents
	 */
	protected $config;

	/**
	 * Current user-agent platform
	 *
	 * @var string
	 */
	protected $platform = '';

	/**
	 * Current user-agent browser
	 *
	 * @var string
	 */
	protected $browser = '';

	/**
	 * Current user-agent version
	 *
	 * @var string
	 */
	protected $version = '';

	/**
	 * Current user-agent mobile name
	 *
	 * @var string
	 */
	protected $mobile = '';

	/**
	 * Current user-agent robot name
	 *
	 * @var string
	 */
	protected $robot = '';

	/**
	 * HTTP Referer
	 *
	 * @var mixed
	 */
	protected $referrer;

	//--------------------------------------------------------------------

	/**
	 * Constructor
	 *
	 * Sets the User Agent and runs the compilation routine
	 *
	 * @param null $config
	 */
	public function __construct($config = null)
	{
		if (is_null($config))
		{
			$this->config = new UserAgents();
		}

		if (isset($_SERVER['HTTP_USER_AGENT']))
		{
			$this->agent = trim($_SERVER['HTTP_USER_AGENT']);
			$this->compileData();
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Is Browser
	 *
	 * @param string $key
	 *
	 * @return boolean
	 */
	public function isBrowser(string $key = null): bool
	{
		if (! $this->isBrowser)
		{
			return false;
		}

		// No need to be specific, it's a browser
		if ($key === null)
		{
			return true;
		}

		// Check for a specific browser
		return (isset($this->config->browsers[$key]) && $this->browser === $this->config->browsers[$key]);
	}

	//--------------------------------------------------------------------

	/**
	 * Is Robot
	 *
	 * @param string $key
	 *
	 * @return boolean
	 */
	public function isRobot(string $key = null): bool
	{
		if (! $this->isRobot)
		{
			return false;
		}

		// No need to be specific, it's a robot
		if ($key === null)
		{
			return true;
		}

		// Check for a specific robot
		return (isset($this->config->robots[$key]) && $this->robot === $this->config->robots[$key]);
	}

	//--------------------------------------------------------------------

	/**
	 * Is Mobile
	 *
	 * @param string $key
	 *
	 * @return boolean
	 */
	public function isMobile(string $key = null): bool
	{
		if (! $this->isMobile)
		{
			return false;
		}

		// No need to be specific, it's a mobile
		if ($key === null)
		{
			return true;
		}

		// Check for a specific robot
		return (isset($this->config->mobiles[$key]) && $this->mobile === $this->config->mobiles[$key]);
	}

	//--------------------------------------------------------------------

	/**
	 * Is this a referral from another site?
	 *
	 * @return boolean
	 */
	public function isReferral(): bool
	{
		if (! isset($this->referrer))
		{
			if (empty($_SERVER['HTTP_REFERER']))
			{
				$this->referrer = false;
			}
			else
			{
				$referer_host = @parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
				$own_host     = parse_url(\base_url(), PHP_URL_HOST);

				$this->referrer = ($referer_host && $referer_host !== $own_host);
			}
		}

		return $this->referrer;
	}

	//--------------------------------------------------------------------

	/**
	 * Agent String
	 *
	 * @return string
	 */
	public function getAgentString(): string
	{
		return $this->agent;
	}

	//--------------------------------------------------------------------

	/**
	 * Get Platform
	 *
	 * @return string
	 */
	public function getPlatform(): string
	{
		return $this->platform;
	}

	//--------------------------------------------------------------------

	/**
	 * Get Browser Name
	 *
	 * @return string
	 */
	public function getBrowser(): string
	{
		return $this->browser;
	}

	//--------------------------------------------------------------------

	/**
	 * Get the Browser Version
	 *
	 * @return string
	 */
	public function getVersion(): string
	{
		return $this->version;
	}

	//--------------------------------------------------------------------

	/**
	 * Get The Robot Name
	 *
	 * @return string
	 */
	public function getRobot(): string
	{
		return $this->robot;
	}
	//--------------------------------------------------------------------

	/**
	 * Get the Mobile Device
	 *
	 * @return string
	 */
	public function getMobile(): string
	{
		return $this->mobile;
	}

	//--------------------------------------------------------------------

	/**
	 * Get the referrer
	 *
	 * @return string
	 */
	public function getReferrer(): string
	{
		return empty($_SERVER['HTTP_REFERER']) ? '' : trim($_SERVER['HTTP_REFERER']);
	}

	//--------------------------------------------------------------------

	/**
	 * Parse a custom user-agent string
	 *
	 * @param string $string
	 *
	 * @return void
	 */
	public function parse(string $string)
	{
		// Reset values
		$this->isBrowser = false;
		$this->isRobot   = false;
		$this->isMobile  = false;
		$this->browser   = '';
		$this->version   = '';
		$this->mobile    = '';
		$this->robot     = '';

		// Set the new user-agent string and parse it, unless empty
		$this->agent = $string;

		if (! empty($string))
		{
			$this->compileData();
		}
	}
	//--------------------------------------------------------------------

	/**
	 * Compile the User Agent Data
	 *
	 * @return void
	 */
	protected function compileData()
	{
		$this->setPlatform();

		foreach (['setRobot', 'setBrowser', 'setMobile'] as $function)
		{
			if ($this->$function() === true)
			{
				break;
			}
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Set the Platform
	 *
	 * @return boolean
	 */
	protected function setPlatform(): bool
	{
		if (is_array($this->config->platforms) && $this->config->platforms)
		{
			foreach ($this->config->platforms as $key => $val)
			{
				if (preg_match('|' . preg_quote($key) . '|i', $this->agent))
				{
					$this->platform = $val;

					return true;
				}
			}
		}

		$this->platform = 'Unknown Platform';

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Set the Browser
	 *
	 * @return boolean
	 */
	protected function setBrowser(): bool
	{
		if (is_array($this->config->browsers) && $this->config->browsers)
		{
			foreach ($this->config->browsers as $key => $val)
			{
				if (preg_match('|' . $key . '.*?([0-9\.]+)|i', $this->agent, $match))
				{
					$this->isBrowser = true;
					$this->version   = $match[1];
					$this->browser   = $val;
					$this->setMobile();

					return true;
				}
			}
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Set the Robot
	 *
	 * @return boolean
	 */
	protected function setRobot(): bool
	{
		if (is_array($this->config->robots) && $this->config->robots)
		{
			foreach ($this->config->robots as $key => $val)
			{
				if (preg_match('|' . preg_quote($key) . '|i', $this->agent))
				{
					$this->isRobot = true;
					$this->robot   = $val;
					$this->setMobile();

					return true;
				}
			}
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Set the Mobile Device
	 *
	 * @return boolean
	 */
	protected function setMobile(): bool
	{
		if (is_array($this->config->mobiles) && $this->config->mobiles)
		{
			foreach ($this->config->mobiles as $key => $val)
			{
				if (false !== (stripos($this->agent, $key)))
				{
					$this->isMobile = true;
					$this->mobile   = $val;

					return true;
				}
			}
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Outputs the original Agent String when cast as a string.
	 *
	 * @return string
	 */
	public function __toString(): string
	{
		return $this->getAgentString();
	}

}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Database\Postgre;

/**
 * Forge for Postgre
 */
class Forge extends \CodeIgniter\Database\Forge
{

	/**
	 * DROP CONSTRAINT statement
	 *
	 * @var string
	 */
	protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s';

	/**
	 * UNSIGNED support
	 *
	 * @var array
	 */
	protected $_unsigned = [
		'INT2'     => 'INTEGER',
		'SMALLINT' => 'INTEGER',
		'INT'      => 'BIGINT',
		'INT4'     => 'BIGINT',
		'INTEGER'  => 'BIGINT',
		'INT8'     => 'NUMERIC',
		'BIGINT'   => 'NUMERIC',
		'REAL'     => 'DOUBLE PRECISION',
		'FLOAT'    => 'DOUBLE PRECISION',
	];

	/**
	 * NULL value representation in CREATE/ALTER TABLE statements
	 *
	 * @var string
	 */
	protected $_null = 'NULL';

	//--------------------------------------------------------------------

	/**
	 * CREATE TABLE attributes
	 *
	 * @param  array $attributes Associative array of table attributes
	 * @return string
	 */
	protected function _createTableAttributes(array $attributes): string
	{
		return '';
	}

	//--------------------------------------------------------------------

	/**
	 * ALTER TABLE
	 *
	 * @param string $alter_type ALTER type
	 * @param string $table      Table name
	 * @param mixed  $field      Column definition
	 *
	 * @return string|array
	 */
	protected function _alterTable(string $alter_type, string $table, $field)
	{
		if (in_array($alter_type, ['DROP', 'ADD'], true))
		{
			return parent::_alterTable($alter_type, $table, $field);
		}

		$sql  = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
		$sqls = [];
		foreach ($field as $data)
		{
			if ($data['_literal'] !== false)
			{
				return false;
			}

			if (version_compare($this->db->getVersion(), '8', '>=') && isset($data['type']))
			{
				$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
						. " TYPE {$data['type']}{$data['length']}";
			}

			if (! empty($data['default']))
			{
				$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
						. " SET DEFAULT {$data['default']}";
			}

			if (isset($data['null']))
			{
				$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
						. ($data['null'] === true ? ' DROP' : ' SET') . ' NOT NULL';
			}

			if (! empty($data['new_name']))
			{
				$sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($data['name'])
						. ' TO ' . $this->db->escapeIdentifiers($data['new_name']);
			}

			if (! empty($data['comment']))
			{
				$sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table)
						. '.' . $this->db->escapeIdentifiers($data['name'])
						. " IS {$data['comment']}";
			}
		}

		return $sqls;
	}

		//--------------------------------------------------------------------

	/**
	 * Process column
	 *
	 * @param  array $field
	 * @return string
	 */
	protected function _processColumn(array $field): string
	{
		return $this->db->escapeIdentifiers($field['name'])
				. ' ' . $field['type'] . $field['length']
				. $field['default']
				. $field['null']
				. $field['auto_increment']
				. $field['unique'];
	}

	//--------------------------------------------------------------------

	/**
	 * Field attribute TYPE
	 *
	 * Performs a data type mapping between different databases.
	 *
	 * @param array &$attributes
	 *
	 * @return void
	 */
	protected function _attributeType(array &$attributes)
	{
		// Reset field lengths for data types that don't support it
		if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== false)
		{
			$attributes['CONSTRAINT'] = null;
		}

		switch (strtoupper($attributes['TYPE']))
		{
			case 'TINYINT':
				$attributes['TYPE']     = 'SMALLINT';
				$attributes['UNSIGNED'] = false;
				break;
			case 'MEDIUMINT':
				$attributes['TYPE']     = 'INTEGER';
				$attributes['UNSIGNED'] = false;
				break;
			case 'DATETIME':
				$attributes['TYPE'] = 'TIMESTAMP';
				break;
			default:
				break;
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Field attribute AUTO_INCREMENT
	 *
	 * @param array &$attributes
	 * @param array &$field
	 *
	 * @return void
	 */
	protected function _attributeAutoIncrement(array &$attributes, array &$field)
	{
		if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true)
		{
			$field['type'] = $field['type'] === 'NUMERIC' ? 'BIGSERIAL' : 'SERIAL';
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Drop Table
	 *
	 * Generates a platform-specific DROP TABLE string
	 *
	 * @param string  $table     Table name
	 * @param boolean $if_exists Whether to add an IF EXISTS condition
	 * @param boolean $cascade
	 *
	 * @return string
	 */
	protected function _dropTable(string $table, bool $if_exists, bool $cascade): string
	{
		$sql = parent::_dropTable($table, $if_exists, $cascade);

		if ($cascade === true)
		{
			$sql .= ' CASCADE';
		}

		return $sql;
	}

	//--------------------------------------------------------------------

}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Format;

use CodeIgniter\Format\Exceptions\FormatException;

/**
 * JSON data formatter
 */
class JSONFormatter implements FormatterInterface
{

	/**
	 * Takes the given data and formats it.
	 *
	 * @param $data
	 *
	 * @return string|bool (JSON string | false)
	 */
	public function format($data)
	{
		$options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;

		$options = ENVIRONMENT === 'production' ? $options : $options | JSON_PRETTY_PRINT;

		$result = json_encode($data, $options, 512);

		if (json_last_error() !== JSON_ERROR_NONE)
		{
			throw FormatException::forInvalidJSON(json_last_error_msg());
		}

		return $result;
	}

	//--------------------------------------------------------------------
}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Typography;

/**
 * Typography Class
 */
class Typography
{

	/**
	 * Block level elements that should not be wrapped inside
 tags
	 *
	 * @var string
	 */
	public $blockElements = 'address|blockquote|div|dl|fieldset|form|h\d|hr|noscript|object|ol|p|pre|script|table|ul';

	/**
	 * Elements that should not have
 and
 tags within them.
	 *
	 * @var string
	 */
	public $skipElements = 'p|pre|ol|ul|dl|object|table|h\d';

	/**
	 * Tags we want the parser to completely ignore when splitting the string.
	 *
	 * @var string
	 */
	public $inlineElements = 'a|abbr|acronym|b|bdo|big|br|button|cite|code|del|dfn|em|i|img|ins|input|label|map|kbd|q|samp|select|small|span|strong|sub|sup|textarea|tt|var';

	/**
	 * array of block level elements that require inner content to be within another block level element
	 *
	 * @var array
	 */
	public $innerBlockRequired = ['blockquote'];

	/**
	 * the last block element parsed
	 *
	 * @var string
	 */
	public $lastBlockElement = '';

	/**
	 * whether or not to protect quotes within { curly braces }
	 *
	 * @var boolean
	 */
	public $protectBracedQuotes = false;

	/**
	 * Auto Typography
	 *
	 * This function converts text, making it typographically correct:
	 *     - Converts double spaces into paragraphs.
	 *     - Converts single line breaks into
 tags
	 *     - Converts single and double quotes into correctly facing curly quote entities.
	 *     - Converts three dots into ellipsis.
	 *     - Converts double dashes into em-dashes.
	 *  - Converts two spaces into entities
	 *
	 * @param string  $str
	 * @param boolean $reduce_linebreaks whether to reduce more then two consecutive newlines to two
	 *
	 * @return string
	 */
	public function autoTypography(string $str, bool $reduce_linebreaks = false): string
	{
		if ($str === '')
		{
			return '';
		}

		// Standardize Newlines to make matching easier
		if (strpos($str, "\r") !== false)
		{
			$str = str_replace(["\r\n", "\r"], "\n", $str);
		}

		// Reduce line breaks.  If there are more than two consecutive linebreaks
		// we'll compress them down to a maximum of two since there's no benefit to more.
		if ($reduce_linebreaks === false)
		{
			$str = preg_replace("/\n\n+/", "\n\n", $str);
		}

		// HTML comment tags don't conform to patterns of normal tags, so pull them out separately, only if needed
		$html_comments = [];
		if (strpos($str, '


<?php

namespace Hanson\Vbot\Core;

use Hanson\Vbot\Foundation\Vbot;
use Hanson\Vbot\Message\Card;
use Hanson\Vbot\Message\Emoticon;
use Hanson\Vbot\Message\GroupChange;
use Hanson\Vbot\Message\Image;
use Hanson\Vbot\Message\Location;
use Hanson\Vbot\Message\NewFriend;
use Hanson\Vbot\Message\Recall;
use Hanson\Vbot\Message\RedPacket;
use Hanson\Vbot\Message\RequestFriend;
use Hanson\Vbot\Message\Text;
use Hanson\Vbot\Message\Touch;
use Hanson\Vbot\Message\Transfer;
use Hanson\Vbot\Message\Video;
use Hanson\Vbot\Message\Voice;
use Illuminate\Support\Collection;

class MessageFactory
{
    /**
     * @var Vbot
     */
    private $vbot;

    /**
     * MessageFactory constructor.
     *
     * @param Vbot $vbot
     */
    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
    }

    /**
     * @param $msg
     *
     * @return Collection
     */
    public function make($msg)
    {
        switch ($msg['MsgType']) {
            case 1: //文本消息
                if (Location::isLocation($msg)) {
                    return (new Location())->make($msg);
                } elseif ($this->vbot->friends->get($msg['FromUserName']) && str_contains($msg['Content'], '过了你的朋友验证请求')) {
                    return (new NewFriend())->make($msg);
                } else {
                    return (new Text())->make($msg);
                }
            case 3: // 图片消息
                return (new Image())->make($msg);
            case 34: // 语音消息
                return (new Voice())->make($msg);
            case 43: // 视频
                return (new Video())->make($msg);
            case 47: // 动画表情
                return (new Emoticon())->make($msg);
            case 10002:
                return (new Recall())->make($msg);
            case 10000:
                if (str_contains($msg['Content'], '利是') || str_contains($msg['Content'], '红包')) {
                    return (new RedPacket())->make($msg);
                } elseif (str_contains($msg['Content'], '添加') || str_contains($msg['Content'], '打招呼')) {
                    // 添加好友
                    return (new NewFriend())->make($msg);
                } elseif (str_contains($msg['Content'], '加入了群聊') || str_contains($msg['Content'], '移出了群聊') || str_contains($msg['Content'], '改群名为') || str_contains($msg['Content'], '移出群聊') || str_contains($msg['Content'], '邀请你') || str_contains($msg['Content'], '分享的二维码加入群聊')) {
                    return (new GroupChange())->make($msg);
                }
                break;
            case 49:
                if ($msg['FileName'] === '微信转账') {
                    return (new Transfer())->make($msg);
                } elseif ($msg['FileName'] === '我发起了位置共享') {
                    return (new Location())->make($msg);
                } elseif (str_contains($msg['Content'], '该类型暂不支持，请在手机上查看')) {
                    return;
                } else {
                    return $this->vbot->shareFactory->make($msg);
                }
            case 37: // 好友验证
                return (new RequestFriend())->make($msg);
            case 42: //共享名片
                return (new Card())->make($msg);
//            case 62:
//                //Video
//                break;
            case 51:
                if ($msg['ToUserName'] === $msg['StatusNotifyUserName']) {
                    return (new Touch())->make($msg);
                }
                break;
//            case 53:
//                //VideoCall
//                break;
            default:
                //Unknown
                break;
        }
    }
}


<?php

namespace Hanson\Vbot\Extension;

use Hanson\Vbot\Exceptions\ExtensionException;
use Hanson\Vbot\Message\Text;
use Illuminate\Support\Collection;

abstract class AbstractMessageHandler
{
    public $version = '1.0';

    public $author = 'HanSon';

    public $name;

    public $zhName;

    public $status = true;

    public static $admin;

    public $baseExtensions = [];

    /**
     * 拓展配置.
     *
     * @var
     */
    public $config;

    /**
     * 初始化拓展.
     */
    public function init()
    {
        $this->config = vbot('config')->get('extension.'.$this->name);

        $this->admin();

        $this->register();

        return $this;
    }

    /**
     * 注册拓展时的操作.
     */
    abstract public function register();

    /**
     * 开发者需要实现的方法.
     *
     * @param Collection $collection
     *
     * @return mixed
     */
    abstract public function handler(Collection $collection);

    /**
     * 消息处理器.
     *
     * @param Collection $collection
     *
     * @return mixed
     */
    final public function messageHandler(Collection $collection)
    {
        if ($collection['type'] === 'text' && $this->isAdmin($collection['username'])) {
            if (starts_with($collection['content'], $this->name.' ')) {
                $content = str_replace($this->name.' ', '', $collection['content']);

                switch ($content) {
                    case  'info':
                        $this->applicationInfo($collection);
                        break;
                    case 'on':
                        $this->setStatus(true, $collection);
                        break;
                    case 'off':
                        $this->setStatus(false, $collection);
                        break;
                    default:
                        break;
                }
            }
        }

        if (!$this->status) {
            return false;
        }

        return $this->handler($collection);
    }

    final public function applicationInfo($collection)
    {
        $status = $this->status ? '开' : '关';

        $admin = static::$admin;

        Text::send($collection['from']['UserName'], "当前应用名称：{$this->zhName}\n名称：{$this->name}\n状态：{$status}\n版本：{$this->version}\n作者：{$this->author}\n管理员 Username：{$admin}");
    }

    /**
     * 设置拓展开关.
     *
     * @param bool $boolean
     * @param $collection
     */
    final public function setStatus(bool $boolean, $collection)
    {
        $this->status = $boolean;

        $status = $this->status ? '开' : '关';

        Text::send($collection['from']['UserName'], "应用：{$this->zhName} 状态已更改为：{$status}");
    }

    /**
     * 设置管理员.
     *
     * @throws ExtensionException
     */
    final public function admin()
    {
        $remark = vbot('config')->get('extension.admin.remark');

        if ($remark) {
            static::$admin = vbot('friends')->getUsernameByRemarkName($remark);
        }

        if (!$remark && ($nickname = vbot('config')->get('extension.admin.nickname'))) {
            static::$admin = vbot('friends')->getUsernameByNickname($nickname);
        }
    }

    /**
     * 判断是否管理员.
     *
     * @param $username
     *
     * @return bool
     */
    final public function isAdmin($username) : bool
    {
        return $username === static::$admin || $username === vbot('myself')->username;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Hanson
 * Date: 2016/12/9
 * Time: 21:13.
 */

namespace Hanson\Vbot\Support;

use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Cookie\FileCookieJar;
use Hanson\Vbot\Console\Console;
use Hanson\Vbot\Foundation\Vbot;

class Http
{
    public static $instance;

    protected $client;

    /**
     * @var FileCookieJar;
     */
    protected $cookieJar;

    /**
     * @var Vbot
     */
    protected $vbot;

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
        $this->cookieJar = new FileCookieJar($vbot->config['cookie_file'], true);
        $this->client = new HttpClient(['cookies' => $this->cookieJar]);
    }

    public function get($url, array $options = [])
    {
        return $this->request($url, 'GET', $options);
    }

    public function post($url, $query = [], $array = false)
    {
        $key = is_array($query) ? 'form_params' : 'body';

        $content = $this->request($url, 'POST', [$key => $query]);

        return $array ? json_decode($content, true) : $content;
    }

    public function json($url, $params = [], $array = false, $extra = [])
    {
        $params = array_merge(['json' => $params], $extra);

        $content = $this->request($url, 'POST', $params);

        return $array ? json_decode($content, true) : $content;
    }

    public function setClient(HttpClient $client)
    {
        $this->client = $client;

        return $this;
    }

    /**
     * Return GuzzleHttp\Client instance.
     *
     * @return \GuzzleHttp\Client
     */
    public function getClient()
    {
        return $this->client;
    }

    /**
     * @param $url
     * @param string $method
     * @param array  $options
     * @param bool   $retry
     *
     * @return string
     */
    public function request($url, $method = 'GET', $options = [], $retry = false)
    {
        try {
            $options = array_merge(['timeout' => 10, 'verify' => false], $options);

            $response = $this->getClient()->request($method, $url, $options);

            $this->cookieJar->save($this->vbot->config['cookie_file']);

            return $response->getBody()->getContents();
        } catch (\Exception $e) {
            $this->vbot->console->log($url.$e->getMessage(), Console::ERROR, true);

            if (!$retry) {
                return $this->request($url, $method, $options, true);
            }

            return false;
        }
    }
}


<?php

namespace Hanson\Vbot\Foundation;

use Hanson\Vbot\Session\Session;

class Kernel
{
    /**
     * @var Vbot
     */
    private $vbot;

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
    }

    public function bootstrap()
    {
        $this->checkEnvironment();
        $this->registerProviders();
        $this->bootstrapException();
        $this->initializeConfig();
        $this->prepareSession();
        $this->initializePath();
    }

    private function checkEnvironment()
    {
        if (PHP_SAPI !== 'cli') {
            die('Please execute script in terminal!');
        }

        if (version_compare(PHP_VERSION, '7.0.0', '<')) {
            die('Vbot have to run under php 7! Current version is :'.PHP_VERSION);
        }

        $mustExtensions = ['gd', 'fileinfo', 'SimpleXML'];

        $diff = array_diff($mustExtensions, get_loaded_extensions());

        if ($diff) {
            die('Running script failed! please install extensions: '.PHP_EOL.implode("\n", $diff).PHP_EOL);
        }

        if ($this->vbot->config->get('swoole.status') && !in_array('swoole', get_loaded_extensions())) {
            die('Please install extension: swoole. Or you can turn it off in config.'.PHP_EOL);
        }
    }

    private function registerProviders()
    {
        $this->vbot->registerProviders();
    }

    private function prepareSession()
    {
        $session = new Session($this->vbot);

        $sessionKey = $session->currentSession();

        $this->vbot->config['session'] = $sessionKey;
        $this->vbot->config['session_key'] = 'session.'.$sessionKey;
    }

    private function bootstrapException()
    {
        error_reporting(-1);
        set_error_handler([$this->vbot->exception, 'handleError']);
        set_exception_handler([$this->vbot->exception, 'handleException']);
        register_shutdown_function([$this->vbot->exception, 'handleShutdown']);
    }

    /**
     * initialize config.
     */
    private function initializeConfig()
    {
        if (!is_dir($this->vbot->config['path'])) {
            mkdir($this->vbot->config['path'], 0755, true);
        }

        $this->vbot->config['storage'] = $this->vbot->config['storage'] ?: 'collection';

        $this->vbot->config['path'] = realpath($this->vbot->config['path']);
    }

    private function initializePath()
    {
        if (!is_dir($this->vbot->config['path'].'/cookies')) {
            mkdir($this->vbot->config['path'].'/cookies', 0755, true);
        }

        if (!is_dir($this->vbot->config['path'].'/users')) {
            mkdir($this->vbot->config['path'].'/users', 0755, true);
        }

        if (!is_dir($this->vbot->config['download.emoticon_path'])) {
            mkdir($this->vbot->config['download.emoticon_path'], 0755, true);
        }

        $this->vbot->config['cookie_file'] = $this->vbot->config['path'].'/cookies/'.$this->vbot->config['session'];
        $this->vbot->config['user_path'] = $this->vbot->config['path'].'/users/';
    }
}


<?php

namespace Hanson\Vbot\Console;

use PHPQRCode\QRcode as QrCodeConsole;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;

class QrCode extends Console
{
    /**
     * show qrCode on console.
     *
     * @param $text
     *
     * @return bool
     */
    public function show($text)
    {
        if (!array_get($this->config, 'qrcode', true)) {
            return false;
        }
        $output = new ConsoleOutput();
        static::initQrcodeStyle($output);

        $pxMap[0] = Console::isWin() ? '<whitec>mm</whitec>' : '<whitec>  </whitec>';
        $pxMap[1] = '<blackc>  </blackc>';

        $text = QrCodeConsole::text($text);

        $length = strlen($text[0]);

        $output->write("\n");
        foreach ($text as $line) {
            $output->write($pxMap[0]);
            for ($i = 0; $i < $length; $i++) {
                $type = substr($line, $i, 1);
                $output->write($pxMap[$type]);
            }
            $output->writeln($pxMap[0]);
        }
    }

    /**
     * init qrCode style.
     *
     * @param OutputInterface $output
     */
    private static function initQrcodeStyle(OutputInterface $output)
    {
        $style = new OutputFormatterStyle('black', 'black', ['bold']);
        $output->getFormatter()->setStyle('blackc', $style);
        $style = new OutputFormatterStyle('white', 'white', ['bold']);
        $output->getFormatter()->setStyle('whitec', $style);
    }
}


<?php

namespace Hanson\Vbot\Contact;

use Hanson\Vbot\Foundation\Vbot;
use Hanson\Vbot\Support\Content;
use Illuminate\Support\Collection;

class Contacts extends Collection
{
    /**
     * @var Vbot
     */
    protected $vbot;

    public function __construct()
    {
        parent::__construct();
    }

    public function setVbot(Vbot $vbot)
    {
        $this->vbot = $vbot;

        return $this;
    }

    /**
     * 根据昵称获取对象
     *
     * @param $nickname
     * @param bool $blur
     *
     * @return bool|string
     */
    public function getUsernameByNickname($nickname, $blur = false)
    {
        return $this->getUsername($nickname, 'NickName', $blur);
    }

    /**
     * 根据备注获取对象
     *
     * @param $remark
     * @param $blur
     *
     * @return mixed
     */
    public function getUsernameByRemarkName($remark, $blur = false)
    {
        return $this->getUsername($remark, 'RemarkName', $blur);
    }

    /**
     * 获取Username.
     *
     * @param $search
     * @param $key
     * @param bool $blur
     *
     * @return string
     */
    public function getUsername($search, $key, $blur = false)
    {
        return $this->search(function ($item) use ($search, $key, $blur) {
            if (!isset($item[$key])) {
                return false;
            }

            if ($blur && str_contains($item[$key], $search)) {
                return true;
            } elseif (!$blur && $item[$key] === $search) {
                return true;
            }

            return false;
        });
    }

    /**
     * 获取整个数组.
     *
     * @param $search
     * @param $key
     * @param bool $blur
     *
     * @return mixed|static
     */
    public function getObject($search, $key, $blur = false)
    {
        $username = $this->getUsername($search, $key, $blur);

        return $username ? $this->get($username) : null;
    }

    /**
     * 根据username获取账号.
     *
     * @param $username
     *
     * @return mixed
     */
    public function getAccount($username)
    {
        if (starts_with($username, '@@')) {
            return $this->vbot->groups->get($username);
        } else {
            $account = $this->vbot->friends->get($username, null);

            $account = $account ?: $this->vbot->members->get($username, null);

            $account = $account ?: $this->vbot->officials->get($username, null);

            return $account ?: $this->vbot->specials->get($username, null);
        }
    }

    public function getAvatar($username)
    {
        $params = [
            'userName' => $username,
            'type'     => 'big',
        ];

        $api = $this->vbot->groups->isGroup($username) ? '/webwxgetheadimg' : '/webwxgeticon';

        return $this->vbot->http->get($this->vbot->config['server.uri.base'].$api, ['query' => $params]);
    }

    /**
     * 存储时处理emoji.
     *
     * @param mixed $key
     * @param mixed $value
     *
     * @return Collection
     */
    public function put($key, $value)
    {
        $value = $this->format($value);

        return parent::put($key, $value);
    }

    /**
     * 处理联系人.
     *
     * @param $contact
     *
     * @return mixed
     */
    public function format($contact)
    {
        if (isset($contact['DisplayName'])) {
            $contact['DisplayName'] = Content::emojiHandle($contact['DisplayName']);
        }

        if (isset($contact['RemarkName'])) {
            $contact['RemarkName'] = Content::emojiHandle($contact['RemarkName']);
        }

        if (isset($contact['Signature'])) {
            $contact['Signature'] = Content::emojiHandle($contact['Signature']);
        }

        $contact['NickName'] = Content::emojiHandle($contact['NickName']);

        return $contact;
    }

    /**
     * 通过接口更新群组信息.
     *
     * @param $username
     * @param $list
     *
     * @return array
     */
    public function update($username, $list) :array
    {
        $usernames = is_string($username) ? [$username] : $username;

        $url = $this->vbot->config['server.uri.base'].'/webwxbatchgetcontact?type=ex&r='.time();

        $data = [
            'BaseRequest' => $this->vbot->config['server.baseRequest'],
            'Count'       => count($usernames),
            'List'        => $list,
        ];

        $response = $this->vbot->http->json($url, $data, true);

        if (!$response) {
            return [];
        }

        foreach ($response['ContactList'] as $item) {
            $this->put($item['UserName'], $item);
        }

        return is_string($username) ? head($response['ContactList']) : $response['ContactList'];
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2017/1/24
 * Time: 17:44.
 */

namespace Hanson\Vbot\Support;

/**
 * Content 处理类.
 *
 * Class Content
 */
class Content
{
    const EMOJI_MAP = [
        '1f63c' => '1f601',
        '1f639' => '1f602',
        '1f63a' => '1f603',
        '1f4ab' => '1f616',
        '1f64d' => '1f614',
        '1f63b' => '1f60d',
        '1f63d' => '1f618',
        '1f64e' => '1f621',
        '1f63f' => '1f622',
    ];

    /**
     * format XML for Content.
     *
     * @param $content
     *
     * @return string
     */
    public static function formatContent($content)
    {
        $content = self::emojiHandle($content);
        $content = self::replaceBr($content);

        return self::htmlDecode($content);
    }

    public static function htmlDecode($content)
    {
        return html_entity_decode($content);
    }

    public static function replaceBr($content)
    {
        return str_replace('
', "\n", $content);
    }

    /**
     * 处理微信EMOJI.
     *
     * @param string $content
     *
     * @return mixed
     */
    public static function emojiHandle(string $content)
    {
        // 微信的坑
        $content = str_replace('</span', '', $content);
        preg_match_all('/<\/span>/', $content, $match);

        foreach ($match[1] as &$unicode) {
            $unicode = array_get(self::EMOJI_MAP, $unicode, $unicode);
            $unicode = html_entity_decode("&#x{$unicode};");
        }

        return str_replace($match[0], $match[1], $content);
    }
}


<?php

namespace Hanson\Vbot\Core;

use Carbon\Carbon;
use Hanson\Vbot\Exceptions\ArgumentException;
use Hanson\Vbot\Foundation\Vbot;
use Hanson\Vbot\Message\Text;
use Illuminate\Support\Collection;

class MessageHandler
{
    /**
     * @var Vbot
     */
    protected $vbot;

    protected $handler;

    protected $customHandler;

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
    }

    public function listen($server = null)
    {
        $this->vbot->beforeMessageObserver->trigger();

        $this->vbot->messageExtension->initServiceExtensions();

        $time = 0;

        while (true) {
            if ($this->customHandler) {
                call_user_func($this->customHandler);
            }

            $time = $this->heartbeat($time);

            if (!($checkSync = $this->checkSync())) {
                continue;
            }

            if (!$this->handleCheckSync($checkSync[0], $checkSync[1])) {
                if ($server) {
                    $server->shutdown();
                } else {
                    break;
                }
            }
        }
    }

    /**
     * make a heartbeat every 30 minutes.
     *
     * @param $time
     *
     * @return int
     */
    private function heartbeat($time)
    {
        if (time() - $time > 1800) {
            Text::send('filehelper', 'heart beat '.Carbon::now()->toDateTimeString());

            return time();
        }

        return $time;
    }

    private function checkSync()
    {
        return $this->vbot->sync->checkSync();
    }

    /**
     * handle a sync from wechat.
     *
     * @param $retCode
     * @param $selector
     * @param bool $test
     *
     * @return bool
     */
    public function handleCheckSync($retCode, $selector, $test = false)
    {
        if (in_array($retCode, [1100, 1101, 1102, 1205])) { // 微信客户端上登出或者其他设备登录

            $this->vbot->console->log('vbot exit normally.');
            $this->vbot->cache->forget('session.'.$this->vbot->config['session']);

            return false;
        } elseif ($retCode != 0) {
            $this->vbot->needActivateObserver->trigger();
        } else {
            if (!$test) {
                $this->handleMessage($selector);
            }

            return true;
        }
    }

    /**
     * 处理消息.
     *
     * @param $selector
     */
    private function handleMessage($selector)
    {
        if ($selector == 0) {
            return;
        }

        $message = $this->vbot->sync->sync();

        $this->log($message);

        $this->storeContactsFromMessage($message);

        if ($message['AddMsgList']) {
            foreach ($message['AddMsgList'] as $msg) {
                $collection = $this->vbot->messageFactory->make($msg);
                if ($collection) {
                    $this->cache($msg, $collection);
                    $this->console($collection);
                    if (!$this->vbot->messageExtension->exec($collection) && $this->handler) {
                        call_user_func_array($this->handler, [$collection]);
                    }
                }
            }
        }
    }

    /**
     * log the message.
     *
     * @param $message
     */
    private function log($message)
    {
        if ($this->vbot->messageLog && ($message['ModContactList'] || $message['AddMsgList'])) {
            $this->vbot->messageLog->info(json_encode($message));
        }
    }

    private function console(Collection $collection)
    {
        $this->vbot->console->message($collection['content']);
    }

    private function storeContactsFromMessage($message)
    {
        if (count($message['ModContactList']) > 0) {
            $this->vbot->contactFactory->store($message['ModContactList']);
        }
    }

    private function cache($msg, Collection $collection)
    {
        $this->vbot->cache->put('msg-'.$msg['MsgId'], $collection->toArray(), 2);
    }

    /**
     * set a message handler.
     *
     * @param $callback
     *
     * @throws ArgumentException
     */
    public function setHandler($callback)
    {
        if (!is_callable($callback)) {
            throw new ArgumentException('Argument must be callable in '.get_class());
        }

        $this->handler = $callback;
    }

    /**
     * set a custom handler.
     *
     * @param $callback
     *
     * @throws ArgumentException
     */
    public function setCustomHandler($callback)
    {
        if (!is_callable($callback)) {
            throw new ArgumentException('Argument must be callable in '.get_class());
        }

        $this->customHandler = $callback;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Hanson
 * Date: 2016/12/9
 * Time: 21:10.
 */

namespace Hanson\Vbot\Core;

use Carbon\Carbon;
use Hanson\Vbot\Console\Console;
use Hanson\Vbot\Exceptions\FetchUuidException;
use Hanson\Vbot\Exceptions\InitFailException;
use Hanson\Vbot\Exceptions\LoginFailedException;
use Hanson\Vbot\Exceptions\LoginTimeoutException;
use Hanson\Vbot\Foundation\Vbot;

class Server
{
    /**
     * @var Vbot
     */
    protected $vbot;

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
    }

    public function serve()
    {
        if (!$this->tryLogin()) {
            $this->cleanCookies();
            $this->login();
        }

        $this->init();

        if ($this->vbot->config['swoole.status']) {
            $this->vbot->swoole->run();
        } else {
            $this->vbot->messageHandler->listen();
        }
    }

    /**
     * 尝试登录.
     *
     * @return bool
     */
    private function tryLogin(): bool
    {
        if (is_file($this->vbot->config['cookie_file']) && $this->vbot->cache->has($this->vbot->config['session_key'])) {
            $configs = json_decode($this->vbot->cache->get($this->vbot->config['session_key']), true);

            $this->vbot->config['server'] = $configs;
            $this->vbot->config['server.time'] = $this->vbot->config['server.time'] ?: Carbon::now()->toDateTimeString();

            if (!($checkSync = $this->vbot->sync->checkSync())) {
                return false;
            }

            $result = $this->vbot->messageHandler->handleCheckSync($checkSync[0], $checkSync[1], true);

            if ($result) {
                $this->vbot->reLoginSuccessObserver->trigger();

                return true;
            }
        }
        $this->vbot->config['server.time'] = Carbon::now()->toDateTimeString();

        return false;
    }

    private function cleanCookies()
    {
        $this->vbot->console->log('cleaning useless cookies.');
        if (is_file($this->vbot->config['cookie_file'])) {
            unlink($this->vbot->config['cookie_file']);
        }
    }

    /**
     * login.
     */
    public function login()
    {
        $this->getUuid();
        $this->showQrCode();
        $this->waitForLogin();
        $this->getLogin();
    }

    /**
     * get uuid.
     *
     * @throws \Exception
     */
    protected function getUuid()
    {
        $content = $this->vbot->http->get('https://login.weixin.qq.com/jslogin', ['query' => [
            'appid' => 'wx782c26e4c19acffb',
            'fun'   => 'new',
            'lang'  => 'zh_CN',
            '_'     => time(),
        ]]);

        preg_match('/window.QRLogin.code = (\d+); window.QRLogin.uuid = \"(\S+?)\"/', $content, $matches);

        if (!$matches) {
            throw new FetchUuidException('fetch uuid failed.');
        }

        $this->vbot->config['server.uuid'] = $matches[2];
    }

    /**
     * show a login qrCode.
     */
    public function showQrCode()
    {
        $url = 'https://login.weixin.qq.com/l/'.$this->vbot->config['server.uuid'];

        $this->vbot->qrCodeObserver->trigger($url);

        $this->vbot->qrCode->show($url);
    }

    /**
     * waiting user to login.
     *
     * @throws \Exception
     */
    protected function waitForLogin()
    {
        $retryTime = 10;
        $tip = 1;

        $this->vbot->console->log('please scan the qrCode with wechat.');
        while ($retryTime > 0) {
            $url = sprintf('https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s', $tip, $this->vbot->config['server.uuid'], time());

            $content = $this->vbot->http->get($url, ['timeout' => 35]);

            preg_match('/window.code=(\d+);/', $content, $matches);

            $code = $matches[1];
            switch ($code) {
                case '201':
                    $this->vbot->console->log('please confirm login in wechat.');
                    $tip = 0;
                    break;
                case '200':
                    preg_match('/window.redirect_uri="(https:\/\/(\S+?)\/\S+?)";/', $content, $matches);

                    $this->vbot->config['server.uri.redirect'] = $matches[1].'&fun=new';
                    $url = 'https://%s/cgi-bin/mmwebwx-bin';
                    $this->vbot->config['server.uri.file'] = sprintf($url, 'file.'.$matches[2]);
                    $this->vbot->config['server.uri.push'] = sprintf($url, 'webpush.'.$matches[2]);
                    $this->vbot->config['server.uri.base'] = sprintf($url, $matches[2]);

                    return;
                case '408':
                    $tip = 1;
                    $retryTime -= 1;
                    sleep(1);
                    break;
                default:
                    $tip = 1;
                    $retryTime -= 1;
                    sleep(1);
                    break;
            }
        }

        $this->vbot->console->log('login time out!', Console::ERROR);

        throw new LoginTimeoutException('Login time out.');
    }

    /**
     * login wechat.
     *
     * @throws \Exception
     */
    private function getLogin()
    {
        $content = $this->vbot->http->get($this->vbot->config['server.uri.redirect']);

        $data = (array) simplexml_load_string($content, 'SimpleXMLElement', LIBXML_NOCDATA);

        $this->vbot->config['server.skey'] = $data['skey'];
        $this->vbot->config['server.sid'] = $data['wxsid'];
        $this->vbot->config['server.uin'] = $data['wxuin'];
        $this->vbot->config['server.passTicket'] = $data['pass_ticket'];

        if (in_array('', [$data['wxsid'], $data['wxuin'], $data['pass_ticket']])) {
            throw new LoginFailedException('Login failed.');
        }

        $this->vbot->config['server.deviceId'] = 'e'.substr(mt_rand().mt_rand(), 1, 15);

        $this->vbot->config['server.baseRequest'] = [
            'Uin'      => $data['wxuin'],
            'Sid'      => $data['wxsid'],
            'Skey'     => $data['skey'],
            'DeviceID' => $this->vbot->config['server.deviceId'],
        ];

        $this->saveServer();
    }

    /**
     * store config to cache.
     */
    private function saveServer()
    {
        $this->vbot->cache->forever('session.'.$this->vbot->config['session'], json_encode($this->vbot->config['server']));
    }

    /**
     * init.
     *
     * @param bool $first
     *
     * @throws InitFailException
     */
    protected function init($first = true)
    {
        $this->beforeInitSuccess();
        $url = $this->vbot->config['server.uri.base'].'/webwxinit?r='.time();

        $result = $this->vbot->http->json($url, [
            'BaseRequest' => $this->vbot->config['server.baseRequest'],
        ], true);

        $this->generateSyncKey($result, $first);

        $this->vbot->myself->init($result['User']);

        ApiExceptionHandler::handle($result, function ($result) {
            $this->vbot->cache->forget('session.'.$this->vbot->config['session']);
            $this->vbot->log->error('Init failed.'.json_encode($result));

            throw new InitFailException('Init failed.');
        });

        $this->afterInitSuccess($result);

        $this->initContactList($result['ContactList']);
        $this->initContact();
    }

    /**
     * before init success.
     */
    private function beforeInitSuccess()
    {
        $this->vbot->console->log('current session: '.$this->vbot->config['session']);
        $this->vbot->console->log('init begin.');
    }

    /**
     * after init success.
     *
     * @param $content
     */
    private function afterInitSuccess($content)
    {
        $this->vbot->log->info('response:'.json_encode($content));
        $this->vbot->console->log('init success.');
        $this->vbot->loginSuccessObserver->trigger();
        $this->vbot->console->log('init contacts begin.');
    }

    protected function initContactList($contactList)
    {
        if ($contactList) {
            $this->vbot->contactFactory->store($contactList);
        }
    }

    protected function initContact()
    {
        $this->vbot->contactFactory->fetchAll();
    }

    /**
     * open wechat status notify.
     */
    protected function statusNotify()
    {
        $url = sprintf($this->vbot->config['server.uri.base'].'/webwxstatusnotify?lang=zh_CN&pass_ticket=%s', $this->vbot->config['server.passTicket']);

        $this->vbot->http->json($url, [
            'BaseRequest'  => $this->vbot->config['server.baseRequest'],
            'Code'         => 3,
            'FromUserName' => $this->vbot->myself->username,
            'ToUserName'   => $this->vbot->myself->username,
            'ClientMsgId'  => time(),
        ]);
    }

    protected function generateSyncKey($result, $first)
    {
        $this->vbot->config['server.syncKey'] = $result['SyncKey'];

        $syncKey = [];

        if (is_array($this->vbot->config['server.syncKey.List'])) {
            foreach ($this->vbot->config['server.syncKey.List'] as $item) {
                $syncKey[] = $item['Key'].'_'.$item['Val'];
            }
        } elseif ($first) {
            $this->init(false);
        }

        $this->vbot->config['server.syncKeyStr'] = implode('|', $syncKey);
    }
}


<?php

namespace Hanson\Vbot\Message\Traits;

use Hanson\Vbot\Console\Console;
use Hanson\Vbot\Core\ApiExceptionHandler;
use Hanson\Vbot\Exceptions\ArgumentException;
use Hanson\Vbot\Support\File;

trait Multimedia
{
    private static $file;

    /**
     * download multimedia.
     *
     * @param $message
     * @param null $callback
     *
     * @throws ArgumentException
     *
     * @return bool
     */
    public static function download($message, $callback = null)
    {
        if (!$callback) {
            static::autoDownload($message['raw'], true);

            return true;
        }

        if ($callback && !is_callable($callback)) {
            throw new ArgumentException();
        }

        call_user_func_array($callback, [static::getResource($message['raw'])]);

        return true;
    }

    /**
     * get a resource through api.
     *
     * @param $message
     *
     * @return mixed
     */
    private static function getResource($message)
    {
        $url = static::getDownloadUrl($message);

        $content = vbot('http')->get($url, static::getDownloadOption($message));

        if (!$content) {
            vbot('console')->log('download file failed.', Console::WARNING);
        } else {
            return $content;
        }
    }

    protected static function getDownloadUrl($message)
    {
        $serverConfig = vbot('config')['server'];

        return $serverConfig['uri']['base'].DIRECTORY_SEPARATOR.static::DOWNLOAD_API."{$message['MsgId']}&skey={$serverConfig['skey']}";
    }

    protected static function getDownloadOption($message)
    {
        return [];
    }

    /**
     * download resource to a default path.
     *
     * @param $message
     * @param bool $force
     */
    protected static function autoDownload($message, $force = false)
    {
        $isDownload = vbot('config')['download.'.static::TYPE];

        if ($isDownload || $force) {
            $resource = static::getResource($message);

            if ($resource) {
                File::saveTo(vbot('config')['user_path'].static::TYPE.DIRECTORY_SEPARATOR.
                    static::fileName($message), $resource);
            }
        }
    }

    protected static function fileName($message)
    {
        return $message['MsgId'].static::EXT;
    }

    protected static function getDefaultFile($message)
    {
        return vbot('config')['user_path'].static::TYPE.DIRECTORY_SEPARATOR.static::fileName($message);
    }

    /**
     * @param $username
     * @param $file
     *
     * @return bool|mixed|string
     */
    public static function uploadMedia($username, $file)
    {
        if (!is_file($file)) {
            return false;
        }

        $url = vbot('config')['server.uri.file'].'/webwxuploadmedia?f=json';

        static::$file = $file;
        list($mime, $mediaType) = static::getMediaType($file);

        $data = [
            'id'                 => 'WU_FILE_0',
            'name'               => basename($file),
            'type'               => $mime,
            'lastModifieDate'    => gmdate('D M d Y H:i:s TO', filemtime($file)).' (CST)',
            'size'               => filesize($file),
            'mediatype'          => $mediaType,
            'uploadmediarequest' => json_encode([
                'BaseRequest'   => vbot('config')['server.baseRequest'],
                'ClientMediaId' => time(),
                'TotalLen'      => filesize($file),
                'StartPos'      => 0,
                'DataLen'       => filesize($file),
                'MediaType'     => 4,
                'UploadType'    => 2,
                'FromUserName'  => vbot('myself')->username,
                'ToUserName'    => $username,
                'FileMd5'       => md5_file($file),
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'webwx_data_ticket' => static::getTicket(),
            'pass_ticket'       => vbot('config')['server.passTicket'],
            'filename'          => fopen($file, 'r'),
        ];

        $data = static::dataToMultipart($data);

        $result = vbot('http')->request($url, 'post', [
            'multipart' => $data,
        ]);
        $result = json_decode($result, true);

        return ApiExceptionHandler::handle($result);
    }

    /**
     * 获取媒体类型.
     *
     * @param $file
     *
     * @return array
     */
    private static function getMediaType($file)
    {
        $info = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($info, $file);
        finfo_close($info);

        $fileExplode = explode('.', $file);
        $fileExtension = end($fileExplode);

        return [$mime, $fileExtension === 'jpg' ? 'pic' : ($fileExtension === 'mp4' ? 'video' : 'doc')];
    }

    /**
     * 获取cookie的ticket.
     *
     * @return mixed
     */
    private static function getTicket()
    {
        $cookies = vbot('http')->getClient()->getConfig('cookies')->toArray();

        $key = array_search('webwx_data_ticket', array_column($cookies, 'Name'));

        return $cookies[$key]['Value'];
    }

    /**
     * 把请求数组转为multipart模式.
     *
     * @param $data
     *
     * @return array
     */
    private static function dataToMultipart($data)
    {
        $result = [];

        foreach ($data as $key => $item) {
            $field = [
                'name'     => $key,
                'contents' => $item,
            ];
            if ($key === 'filename') {
                $field['filename'] = basename(static::$file);
            }
            $result[] = $field;
        }

        return $result;
    }
}


<?php

namespace Hanson\Vbot\Foundation\Bootstrap;

use ErrorException;
use Exception;
use Hanson\Vbot\Foundation\Vbot;
use Symfony\Component\Debug\Exception\FatalThrowableError;

class HandleExceptions
{
    protected $app;

    /**
     * Bootstrap the given application.
     *
     * @param Vbot $app
     *
     * @return void
     */
    public function bootstrap(Vbot $app)
    {
        $this->app = $app;
        error_reporting(-1);
        set_error_handler([$this, 'handleError']);
        set_exception_handler([$this, 'handleException']);
        register_shutdown_function([$this, 'handleShutdown']);
    }

    /**
     * Convert PHP errors to ErrorException instances.
     *
     * @param int    $level
     * @param string $message
     * @param string $file
     * @param int    $line
     * @param array  $context
     *
     * @throws \ErrorException
     *
     * @return void
     */
    public function handleError($level, $message, $file = '', $line = 0, $context = [])
    {
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }

    /**
     * Handle an uncaught exception from the application.
     *
     * Note: Most exceptions can be handled via the try / catch block in
     * the HTTP and Console kernels. But, fatal error exceptions must
     * be handled differently since they are not normal exceptions.
     *
     * @param \Throwable $e
     *
     * @return void
     */
    public function handleException($e)
    {
        if (!$e instanceof Exception) {
            $e = new FatalThrowableError($e);
        }

        $this->getExceptionHandler()->report($e);
        if ($this->app->runningInConsole()) {
            $this->renderForConsole($e);
        } else {
            $this->renderHttpResponse($e);
        }
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: HanSon
 * Date: 2017/1/14
 * Time: 11:21.
 */

namespace Hanson\Vbot\Core;

use Hanson\Vbot\Exceptions\WebSyncException;
use Hanson\Vbot\Foundation\Vbot;

class Sync
{
    /**
     * @var Vbot
     */
    private $vbot;

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
    }

    /**
     * check if got a new message.
     *
     * @return array|bool
     */
    public function checkSync()
    {
        $content = $this->vbot->http->get($this->vbot->config['server.uri.push'].'/synccheck', ['timeout' => 35, 'query' => [
            'r'        => time(),
            'sid'      => $this->vbot->config['server.sid'],
            'uin'      => $this->vbot->config['server.uin'],
            'skey'     => $this->vbot->config['server.skey'],
            'deviceid' => $this->vbot->config['server.deviceId'],
            'synckey'  => $this->vbot->config['server.syncKeyStr'],
            '_'        => time(),
        ]]);

        if (!$content) {
            $this->vbot->console->log('checkSync no response');

            return false;
        }

        return preg_match('/window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}/', $content, $matches) ?
            [$matches[1], $matches[2]] : false;
    }

    /**
     * get a message.
     *
     * @throws WebSyncException
     *
     * @return mixed|string
     */
    public function sync()
    {
        $url = sprintf($this->vbot->config['server.uri.base'].'/webwxsync?sid=%s&skey=%s&lang=zh_CN&pass_ticket=%s',
            $this->vbot->config['server.sid'],
            $this->vbot->config['server.skey'],
            $this->vbot->config['server.passTicket']
        );

        $result = $this->vbot->http->json($url, [
            'BaseRequest' => $this->vbot->config['server.baseRequest'],
            'SyncKey'     => $this->vbot->config['server.syncKey'],
            'rr'          => ~time(),
        ], true);

        if ($result && $result['BaseResponse']['Ret'] == 0) {
            $this->generateSyncKey($result);
        }

        return $result;
    }

    /**
     * generate a sync key.
     *
     * @param $result
     */
    public function generateSyncKey($result)
    {
        $this->vbot->config['server.syncKey'] = $result['SyncKey'];

        $syncKey = [];

        if (is_array($this->vbot->config['server.syncKey.List'])) {
            foreach ($this->vbot->config['server.syncKey.List'] as $item) {
                $syncKey[] = $item['Key'].'_'.$item['Val'];
            }
        }

        $this->vbot->config['server.syncKeyStr'] = implode('|', $syncKey);
    }
}


<?php

namespace Hanson\Vbot\Console;

use Carbon\Carbon;
use Hanson\Vbot\Foundation\Vbot;
use Monolog\Logger;

class Console
{
    const INFO = 'INFO';
    const WARNING = 'WARNING';
    const ERROR = 'ERROR';
    const MESSAGE = 'MESSAGE';

    /**
     * @var Vbot
     */
    protected $vbot;

    /**
     * console config.
     *
     * @var array
     */
    protected $config;

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;

        $this->config = $this->vbot->config['console'];
    }

    /**
     * determine the console is windows or linux.
     *
     * @return bool
     */
    public static function isWin()
    {
        return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    }

    /**
     * print in terminal.
     *
     * @param $str
     * @param string $level
     * @param bool   $log
     */
    public function log($str, $level = 'INFO', $log = false)
    {
        if ($this->isOutput()) {
            if ($log && in_array($level, array_keys(Logger::getLevels()))) {
                $this->vbot->log->log($level, $str);
            }
            echo '['.Carbon::now()->toDateTimeString().']'."[{$level}] ".$str.PHP_EOL;
        }
    }

    /**
     * print message.
     *
     * @param $str
     */
    public function message($str)
    {
        if (array_get($this->config, 'message', true)) {
            $this->log($str, self::MESSAGE);
        }
    }

    public function isOutput()
    {
        return array_get($this->config, 'output', true);
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Hanson
 * Date: 2016/12/13
 * Time: 20:56.
 */

namespace Hanson\Vbot\Contact;

use Hanson\Vbot\Core\ApiExceptionHandler;

class Friends extends Contacts
{
    /**
     * 根据微信号获取联系人.
     *
     * @param $alias
     *
     * @return mixed
     */
    public function getContactByAlias($alias)
    {
        return $this->getObject($alias, 'Alias', true);
    }

    /**
     * 根据微信号获取联系username.
     *
     * @param $alias
     *
     * @return mixed
     */
    public function getUsernameByAlias($alias)
    {
        return $this->getUsername($alias, 'Alias');
    }

    /**
     * 设置备注.
     *
     * @param $username
     * @param $remarkName
     *
     * @return bool
     */
    public function setRemarkName($username, $remarkName)
    {
        $url = sprintf('%s/webwxoplog?lang=zh_CN&pass_ticket=%s', $this->vbot->config['server.uri.base'], $this->vbot->config['server.passTicket']);

        $result = $this->vbot->http->post($url, json_encode([
            'UserName'    => $username,
            'CmdId'       => 2,
            'RemarkName'  => $remarkName,
            'BaseRequest' => $this->vbot->config['server.baseRequest'],
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);

        return $result['BaseResponse']['Ret'] == 0;
    }

    /**
     * 设置是否置顶.
     *
     * @param $username
     * @param bool $isStick
     *
     * @return bool
     */
    public function setStick($username, $isStick = true)
    {
        $url = sprintf('%s/webwxoplog?lang=zh_CN&pass_ticket=%s', $this->vbot->config['server.uri.base'], $this->vbot->config['server.passTicket']);

        $result = $this->vbot->http->json($url, [
            'UserName'    => $username,
            'CmdId'       => 3,
            'OP'          => (int) $isStick,
            'BaseRequest' => $this->vbot->config['server.baseRequest'],
        ], true);

        return $result['BaseResponse']['Ret'] == 0;
    }

    /**
     * 主动添加好友.
     *
     * @param $username
     * @param null $content
     */
    public function add($username, $content = null)
    {
        $this->verifyUser(2, [
            'Value'            => $username,
            'VerifyUserTicket' => '',
        ], $content);
    }

    /**
     * 通过好友申请.
     *
     * @param $message
     */
    public function approve($message)
    {
        $this->verifyUser(3, [
            'Value'            => $message['info']['UserName'],
            'VerifyUserTicket' => $message['info']['Ticket'],
        ]);
    }

    /**
     * 验证通过好友.
     *
     * @param $code
     * @param $userList
     * @param null $content
     *
     * @return bool
     */
    public function verifyUser($code, $userList, $content = null)
    {
        $url = sprintf($this->vbot->config['server.uri.base'].'/webwxverifyuser?lang=zh_CN&r=%s&pass_ticket=%s', time() * 1000, $this->vbot->config['server.passTicket']);
        $data = [
            'BaseRequest'        => $this->vbot->config['server.baseRequest'],
            'Opcode'             => $code,
            'VerifyUserListSize' => 1,
            'VerifyUserList'     => [$userList],
            'VerifyContent'      => $content,
            'SceneListCount'     => 1,
            'SceneList'          => [33],
            'skey'               => $this->vbot->config['server.skey'],
        ];

        $result = $this->vbot->http->post($url, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);

        return ApiExceptionHandler::handle($result);
    }

    /**
     * 更新群组.
     *
     * @param $username
     * @param null $list
     *
     * @return array
     */
    public function update($username, $list = null) :array
    {
        $username = is_array($username) ?: [$username];

        return parent::update($username, $this->makeUsernameList($username));
    }

    /**
     * 生成username list 格式.
     *
     * @param $username
     *
     * @return array
     */
    public function makeUsernameList($username)
    {
        $usernameList = [];

        foreach ($username as $item) {
            $usernameList[] = ['UserName' => $item, 'EncryChatRoomId' => ''];
        }

        return $usernameList;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Hanson
 * Date: 2016/12/16
 * Time: 18:33.
 */

namespace Hanson\Vbot\Message;

use Hanson\Vbot\Message\Traits\SendAble;

class Text extends Message implements MessageInterface
{
    use SendAble;

    const TYPE = 'text';
    const API = 'webwxsendmsg?';

    private $isAt = false;

    private $pure;

    public function make($msg)
    {
        return $this->getCollection($msg, static::TYPE);
    }

    protected function afterCreate()
    {
        $this->isAt = str_contains($this->message, '@'.vbot('myself')->nickname);
        $this->pure = $this->pureText();
    }

    private function pureText()
    {
        $content = str_replace(' ', ' ', $this->message);
        $isMatch = preg_match('/^@(.+?)\s([\s\S]*)/', $content, $match);

        return $isMatch ? $match[2] : $this->message;
    }

    protected function getExpand(): array
    {
        return ['isAt' => $this->isAt, 'pure' => $this->pure];
    }

    protected function parseToContent():string
    {
        return $this->message;
    }

    /**
     * send a text message.
     *
     * @param $word string
     * @param $username string
     *
     * @return bool|mixed
     */
    public static function send($username, $word)
    {
        if (!$word || !$username) {
            return false;
        }

        return static::sendMsg([
            'Type'         => 1,
            'Content'      => $word,
            'FromUserName' => vbot('myself')->username,
            'ToUserName'   => $username,
            'LocalID'      => time() * 1e4,
            'ClientMsgId'  => time() * 1e4,
        ]);
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Hanson
 * Date: 2016/12/13
 * Time: 20:56.
 */

namespace Hanson\Vbot\Contact;

use Hanson\Vbot\Exceptions\CreateGroupException;

class Groups extends Contacts
{
    /**
     * 判断是否群组.
     *
     * @param $userName
     *
     * @return bool
     */
    public function isGroup($userName)
    {
        return strstr($userName, '@@') !== false;
    }

    /**
     * 根据群名筛选群组.
     *
     * @param $nickname
     * @param bool $blur
     *
     * @return static
     */
    public function getGroupsByNickname($nickname, $blur = false)
    {
        return $this->getObject($nickname, 'NickName', $blur);
    }

    /**
     * 根据username获取群成员.
     *
     * @param $username
     * @param $memberUsername
     *
     * @return mixed
     */
    public function getMemberByUsername($username, $memberUsername)
    {
        $members = $this->get($username)['MemberList'];

        if (count($members) === 0) {
            return;
        }

        foreach ($members as $member) {
            if ($memberUsername === $member['UserName']) {
                return $member;
            }
        }
    }

    /**
     * 根据昵称搜索群成员.
     *
     * @param $groupUsername
     * @param $memberNickname
     * @param bool $blur
     *
     * @return array|bool
     */
    public function getMembersByNickname($groupUsername, $memberNickname, $blur = false)
    {
        $group = $this->get($groupUsername);

        if (!$group) {
            return false;
        }

        $result = [];

        foreach ($group['MemberList'] as $member) {
            if ($blur && str_contains($member['NickName'], $memberNickname)) {
                $result[] = $member;
            } elseif (!$blur && $member['NickName'] === $memberNickname) {
                $result[] = $member;
            }
        }

        return $result;
    }

    /**
     * 创建群聊天.
     *
     * @param array $contacts
     *
     * @return bool
     */
    public function create(array $contacts)
    {
        $url = sprintf('%s/webwxcreatechatroom?lang=zh_CN&r=%s', $this->vbot->config['server.uri.base'], time());

        $result = $this->vbot->http->json($url, [
            'MemberCount' => count($contacts),
            'MemberList'  => $this->makeMemberList($contacts),
            'Topic'       => '',
            'BaseRequest' => $this->vbot->config['server.baseRequest'],
        ], true);

        if ($result['BaseResponse']['Ret'] != 0) {
            return false;
        }

        return $this->add($result['ChatRoomName']);
    }

    /**
     * 删除群成员.
     *
     * @param $group
     * @param $members
     *
     * @return bool
     */
    public function deleteMember($group, $members)
    {
        $members = is_string($members) ? [$members] : $members;
        $result = $this->vbot->http->json(sprintf('%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s', $this->vbot->config['server.uri.base'], $this->vbot->config['server.passTicket']), [
            'BaseRequest'   => $this->vbot->config['server.baseRequest'],
            'ChatRoomName'  => $group,
            'DelMemberList' => implode(',', $members),
        ], true);

        return $result['BaseResponse']['Ret'] == 0;
    }

    /**
     * 添加群成员.
     *
     * @param $groupUsername
     * @param $members
     *
     * @return bool
     */
    public function addMember($groupUsername, $members)
    {
        if (!$groupUsername) {
            return false;
        }
        $group = $this->get($groupUsername);

        if (!$group) {
            return false;
        }

        $groupCount = count($group['MemberList']);
        list($fun, $key) = $groupCount > 40 ? ['invitemember', 'InviteMemberList'] : ['addmember', 'AddMemberList'];
        $members = is_string($members) ? [$members] : $members;

        $result = $this->vbot->http->json(sprintf('%s/webwxupdatechatroom?fun=%s&pass_ticket=%s', $this->vbot->config['server.uri.base'], $fun, $this->vbot->config['server.passTicket']), [
            'BaseRequest'  => $this->vbot->config['server.baseRequest'],
            'ChatRoomName' => $groupUsername,
            $key           => implode(',', $members),
        ], true);

        return $result['BaseResponse']['Ret'] == 0;
    }

    /**
     * 设置群名称.
     *
     * @param $group
     * @param $name
     *
     * @return bool
     */
    public function setGroupName($group, $name)
    {
        $result = $this->vbot->http->post(sprintf('%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s', $this->vbot->config['server.uri.base'], $this->vbot->config['server.passTicket']),
            json_encode([
            'BaseRequest'  => $this->vbot->config['server.baseRequest'],
            'ChatRoomName' => $group,
            'NewTopic'     => $name,
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);

        return $result['BaseResponse']['Ret'] == 0;
    }

    /**
     * 增加群聊天到group.
     *
     * @param $username
     *
     * @throws CreateGroupException
     *
     * @return bool
     */
    public function add($username)
    {
        $result = $this->vbot->http->json(sprintf('%s/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s', $this->vbot->config['server.uri.base'], time(), $this->vbot->config['server.passTicket']), [
            'Count'       => 1,
            'BaseRequest' => $this->vbot->config['server.baseRequest'],
            'List'        => [
                [
                    'ChatRoomId' => '',
                    'UserName'   => $username,
                ],
            ],
        ], true);

        if ($result['BaseResponse']['Ret'] != 0) {
            throw new CreateGroupException('create group chat fail.');
        }

        $this->put($username, $result['ContactList'][0]);

        return $result['ContactList'][0];
    }

    /**
     * 更新群组.
     *
     * @param $username
     * @param null $list
     *
     * @return array
     */
    public function update($username, $list = null) :array
    {
        $username = is_array($username) ?: [$username];

        return parent::update($username, $this->makeUsernameList($username));
    }

    /**
     * 生成username list 格式.
     *
     * @param $username
     *
     * @return array
     */
    public function makeUsernameList($username)
    {
        $usernameList = [];

        foreach ($username as $item) {
            $usernameList[] = ['UserName' => $item, 'ChatRoomId' => ''];
        }

        return $usernameList;
    }

    /**
     * 生成member list 格式.
     *
     * @param $contacts
     *
     * @return array
     */
    private function makeMemberList($contacts)
    {
        $memberList = [];

        foreach ($contacts as $contact) {
            $memberList[] = ['UserName' => $contact];
        }

        return $memberList;
    }

    /**
     * 存储群组前批量修改群成员nickname.
     *
     * @param mixed $key
     * @param mixed $value
     *
     * @return \Illuminate\Support\Collection
     */
    public function put($key, $value)
    {
        foreach ($value['MemberList'] as &$member) {
            $member = $this->format($member);
        }

        return parent::put($key, $value);
    }

    /**
     * 修改群组获取，为空时更新群组.
     *
     * @param mixed $key
     * @param null  $default
     *
     * @return mixed
     */
    public function get($key, $default = null)
    {
        $group = parent::get($key);

        return $group ?: current($this->update($key));
    }
}


<?php

namespace Hanson\Vbot\Core;

use Hanson\Vbot\Foundation\Vbot;

class ContactFactory
{
    const SPECIAL_USERS = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail',
        'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle',
        'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp',
        'blogapp', 'facebookapp', 'masssendapp', 'meishiapp',
        'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder',
        'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c',
        'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11',
        'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages', ];

    /**
     * @var Vbot
     */
    private $vbot;

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
    }

    public function fetchAll()
    {
        $this->fetchAllContacts();

        $this->fetchGroupMembers();

        $this->vbot->fetchContactObserver->trigger([
            'friends'   => $this->vbot->friends,
            'groups'    => $this->vbot->groups,
            'officials' => $this->vbot->officials,
            'special'   => $this->vbot->specials,
            'members'   => $this->vbot->members,
        ]);
    }

    /**
     * fetch all contacts through api.
     *
     * @param $seq
     */
    public function fetchAllContacts($seq = 0)
    {
        $url = sprintf($this->vbot->config['server.uri.base'].'/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s&seq=%s',
            $this->vbot->config['server.passTicket'],
            $this->vbot->config['server.skey'],
            time(),
            $seq
        );

        $result = $this->vbot->http->json($url, [], true, ['timeout' => 60]);

        if (isset($result['MemberList']) && $result['MemberList']) {
            $this->store($result['MemberList']);
        }

        if (isset($result['Seq']) && $result['Seq'] != 0) {
            $this->fetchAllContacts($result['Seq']);
        }
    }

    /**
     * create and save contacts to collections.
     *
     * @param $memberList
     */
    public function store($memberList)
    {
        foreach ($memberList as $contact) {
            if (in_array($contact['UserName'], static::SPECIAL_USERS)) {
                $this->vbot->specials->put($contact['UserName'], $contact);
            } elseif ($this->vbot->officials->isOfficial($contact['VerifyFlag'])) {
                $this->vbot->officials->put($contact['UserName'], $contact);
            } elseif (strstr($contact['UserName'], '@@') !== false) {
                $this->vbot->groups->put($contact['UserName'], $contact);
            } else {
                $this->vbot->friends->put($contact['UserName'], $contact);
            }
        }
    }

    /**
     * fetch group members.
     */
    public function fetchGroupMembers()
    {
        $url = sprintf($this->vbot->config['server.uri.base'].'/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s',
            time(), $this->vbot->config['server.passTicket']
        );

        $list = [];
        $this->vbot->groups->each(function ($item, $key) use (&$list) {
            $list[] = ['UserName' => $key, 'EncryChatRoomId' => ''];
        });

        $content = $this->vbot->http->json($url, [
            'BaseRequest' => $this->vbot->config['server.baseRequest'],
            'Count'       => $this->vbot->groups->count(),
            'List'        => $list,
        ], true, ['timeout' => 60]);

        $this->storeMembers($content);
    }

    /**
     * store group members.
     *
     * @param $array
     */
    private function storeMembers($array)
    {
        if (isset($array['ContactList']) && $array['ContactList']) {
            foreach ($array['ContactList'] as $group) {
                $groupAccount = $this->vbot->groups->get($group['UserName']);
                $groupAccount['MemberList'] = $group['MemberList'];
                $groupAccount['ChatRoomId'] = $group['EncryChatRoomId'];
                $this->vbot->groups->put($group['UserName'], $groupAccount);
                foreach ($group['MemberList'] as $member) {
                    $this->vbot->members->put($member['UserName'], $member);
                }
            }
        }
    }
}


<?php

namespace Hanson\Vbot\Foundation;

use Closure;
use ErrorException;
use Exception;
use Hanson\Vbot\Exceptions\ArgumentException;
use Hanson\Vbot\Exceptions\ConfigErrorException;
use Hanson\Vbot\Exceptions\CreateGroupException;
use Hanson\Vbot\Exceptions\ExtensionException;
use Hanson\Vbot\Exceptions\FetchUuidException;
use Hanson\Vbot\Exceptions\LoginFailedException;
use Hanson\Vbot\Exceptions\LoginTimeoutException;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Throwable;

class ExceptionHandler
{
    protected $dontReport = [
        CreateGroupException::class,
    ];

    protected $fatalException = [
        FetchUuidException::class,
        LoginTimeoutException::class,
        ConfigErrorException::class,
        LoginFailedException::class,
        ExtensionException::class,
    ];

    /**
     * exception handler.
     *
     * @var Closure
     */
    protected $handler;

    /**
     * @var Vbot
     */
    protected $vbot;

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
    }

    /**
     * report while exception.
     *
     * @param Exception $e
     *
     * @throws Exception
     *
     * @return bool
     */
    public function report(Exception $e)
    {
        if ($this->shouldntReport($e)) {
            return true;
        }

        if ($this->handler) {
            call_user_func_array($this->handler, [$e]);
        }
    }

    /**
     * Determine if the exception is in the "do not report" list.
     *
     * @param \Exception $e
     *
     * @return bool
     */
    protected function shouldntReport(Exception $e)
    {
        foreach ($this->dontReport as $type) {
            return $e instanceof $type;
        }

        return false;
    }

    /**
     * set a exception handler.
     *
     * @param $closure
     *
     * @throws ArgumentException
     */
    public function setHandler($closure)
    {
        if (!is_callable($closure)) {
            throw new ArgumentException('Argument must be callable.');
        }

        $this->handler = $closure;
    }

    /**
     * Convert PHP errors to ErrorException instances.
     *
     * @param int    $level
     * @param string $message
     * @param string $file
     * @param int    $line
     *
     * @throws \ErrorException
     *
     * @return void
     */
    public function handleError($level, $message, $file = '', $line = 0)
    {
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }

    /**
     * Handle an uncaught exception from the application.
     *
     * @param \Throwable $e
     *
     * @throws Throwable
     *
     * @return bool
     */
    public function handleException(Throwable $e)
    {
        $this->vbot->log->error($e->getMessage());

        $this->report($e);

        $this->throwFatalException($e);

        throw $e;
    }

    /**
     * Exception that make vbot couldn 't work.
     *
     * @param Throwable $e
     *
     * @throws Throwable
     */
    private function throwFatalException(Throwable $e)
    {
        foreach ($this->fatalException as $exception) {
            if ($e instanceof $exception) {
                throw $e;
            }
        }
    }

    /**
     * Handle the PHP shutdown event.
     *
     * @return void
     */
    public function handleShutdown()
    {
        if (!is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
            $this->handleException($this->fatalExceptionFromError($error, 0));
        }

        $this->vbot->exitObserver->trigger();
    }

    /**
     * Create a new fatal exception instance from an error array.
     *
     * @param array    $error
     * @param int|null $traceOffset
     *
     * @return FatalErrorException
     */
    protected function fatalExceptionFromError(array $error, $traceOffset = null)
    {
        return new FatalErrorException(
            $error['message'], $error['type'], 0, $error['file'], $error['line'], $traceOffset
        );
    }

    /**
     * Determine if the error type is fatal.
     *
     * @param int $type
     *
     * @return bool
     */
    protected function isFatal($type)
    {
        return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
    }
}


<?php

namespace Hanson\Vbot\Extension;

use Hanson\Vbot\Exceptions\ExtensionException;
use Hanson\Vbot\Foundation\Vbot;

class MessageExtension
{
    /**
     * @var Vbot
     */
    protected $vbot;

    /**
     * 业务扩展.
     *
     * @var array
     */
    protected $serviceExtensions = [];

    /**
     * 基础扩展.
     *
     * @var array
     */
    public $baseExtensions = [];

    public function __construct(Vbot $vbot)
    {
        $this->vbot = $vbot;
    }

    /**
     * 读取业务消息拓展.
     *
     * @param $extensions
     *
     * @throws ExtensionException
     */
    public function load($extensions)
    {
        if (!is_array($extensions)) {
            throw new ExtensionException('extensions must pass an array.');
        }

        foreach ($extensions as $extension) {
            $this->addServiceExtension($extension);
        }

        $this->serviceExtensions = array_unique($this->serviceExtensions);
    }

    /**
     * 初始化业务拓展.
     */
    public function initServiceExtensions()
    {
        $tmpExtensions = [];

        foreach ($this->serviceExtensions as $serviceExtensions) {
            $extension = new $serviceExtensions();

            $tmpExtensions[] = $extension->init();

            $this->baseExtensions = array_merge($this->baseExtensions, $extension->baseExtensions);
        }

        $this->serviceExtensions = $tmpExtensions;
        $this->baseExtensions = array_unique($this->baseExtensions);

        $this->initBaseExtensions();
    }

    /**
     * 初始化基础扩展.
     */
    private function initBaseExtensions()
    {
        $tmpExtensions = [];

        foreach ($this->baseExtensions as $baseExtension) {
            $tmpExtensions[] = (new $baseExtension())->init();
        }

        $this->baseExtensions = $tmpExtensions;
    }

    /**
     * 执行拓展.
     *
     * @param $collection
     *
     * @return bool
     */
    public function exec($collection)
    {
        foreach ($this->serviceExtensions as $extension) {
            if ($extension->messageHandler($collection)) {
                return true;
            }
        }
    }

    /**
     * 添加业务消息拓展.
     *
     * @param $extension
     *
     * @throws ExtensionException
     */
    private function addServiceExtension($extension)
    {
        if ($extension instanceof AbstractMessageHandler) {
            throw new ExtensionException($extension.' is not extend AbstractMessageHandler');
        }

        $this->serviceExtensions[] = $extension;
    }
}