Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
executable file 3987 lines (3613 sloc) 141.466 kb
<?php
require_once 'protocol.class.php';
require_once 'BinTreeNodeReader.php';
require_once 'BinTreeNodeWriter.php';
require_once 'Constants.php';
require_once 'func.php';
require_once 'token.php';
require_once 'rc4.php';
require_once 'mediauploader.php';
require_once 'keystream.class.php';
require_once 'tokenmap.class.php';
require_once 'events/WhatsApiEventsManager.php';
require_once 'SqliteMessageStore.php';
class SyncResult
{
public $index;
public $syncId;
/** @var array $existing */
public $existing;
/** @var array $nonExisting */
public $nonExisting;
public function __construct($index, $syncId, $existing, $nonExisting)
{
$this->index = $index;
$this->syncId = $syncId;
$this->existing = $existing;
$this->nonExisting = $nonExisting;
}
}
class WhatsProt
{
/**
* Property declarations.
*/
protected $accountInfo; // The AccountInfo object.
protected $challengeFilename; // Path to nextChallenge.dat.
protected $challengeData; //
protected $debug; // Determines whether debug mode is on or off.
protected $event; // An instance of the WhatsApiEvent Manager.
protected $groupList = array(); // An array with all the groups a user belongs in.
protected $identity; // The Device Identity token. Obtained during registration with this API or using Missvenom to sniff from your phone.
protected $inputKey; // Instances of the KeyStream class.
protected $outputKey; // Instances of the KeyStream class.
protected $groupId = false; // Id of the group created.
protected $lastId = false; // Id to the last message sent.
protected $loginStatus; // Holds the login status.
protected $mediaFileInfo = array(); // Media File Information
protected $mediaQueue = array(); // Queue for media message nodes
protected $messageCounter = 1; // Message counter for auto-id.
protected $messageQueue = array(); // Queue for received messages.
protected $name; // The user name.
protected $newMsgBind = false; //
protected $outQueue = array(); // Queue for outgoing messages.
protected $password; // The user password.
protected $phoneNumber; // The user phone number including the country code without '+' or '00'.
protected $serverReceivedId; // Confirm that the *server* has received your command.
protected $socket; // A socket to connect to the WhatsApp network.
protected $writer; // An instance of the BinaryTreeNodeWriter class.
protected $messageStore;
protected $nodeId = array();
protected $loginTime;
public $reader; // An instance of the BinaryTreeNodeReader class.
/**
* Default class constructor.
*
* @param string $number
* The user phone number including the country code without '+' or '00'.
* @param string $identity
* The Device Identity token. Obtained during registration with this API
* or using Missvenom to sniff from your phone.
* @param string $nickname
* The user name.
* @param $debug
* Debug on or off, false by default.
*/
public function __construct($number, $nickname, $debug = false)
{
$this->writer = new BinTreeNodeWriter();
$this->reader = new BinTreeNodeReader();
$this->debug = $debug;
$this->phoneNumber = $number;
//e.g. ./cache/nextChallenge.12125557788.dat
$this->challengeFilename = sprintf('%s%s%snextChallenge.%s.dat',
__DIR__,
DIRECTORY_SEPARATOR,
Constants::DATA_FOLDER . DIRECTORY_SEPARATOR,
$number);
$this->identity = $this->buildIdentity();
$this->name = $nickname;
$this->loginStatus = Constants::DISCONNECTED_STATUS;
$this->eventManager = new WhatsApiEventsManager();
}
/**
* If you need use different challenge fileName you can use this
*
* @param string $filename
*/
public function setChallengeName($filename)
{
$this->challengeFilename = $filename;
}
/**
* Add message to the outgoing queue.
*
* @param $node
*/
public function addMsgOutQueue($node)
{
$this->outQueue[] = $node;
}
/**
* Check if account credentials are valid.
*
* WARNING: WhatsApp now changes your password everytime you use this.
* Make sure you update your config file if the output informs about
* a password change.
*
* @return object
* An object with server response.
* - status: Account status.
* - login: Phone number with country code.
* - pw: Account password.
* - type: Type of account.
* - expiration: Expiration date in UNIX TimeStamp.
* - kind: Kind of account.
* - price: Formatted price of account.
* - cost: Decimal amount of account.
* - currency: Currency price of account.
* - price_expiration: Price expiration in UNIX TimeStamp.
*
* @throws Exception
*/
public function checkCredentials()
{
if (!$phone = $this->dissectPhone()) {
throw new Exception('The provided phone number is not valid.');
}
$countryCode = ($phone['ISO3166'] != '') ? $phone['ISO3166'] : 'US';
$langCode = ($phone['ISO639'] != '') ? $phone['ISO639'] : 'en';
if ($phone['cc'] == '77' || $phone['cc'] == '79') {
$phone['cc'] = '7';
}
// Build the url.
$host = 'https://' . Constants::WHATSAPP_CHECK_HOST;
$query = array(
'cc' => $phone['cc'],
'in' => $phone['phone'],
'id' => $this->identity,
'lg' => $langCode,
'lc' => $countryCode,
// 'network_radio_type' => "1"
);
$response = $this->getResponse($host, $query);
if ($response->status != 'ok') {
$this->eventManager()->fire("onCredentialsBad",
array(
$this->phoneNumber,
$response->status,
$response->reason
));
$this->debugPrint($query);
$this->debugPrint($response);
throw new Exception('There was a problem trying to request the code.');
} else {
$this->eventManager()->fire("onCredentialsGood",
array(
$this->phoneNumber,
$response->login,
$response->pw,
$response->type,
$response->expiration,
$response->kind,
$response->price,
$response->cost,
$response->currency,
$response->price_expiration
));
}
return $response;
}
/**
* Register account on WhatsApp using the provided code.
*
* @param integer $code
* Numeric code value provided on requestCode().
*
* @return object
* An object with server response.
* - status: Account status.
* - login: Phone number with country code.
* - pw: Account password.
* - type: Type of account.
* - expiration: Expiration date in UNIX TimeStamp.
* - kind: Kind of account.
* - price: Formatted price of account.
* - cost: Decimal amount of account.
* - currency: Currency price of account.
* - price_expiration: Price expiration in UNIX TimeStamp.
*
* @throws Exception
*/
public function codeRegister($code)
{
if (!$phone = $this->dissectPhone()) {
throw new Exception('The provided phone number is not valid.');
}
//$countryCode = ($phone['ISO3166'] != '') ? $phone['ISO3166'] : 'US';
//$langCode = ($phone['ISO639'] != '') ? $phone['ISO639'] : 'en';
// Build the url.
$host = 'https://' . Constants::WHATSAPP_REGISTER_HOST;
$query = array(
'cc' => $phone['cc'],
'in' => $phone['phone'],
'id' => $this->identity,
'code' => $code,
//'lg' => $langCode,
//'lc' => $countryCode,
//'network_radio_type' => "1"
);
$response = $this->getResponse($host, $query);
if ($response->status != 'ok') {
$this->eventManager()->fire("onCodeRegisterFailed",
array(
$this->phoneNumber,
$response->status,
$response->reason,
$response->retry_after
));
$this->debugPrint($query);
$this->debugPrint($response);
throw new Exception('An error occurred registering the registration code from WhatsApp.');
} else {
$this->eventManager()->fire("onCodeRegister",
array(
$this->phoneNumber,
$response->login,
$response->pw,
$response->type,
$response->expiration,
$response->kind,
$response->price,
$response->cost,
$response->currency,
$response->price_expiration
));
}
return $response;
}
/**
* Request a registration code from WhatsApp.
*
* @param string $method Accepts only 'sms' or 'voice' as a value.
* @param string $carrier
*
* @return object
* An object with server response.
* - status: Status of the request (sent/fail).
* - length: Registration code lenght.
* - method: Used method.
* - reason: Reason of the status (e.g. too_recent/missing_param/bad_param).
* - param: The missing_param/bad_param.
* - retry_after: Waiting time before requesting a new code.
*
* @throws Exception
*/
public function codeRequest($method = 'sms', $carrier = "T-Mobile5")
{
if (!$phone = $this->dissectPhone()) {
throw new Exception('The provided phone number is not valid.');
}
$countryCode = ($phone['ISO3166'] != '') ? $phone['ISO3166'] : 'US';
$langCode = ($phone['ISO639'] != '') ? $phone['ISO639'] : 'en';
if ($carrier != null) {
$mnc = $this->detectMnc(strtolower($countryCode), $carrier);
} else {
$mnc = $phone['mnc'];
}
// Build the token.
$token = generateRequestToken($phone['country'], $phone['phone']);
// Build the url.
$host = 'https://' . Constants::WHATSAPP_REQUEST_HOST;
$query = array(
'in' => $phone['phone'],
'cc' => $phone['cc'],
'id' => $this->identity,
'lg' => $langCode,
'lc' => $countryCode,
//'mcc' => '000',
//'mnc' => '000',
'sim_mcc' => $phone['mcc'],
'sim_mnc' => $mnc,
'method' => $method,
//'reason' => "self-send-jailbroken",
'token' => $token,
//'network_radio_type' => "1"
);
$this->debugPrint($query);
$response = $this->getResponse($host, $query);
$this->debugPrint($response);
if ($response->status == 'ok') {
$this->eventManager()->fire("onCodeRegister",
array(
$this->phoneNumber,
$response->login,
$response->pw,
$response->type,
$response->expiration,
$response->kind,
$response->price,
$response->cost,
$response->currency,
$response->price_expiration
));
} else if ($response->status != 'sent') {
if (isset($response->reason) && $response->reason == "too_recent") {
$this->eventManager()->fire("onCodeRequestFailedTooRecent",
array(
$this->phoneNumber,
$method,
$response->reason,
$response->retry_after
));
$minutes = round($response->retry_after / 60);
throw new Exception("Code already sent. Retry after $minutes minutes.");
} else if (isset($response->reason) && $response->reason == "too_many_guesses") {
$this->eventManager()->fire("onCodeRequestFailedTooManyGuesses",
array(
$this->phoneNumber,
$method,
$response->reason,
$response->retry_after
));
$minutes = round($response->retry_after / 60);
throw new Exception("Too many guesses. Retry after $minutes minutes.");
} else {
$this->eventManager()->fire("onCodeRequestFailed",
array(
$this->phoneNumber,
$method,
$response->reason,
isset($response->param) ? $response->param : NULL
));
throw new Exception('There was a problem trying to request the code.');
}
} else {
$this->eventManager()->fire("onCodeRequest",
array(
$this->phoneNumber,
$method,
$response->length
));
}
return $response;
}
/**
* Connect (create a socket) to the WhatsApp network.
*
* @return bool
*/
public function connect()
{
if ($this->isConnected()) {
return true;
}
//$WAData = json_decode(file_get_contents(Constants::WHATSAPP_VER_CHECKER), true);
// if(Constants::WHATSAPP_VER != $WAver)
// {
// updateData('token.php', $WAData->e, $WAData->h);
// updateData('whatsprot.class.php', $WAData->e);
// }
/* Create a TCP/IP socket. */
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket !== false) {
$result = socket_connect($socket, "e" . rand(1, 16) . ".whatsapp.net", Constants::PORT);
if ($result === false) {
$socket = false;
}
}
if ($socket !== false) {
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => Constants::TIMEOUT_SEC, 'usec' => Constants::TIMEOUT_USEC));
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array('sec' => Constants::TIMEOUT_SEC, 'usec' => Constants::TIMEOUT_USEC));
$this->socket = $socket;
$this->eventManager()->fire("onConnect",
array(
$this->phoneNumber,
$this->socket
)
);
return true;
} else {
$this->eventManager()->fire("onConnectError",
array(
$this->phoneNumber,
$this->socket
)
);
return false;
}
}
/**
* Do we have an active socket connection to WhatsApp?
*
* @return bool
*/
public function isConnected()
{
return ($this->socket !== null);
}
/**
* Disconnect from the WhatsApp network.
*/
public function disconnect()
{
if (is_resource($this->socket)) {
@socket_shutdown($this->socket, 2);
@socket_close($this->socket);
$this->socket = null;
$this->loginStatus = Constants::DISCONNECTED_STATUS;
$this->eventManager()->fire("onDisconnect",
array(
$this->phoneNumber,
$this->socket
)
);
}
}
/**
* @return WhatsApiEventsManager
*/
public function eventManager()
{
return $this->eventManager;
}
/**
* Drain the message queue for application processing.
*
* @return ProtocolNode[]
* Return the message queue list.
*/
public function getMessages()
{
$ret = $this->messageQueue;
$this->messageQueue = array();
return $ret;
}
/**
* Log into the WhatsApp server.
*
* ###Warning### using this method will generate a new password
* from the WhatsApp servers each time.
*
* If you know your password and wish to use it without generating
* a new password - use the loginWithPassword() method instead.
*/
public function login()
{
$this->accountInfo = (array) $this->checkCredentials();
if ($this->accountInfo['status'] == 'ok') {
$this->debugPrint("New password received: " . $this->accountInfo['pw'] . "\n");
$this->password = $this->accountInfo['pw'];
}
$this->doLogin();
}
/**
* Login to the WhatsApp server with your password
*
* If you already know your password you can log into the Whatsapp server
* using this method.
*
* @param string $password Your whatsapp password. You must already know this!
*/
public function loginWithPassword($password)
{
$this->password = $password;
if (is_readable($this->challengeFilename)) {
$challengeData = file_get_contents($this->challengeFilename);
if ($challengeData) {
$this->challengeData = $challengeData;
}
}
$this->doLogin();
}
/**
* Fetch a single message node
* @param bool $autoReceipt
* @param string $type
* @return bool
*
* @throws Exception
*/
public function pollMessage($autoReceipt = true, $type = "read")
{
if (!$this->isConnected()) {
throw new ConnectionException('Connection Closed!');
}
$r = array($this->socket);
$w = array();
$e = array();
if (socket_select($r, $w, $e, Constants::TIMEOUT_SEC, Constants::TIMEOUT_USEC)) {
// Something to read
if ($stanza = $this->readStanza()) {
$this->processInboundData($stanza, $autoReceipt, $type);
return true;
}
}
return false;
}
/**
* Send the active status. User will show up as "Online" (as long as socket is connected).
*/
public function sendActiveStatus()
{
$messageNode = new ProtocolNode("presence", array("type" => "active"), null, "");
$this->sendNode($messageNode);
}
/**
* Send a request to get cipher keys from an user
*
* @param $number
* Phone number of the user you want to get the cipher keys.
*/
public function sendGetCipherKeysFromUser($number)
{
$msgId = $this->createMsgId();
$userNode = new ProtocolNode("user",
array(
"jid" => $this->getJID($number)
), null, null);
$keyNode = new ProtocolNode("key", null, array($userNode), null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "encrypt",
"type" => "get",
"to" => Constants::WHATSAPP_SERVER
), array($keyNode), null);
$this->sendNode($node);
}
/**
* Send a Broadcast Message with audio.
*
* The recipients MUST have your number (synced) and in their contact list
* otherwise the message will not deliver to that person.
*
* Approx 20 (unverified) is the maximum number of targets
*
* @param array $targets An array of numbers to send to.
* @param string $path URL or local path to the audio file to send
* @param bool $storeURLmedia Keep a copy of the audio file on your server
* @param int $fsize
* @param string $fhash
* @return string|null Message ID if successfully, null if not.
*/
public function sendBroadcastAudio($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = "")
{
if (!is_array($targets)) {
$targets = array($targets);
}
// Return message ID. Make pull request for this.
return $this->sendMessageAudio($targets, $path, $storeURLmedia, $fsize, $fhash);
}
/**
* Send a Broadcast Message with an image.
*
* The recipients MUST have your number (synced) and in their contact list
* otherwise the message will not deliver to that person.
*
* Approx 20 (unverified) is the maximum number of targets
*
* @param array $targets An array of numbers to send to.
* @param string $path URL or local path to the image file to send
* @param bool $storeURLmedia Keep a copy of the audio file on your server
* @param int $fsize
* @param string $fhash
* @param string $caption
* @return string|null Message ID if successfully, null if not.
*/
public function sendBroadcastImage($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = "", $caption = "")
{
if (!is_array($targets)) {
$targets = array($targets);
}
// Return message ID. Make pull request for this.
return $this->sendMessageImage($targets, $path, $storeURLmedia, $fsize, $fhash, $caption);
}
/**
* Send a Broadcast Message with location data.
*
* The recipients MUST have your number (synced) and in their contact list
* otherwise the message will not deliver to that person.
*
* If no name is supplied , receiver will see large sized google map
* thumbnail of entered Lat/Long but NO name/url for location.
*
* With name supplied, a combined map thumbnail/name box is displayed
* Approx 20 (unverified) is the maximum number of targets
*
* @param array $targets An array of numbers to send to.
* @param float $long The longitude of the location eg 54.31652
* @param float $lat The latitude if the location eg -6.833496
* @param string $name (Optional) A name to describe the location
* @param string $url (Optional) A URL to link location to web resource
* @return string Message ID
*/
public function sendBroadcastLocation($targets, $long, $lat, $name = null, $url = null)
{
if (!is_array($targets)) {
$targets = array($targets);
}
// Return message ID. Make pull request for this.
return $this->sendMessageLocation($targets, $long, $lat, $name, $url);
}
/**
* Send a Broadcast Message
*
* The recipients MUST have your number (synced) and in their contact list
* otherwise the message will not deliver to that person.
*
* Approx 20 (unverified) is the maximum number of targets
*
* @param array $targets An array of numbers to send to.
* @param string $message Your message
* @return string Message ID
*/
public function sendBroadcastMessage($targets, $message)
{
$message = $this->parseMessageForEmojis($message);
$bodyNode = new ProtocolNode("body", null, null, $message);
// Return message ID. Make pull request for this.
return $this->sendBroadcast($targets, $bodyNode, "text");
}
/**
* Send a Broadcast Message with a video.
*
* The recipients MUST have your number (synced) and in their contact list
* otherwise the message will not deliver to that person.
*
* Approx 20 (unverified) is the maximum number of targets
*
* @param array $targets An array of numbers to send to.
* @param string $path URL or local path to the video file to send
* @param bool $storeURLmedia Keep a copy of the audio file on your server
* @param int $fsize
* @param string $fhash
* @param string $caption
* @return string|null Message ID if successfully, null if not.
*/
public function sendBroadcastVideo($targets, $path, $storeURLmedia = false, $fsize = 0, $fhash = "", $caption = "")
{
if (!is_array($targets)) {
$targets = array($targets);
}
// Return message ID. Make pull request for this.
return $this->sendMessageVideo($targets, $path, $storeURLmedia, $fsize, $fhash, $caption);
}
/**
* Delete Broadcast lists
*
* @param string array $lists
* Contains the broadcast-id list
*/
public function sendDeleteBroadcastLists($lists)
{
$msgId = $this->createMsgId();
$listNode = array();
if ($lists != null && count($lists) > 0) {
for ($i = 0; $i < count($lists); $i++) {
$listNode[$i] = new ProtocolNode("list", array("id" => $lists[$i]), null, null);
}
} else {
$listNode = null;
}
$deleteNode = new ProtocolNode("delete", null, $listNode, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "w:b",
"type" => "set",
"to" => Constants::WHATSAPP_SERVER
), array($deleteNode), null);
$this->sendNode($node);
}
/**
* Clears the "dirty" status on your account
*
* @param array $categories
*/
protected function sendClearDirty($categories)
{
$msgId = $this->createMsgId();
$catnodes = array();
foreach ($categories as $category) {
$catnode = new ProtocolNode("clean", array("type" => $category), null, null);
$catnodes[] = $catnode;
}
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"type" => "set",
"to" => Constants::WHATSAPP_SERVER,
"xmlns" => "urn:xmpp:whatsapp:dirty"
), $catnodes, null);
$this->sendNode($node);
}
public function sendClientConfig()
{
$attr = array();
$attr["platform"] = Constants::WHATSAPP_DEVICE;
$attr["version"] = Constants::WHATSAPP_VER;
$child = new ProtocolNode("config", $attr, null, "");
$node = new ProtocolNode("iq",
array(
"id" => $this->createMsgId(),
"type" => "set",
"xmlns" => "urn:xmpp:whatsapp:push",
"to" => Constants::WHATSAPP_SERVER
), array($child), null);
$this->sendNode($node);
}
public function sendGetClientConfig()
{
$msgId = $this->createMsgId();
$child = new ProtocolNode("config", null, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "urn:xmpp:whatsapp:push",
"type" => "get",
"to" => Constants::WHATSAPP_SERVER
), array($child), null);
$this->sendNode($node);
$this->waitForServer($msgId);
}
/**
* Transfer your number to new one
*
* @param string $number
* @param string $identity
*/
public function sendChangeNumber($number, $identity)
{
$msgId = $this->createMsgId();
$usernameNode = new ProtocolNode("username", null, null, $number);
$passwordNode = new ProtocolNode("password", null, null, urldecode($identity));
$modifyNode = new ProtocolNode("modify", null, array($usernameNode, $passwordNode), null);
$iqNode = new ProtocolNode("iq",
array(
"xmlns" => "urn:xmpp:whatsapp:account",
"id" => $msgId,
"type" => "get",
"to" => "c.us"
), array($modifyNode), null);
$this->sendNode($iqNode);
}
/**
* Send a request to return a list of groups user is currently participating in.
*
* To capture this list you will need to bind the "onGetGroups" event.
*/
public function sendGetGroups()
{
$this->sendGetGroupsFiltered("participating");
}
/**
* Send a request to get new Groups V2 info.
*
* @param $groupID
* The group JID
*/
public function sendGetGroupV2Info($groupID)
{
$msgId = $this->nodeId['get_groupv2_info'] = $this->createMsgId();
$queryNode = new ProtocolNode("query",
array(
"request" => "interactive"
), null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "w:g2",
"type" => "get",
"to" => $this->getJID($groupID)
), array($queryNode), null);
$this->sendNode($node);
}
/**
* Send a request to get a list of people you have currently blocked.
*/
public function sendGetPrivacyBlockedList()
{
$msgId = $this->nodeId['privacy'] = $this->createMsgId();
$child = new ProtocolNode("list",
array(
"name" => "default"
), null, null);
$child2 = new ProtocolNode("query", array(), array($child), null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "jabber:iq:privacy",
"type" => "get"
), array($child2), null);
$this->sendNode($node);
$this->waitForServer($msgId);
}
/**
* Send a request to get privacy settings.
*/
public function sendGetPrivacySettings()
{
$msgId = $this->createMsgId();
$privacyNode = new ProtocolNode("privacy", null, null, null);
$node = new ProtocolNode("iq",
array(
"to" => Constants::WHATSAPP_SERVER,
"id" => $msgId,
"xmlns" => "privacy",
"type" => "get"
), array($privacyNode), null);
$this->sendNode($node);
$this->waitForServer($msgId);
}
/**
* Set privacy of 'last seen', status or profile picture to all, contacts or none.
*
* @param string $category
* Options: 'last', 'status' or 'profile'
* @param string $value
* Options: 'all', 'contacts' or 'none'
*/
public function sendSetPrivacySettings($category, $value)
{
$msgId = $this->createMsgId();
$categoryNode = new ProtocolNode("category",
array(
"name" => $category,
"value" => $value
), null, null);
$privacyNode = new ProtocolNode("privacy", null, array($categoryNode), null);
$node = new ProtocolNode("iq",
array(
"to" => Constants::WHATSAPP_SERVER,
"type" => "set",
"id" => $msgId,
"xmlns" => "privacy"
), array($privacyNode), null);
$this->sendNode($node);
$this->waitForServer($msgId);
}
/**
* Get profile picture of specified user.
*
* @param string $number
* Number or JID of user
* @param bool $large
* Request large picture
*/
public function sendGetProfilePicture($number, $large = false)
{
$msgId = $this->createMsgId();
$hash = array();
$hash["type"] = "image";
if (!$large) {
$hash["type"] = "preview";
}
$picture = new ProtocolNode("picture", $hash, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"type" => "get",
"xmlns" => "w:profile:picture",
"to" => $this->getJID($number)
), array($picture), null);
$this->sendNode($node);
$this->waitForServer($msgId);
}
/**
* @param mixed $numbers Numbers to get profile profile photos of.
* @return bool
*/
public function sendGetProfilePhotoIds($numbers)
{
if (!is_array($numbers)) {
$numbers = array($numbers);
}
$msgId = $this->createMsgId();
$userNode = array();
for ($i=0; $i < count($numbers); $i++) {
$userNode[$i] = new ProtocolNode("user",
array(
"jid" => $this->getJID($numbers[$i])
), null, null);
}
if (!sizeof($userNode)) {
return false;
}
$listNode = new ProtocolNode("list", null, $userNode, null);
$iqNode = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "w:profile:picture",
"type" => "get"
), array($listNode), null);
$this->sendNode($iqNode);
return true;
}
/**
* Request to retrieve the last online time of specific user.
*
* @param string $to Number or JID of user
*/
public function sendGetRequestLastSeen($to)
{
$msgId = $this->createMsgId();
$queryNode = new ProtocolNode("query", null, null, null);
$messageNode = new ProtocolNode("iq",
array(
"to" => $this->getJID($to),
"type" => "get",
"id" => $msgId,
"xmlns" => "jabber:iq:last"
), array($queryNode), "");
$this->sendNode($messageNode);
$this->waitForServer($msgId);
}
/**
* Send a request to get the current server properties.
*/
public function sendGetServerProperties()
{
$id = $this->createMsgId();
$child = new ProtocolNode("props", null, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $id,
"type" => "get",
"xmlns" => "w",
"to" => Constants::WHATSAPP_SERVER
), array($child), null);
$this->sendNode($node);
}
/**
* Send a request to get the current service pricing.
*
* @param string $lg
* Language
* @param string $lc
* Country
*/
public function sendGetServicePricing($lg, $lc)
{
$msgId = $this->createMsgId();
$pricingNode = new ProtocolNode("pricing",
array(
"lg" => $lg,
"lc" => $lc
), null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "urn:xmpp:whatsapp:account",
"type" => "get",
"to" => Constants::WHATSAPP_SERVER
), array($pricingNode), null);
$this->sendNode($node);
}
/**
* Send a request to extend the account.
*/
public function sendExtendAccount()
{
$msgId = $this->createMsgId();
$extendingNode = new ProtocolNode("extend", null, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "urn:xmpp:whatsapp:account",
"type" => "set",
"to" => Constants::WHATSAPP_SERVER
), array($extendingNode), null);
$this->sendNode($node);
}
/**
* Gets all the broadcast lists for an account.
*/
public function sendGetBroadcastLists()
{
$msgId = $this->nodeId['get_lists'] = $this->createMsgId();
$listsNode = new ProtocolNode("lists", null, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "w:b",
"type" => "get",
"to" => Constants::WHATSAPP_SERVER
), array($listsNode), null);
$this->sendNode($node);
}
/**
* Send a request to get the normalized mobile number representing the JID.
*
* @param string $countryCode Country Code
* @param string $number Mobile Number
*/
public function sendGetNormalizedJid($countryCode, $number)
{
$msgId = $this->createMsgId();
$ccNode = new ProtocolNode("cc", null, null, $countryCode);
$inNode = new ProtocolNode("in", null, null, $number);
$normalizeNode = new ProtocolNode("normalize", null, array($ccNode, $inNode), null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "urn:xmpp:whatsapp:account",
"type" => "get",
"to" => Constants::WHATSAPP_SERVER
), array($normalizeNode), null);
$this->sendNode($node);
}
/**
* Removes an account from WhatsApp.
*
* @param string $lg Language
* @param string $lc Country
* @param string $feedback User Feedback
*/
public function sendRemoveAccount($lg = null, $lc = null, $feedback = null)
{
$msgId = $this->createMsgId();
if ($feedback != null && strlen($feedback) > 0)
{
if ($lg == null) {
$lg = "";
}
if ($lc == null) {
$lc = "";
}
$child = new ProtocolNode("body",
array(
"lg" => $lg,
"lc" => $lc
), null, $feedback);
$childNode = array($child);
} else {
$childNode = null;
}
$removeNode = new ProtocolNode("remove", null, $childNode, null);
$node = new ProtocolNode("iq",
array(
"to" => Constants::WHATSAPP_SERVER,
"xmlns" => "urn:xmpp:whatsapp:account",
"type" => "get",
"id" => $msgId
), array($removeNode), null);
$this->sendNode($node);
}
/**
* Send a ping to the server.
*/
public function sendPing()
{
$msgId = $this->createMsgId();
$pingNode = new ProtocolNode("ping", null, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "w:p",
"type" => "get",
"to" => Constants::WHATSAPP_SERVER
), array($pingNode), null);
$this->sendNode($node);
}
/**
* Get VOIP information of a number or numbers.
*
* @param mixed $jids
*/
public function sendGetHasVoipEnabled($jids)
{
$msgId = $this->createMsgId();
if (!is_array($jids))
{
$jids = array($jids);
}
$userNode = array();
foreach ($jids as $jid)
{
$userNode[] = new ProtocolNode("user", array('jid' => $this->getJID($jid)), null, null);
}
$eligibleNode = new ProtocolNode("eligible", null, $userNode, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "voip",
"type" => "get",
"to" => Constants::WHATSAPP_SERVER
), array($eligibleNode), null);
$this->sendNode($node);
}
/**
* Get the current status message of a specific user.
*
* @param mixed $jids The users' JIDs
*/
public function sendGetStatuses($jids)
{
if (!is_array($jids)) {
$jids = array($jids);
}
$children = array();
foreach ($jids as $jid) {
$children[] = new ProtocolNode("user", array("jid" => $this->getJID($jid)), null, null);
}
$node = new ProtocolNode("iq",
array(
"to" => Constants::WHATSAPP_SERVER,
"type" => "get",
"xmlns" => "status",
"id" => $this->createMsgId()
), array(
new ProtocolNode("status", null, $children, null)
), null);
$this->sendNode($node);
}
/**
* Create a group chat.
*
* @param string $subject
* The group Subject
* @param array $participants
* An array with the participants numbers.
*
* @return string
* The group ID.
*/
public function sendGroupsChatCreate($subject, $participants)
{
if (!is_array($participants)) {
$participants = array($participants);
}
$participantNode = array();
foreach ($participants as $participant) {
$participantNode[] = new ProtocolNode("participant", array(
"jid" => $this->getJID($participant)
), null, null);
}
$id = $this->nodeId['groupcreate'] = $this->createMsgId();
$createNode = new ProtocolNode("create",
array(
"subject" => $subject
), $participantNode, null);
$iqNode = new ProtocolNode("iq",
array(
"xmlns" => "w:g2",
"id" => $id,
"type" => "set",
"to" => Constants::WHATSAPP_GROUP_SERVER
), array($createNode), null);
$this->sendNode($iqNode);
$this->waitForServer($id);
$groupId = $this->groupId;
$this->eventManager()->fire("onGroupCreate",
array(
$this->phoneNumber,
$groupId
));
return $groupId;
}
/**
* Change group's subject.
*
* @param string $gjid The group id
* @param string $subject The subject
*/
public function sendSetGroupSubject($gjid, $subject)
{
$child = new ProtocolNode("subject", null, null, $subject);
$node = new ProtocolNode("iq",
array(
"id" => $this->createMsgId(),
"type" => "set",
"to" => $this->getJID($gjid),
"xmlns" => "w:g2"
), array($child), null);
$this->sendNode($node);
}
/**
* Leave a group chat.
*
* @param mixed $gjids Group or group's ID(s)
*/
public function sendGroupsLeave($gjids)
{
$msgId = $this->nodeId['leavegroup'] = $this->createMsgId();
if (!is_array($gjids)) {
$gjids = array($this->getJID($gjids));
}
$nodes = array();
foreach ($gjids as $gjid) {
$nodes[] = new ProtocolNode("group",
array(
"id" => $this->getJID($gjid)
), null, null);
}
$leave = new ProtocolNode("leave",
array(
'action'=>'delete'
), $nodes, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"to" => Constants::WHATSAPP_GROUP_SERVER,
"type" => "set",
"xmlns" => "w:g2"
), array($leave), null);
$this->sendNode($node);
$this->waitForServer($msgId);
}
/**
* Add participant(s) to a group.
*
* @param string $groupId The group ID.
* @param mixed $participants An array with the participants numbers to add
*/
public function sendGroupsParticipantsAdd($groupId, $participants)
{
$msgId = $this->createMsgId();
if (!is_array($participants)) {
$participants = array($participants);
}
$this->sendGroupsChangeParticipants($groupId, $participants, 'add', $msgId);
}
/**
* Remove participant(s) from a group.
*
* @param string $groupId The group ID.
* @param mixed $participants An array with the participants numbers to remove
*/
public function sendGroupsParticipantsRemove($groupId, $participants)
{
$msgId = $this->createMsgId();
if (!is_array($participants)) {
$participants = array($participants);
}
$this->sendGroupsChangeParticipants($groupId, $participants, 'remove', $msgId);
}
/**
* Promote participant(s) of a group; Make a participant an admin of a group.
*
* @param string $gId The group ID.
* @param mixed $participants An array with the participants numbers to promote
*/
public function sendPromoteParticipants($gId, $participants)
{
$msgId = $this->createMsgId();
if (!is_array($participants)) {
$participants = array($participants);
}
$this->sendGroupsChangeParticipants($gId, $participants, "promote", $msgId);
}
/**
* Demote participant(s) of a group; remove participant of being admin of a group.
*
* @param string $gId The group ID.
* @param array $participants An array with the participants numbers to demote
*/
public function sendDemoteParticipants($gId, $participants)
{
$msgId = $this->createMsgId();
if (!is_array($participants)) {
$participants = array($participants);
}
$this->sendGroupsChangeParticipants($gId, $participants, "demote", $msgId);
}
/**
* Lock group: participants cant change group subject or profile picture except admin.
*
* @param string $gId The group ID.
*/
public function sendLockGroup($gId)
{
$msgId = $this->createMsgId();
$lockedNode = new ProtocolNode("locked", null, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "w:g2",
"type" => "set",
"to" => $this->getJID($gId)
), array($lockedNode), null);
$this->sendNode($node);
$this->waitForServer($msgId);
}
/**
* Unlock group: Any participant can change group subject or profile picture.
*
*
* @param string $gId The group ID.
*/
public function sendUnlockGroup($gId)
{
$msgId = $this->createMsgId();
$unlockedNode = new ProtocolNode("unlocked", null, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "w:g2",
"type" => "set",
"to" => $this->getJID($gId)
), array($unlockedNode), null);
$this->sendNode($node);
$this->waitForServer($msgId);
}
/**
* Send a text message to the user/group.
*
* @param string $to The recipient.
* @param string $txt The text message.
* @param $id
*
* @return string Message ID.
*/
public function sendMessage($to, $txt, $id = null)
{
$txt = $this->parseMessageForEmojis($txt);
$bodyNode = new ProtocolNode("body", null, null, $txt);
$id = $this->sendMessageNode($to, $bodyNode, $id);
$this->waitForServer($id);
if ($this->messageStore !== null) {
$this->messageStore->saveMessage($this->phoneNumber, $to, $txt, $id, time());
}
return $id;
}
/**
* Send a read receipt to a message.
*
* @param $to The recipient.
* @param $id
*/
public function sendMessageRead($to, $id)
{
$messageNode = new ProtocolNode("receipt",
array(
"type" => "read",
"to" => $to,
"id" => $id,
"t" => time()
), null, null);
$this->sendNode($messageNode);
}
/**
* Send audio to the user/group.
*
* @param string $to The recipient.
* @param string $filepath The url/uri to the audio file.
* @param bool $storeURLmedia Keep copy of file
* @param int $fsize
* @param string $fhash *
* @return string|null Message ID if successfully, null if not.
*/
public function sendMessageAudio($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = "")
{
if ($fsize == 0 || $fhash == "") {
$allowedExtensions = array('3gp', 'caf', 'wav', 'mp3', 'wma', 'ogg', 'aif', 'aac', 'm4a');
$size = 10 * 1024 * 1024; // Easy way to set maximum file size for this media type.
// Return message ID. Make pull request for this.
return $this->sendCheckAndSendMedia($filepath, $size, $to, 'audio', $allowedExtensions, $storeURLmedia);
} else {
// Return message ID. Make pull request for this.
return $this->sendRequestFileUpload($fhash, 'audio', $fsize, $filepath, $to);
}
}
/**
* Send the composing message status. When typing a message.
*
* @param string $to The recipient to send status to.
*/
public function sendMessageComposing($to)
{
$this->sendChatState($to, "composing");
}
/**
* Send an image file to group/user.
*
* @param string $to Recipient number
* @param string $filepath The url/uri to the image file.
* @param bool $storeURLmedia Keep copy of file
* @param int $fsize size of the media file
* @param string $fhash base64 hash of the media file
* @param string $caption
* @return string|null Message ID if successfully, null if not.
*/
public function sendMessageImage($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = "", $caption = "")
{
$caption = $this->parseMessageForEmojis($caption);
if ($fsize == 0 || $fhash == "") {
$allowedExtensions = array('jpg', 'jpeg', 'gif', 'png');
$size = 5 * 1024 * 1024; // Easy way to set maximum file size for this media type.
// Return message ID. Make pull request for this.
return $this->sendCheckAndSendMedia($filepath, $size, $to, 'image', $allowedExtensions, $storeURLmedia, $caption);
} else {
// Return message ID. Make pull request for this.
return $this->sendRequestFileUpload($fhash, 'image', $fsize, $filepath, $to, $caption);
}
}
/**
* Send a location to the user/group.
*
* If no name is supplied, the receiver will see a large google maps thumbnail of the lat/long,
* but NO name or url of the location.
*
* When a name supplied, a combined map thumbnail/name box is displayed.
*
* @param mixed $to The recipient(s) to send the location to.
* @param float $long The longitude of the location, e.g. 54.31652.
* @param float $lat The latitude of the location, e.g. -6.833496.
* @param string $name (Optional) A custom name for the specified location.
* @param string $url (Optional) A URL to attach to the specified location.
* @return string Message ID
*/
public function sendMessageLocation($to, $long, $lat, $name = null, $url = null)
{
$mediaNode = new ProtocolNode("media",
array(
"type" => "location",
"encoding" => "raw",
"latitude" => $lat,
"longitude" => $long,
"name" => $name,
"url" => $url
), null, null);
$id = (is_array($to)) ? $this->sendBroadcast($to, $mediaNode, "media") : $this->sendMessageNode($to, $mediaNode);
$this->waitForServer($id);
// Return message ID. Make pull request for this.
return $id;
}
/**
* Send the 'paused composing message' status.
*
* @param string $to The recipient number or ID.
*/
public function sendMessagePaused($to)
{
$this->sendChatState($to, "paused");
}
protected function sendChatState($to, $state)
{
$node = new ProtocolNode("chatstate",
array(
"to" => $this->getJID($to)
), array(new ProtocolNode($state, null, null, null)), null);
$this->sendNode($node);
}
/**
* Send a video to the user/group.
*
* @param string $to The recipient to send.
* @param string $filepath A URL/URI to the MP4/MOV video.
* @param bool $storeURLmedia Keep a copy of media file.
* @param int $fsize Size of the media file
* @param string $fhash base64 hash of the media file
* @param string $caption *
* @return string|null Message ID if successfully, null if not.
*/
public function sendMessageVideo($to, $filepath, $storeURLmedia = false, $fsize = 0, $fhash = "", $caption = "")
{
$caption = $this->parseMessageForEmojis($caption);
if ($fsize == 0 || $fhash == "") {
$allowedExtensions = array('3gp', 'mp4', 'mov', 'avi');
$size = 20 * 1024 * 1024; // Easy way to set maximum file size for this media type.
// Return message ID. Make pull request for this.
return $this->sendCheckAndSendMedia($filepath, $size, $to, 'video', $allowedExtensions, $storeURLmedia, $caption);
} else {
// Return message ID. Make pull request for this.
return $this->sendRequestFileUpload($fhash, 'video', $fsize, $filepath, $to, $caption);
}
}
/**
* Send the next message.
*/
public function sendNextMessage()
{
if (count($this->outQueue) > 0) {
$msgnode = array_shift($this->outQueue);
$msgnode->refreshTimes();
$this->lastId = $msgnode->getAttribute('id');
$this->sendNode($msgnode);
} else {
$this->lastId = false;
}
}
/**
* Send the offline status. User will show up as "Offline".
*/
public function sendOfflineStatus()
{
$messageNode = new ProtocolNode("presence", array("type" => "inactive"), null, "");
$this->sendNode($messageNode);
}
/**
* Send a pong to the WhatsApp server. I'm alive!
*
* @param string $msgid The id of the message.
*/
public function sendPong($msgid)
{
$messageNode = new ProtocolNode("iq",
array(
"to" => Constants::WHATSAPP_SERVER,
"id" => $msgid,
"type" => "result"
), null, "");
$this->sendNode($messageNode);
$this->eventManager()->fire("onSendPong",
array(
$this->phoneNumber,
$msgid
));
}
public function sendAvailableForChat($nickname = null)
{
$presence = array();
if ($nickname) {
//update nickname
$this->name = $nickname;
}
$presence['name'] = $this->name;
$node = new ProtocolNode("presence", $presence, null, "");
$this->sendNode($node);
}
/**
* Send presence status.
*
* @param string $type The presence status.
*/
public function sendPresence($type = "active")
{
$node = new ProtocolNode("presence",
array(
"type" => $type
), null, "");
$this->sendNode($node);
$this->eventManager()->fire("onSendPresence",
array(
$this->phoneNumber,
$type,
$this->name
));
}
/**
* Send presence subscription, automatically receive presence updates as long as the socket is open.
*
* @param string $to Phone number.
*/
public function sendPresenceSubscription($to)
{
$node = new ProtocolNode("presence", array("type" => "subscribe", "to" => $this->getJID($to)), null, "");
$this->sendNode($node);
}
/**
* Unsubscribe, will stop subscription.
*
* @param string $to Phone number.
*/
public function sendPresenceUnsubscription($to)
{
$node = new ProtocolNode("presence", array("type" => "unsubscribe", "to" => $this->getJID($to)), null, "");
$this->sendNode($node);
}
/**
* Set the picture for the group.
*
* @param string $gjid The groupID
* @param string $path The URL/URI of the image to use
*/
public function sendSetGroupPicture($gjid, $path)
{
$this->sendSetPicture($gjid, $path);
}
/**
* Set the list of numbers you wish to block receiving from.
*
* @param mixed $blockedJids One or more numbers to block messages from.
*/
public function sendSetPrivacyBlockedList($blockedJids = array())
{
if (!is_array($blockedJids)) {
$blockedJids = array($blockedJids);
}
$items = array();
foreach ($blockedJids as $index => $jid) {
$item = new ProtocolNode("item",
array(
"type" => "jid",
"value" => $this->getJID($jid),
"action" => "deny",
"order" => $index + 1//WhatsApp stream crashes on zero index
), null, null);
$items[] = $item;
}
$child = new ProtocolNode("list",
array(
"name" => "default"
), $items, null);
$child2 = new ProtocolNode("query", null, array($child), null);
$node = new ProtocolNode("iq",
array(
"id" => $this->createMsgId(),
"xmlns" => "jabber:iq:privacy",
"type" => "set"
), array($child2), null);
$this->sendNode($node);
}
/**
* Set your profile picture.
*
* @param string $path URL/URI of image
*/
public function sendSetProfilePicture($path)
{
$this->sendSetPicture($this->phoneNumber, $path);
}
/*
* Removes the profile photo.
*/
public function sendRemoveProfilePicture()
{
$msgId = $this->createMsgId();
$picture = new ProtocolNode("picture", null, null, null);
$thumb = new ProtocolNode("picture",
array(
"type" => "preview"
), null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgId,
"to" => $this->getJID($this->phoneNumber),
"type" => "set",
"xmlns" => "w:profile:picture"
), array($picture, $thumb), null);
$this->sendNode($node);
}
/**
* Set the recovery token for your account to allow you to retrieve your password at a later stage.
*
* @param string $token A user generated token.
*/
public function sendSetRecoveryToken($token)
{
$child = new ProtocolNode("pin",
array(
"xmlns" => "w:ch:p"
), null, $token);
$node = new ProtocolNode("iq",
array(
"id" => $this->createMsgId(),
"type" => "set",
"to" => Constants::WHATSAPP_SERVER
), array($child), null);
$this->sendNode($node);
}
/**
* Update the user status.
*
* @param string $txt The text of the message status to send.
*/
public function sendStatusUpdate($txt)
{
$child = new ProtocolNode("status", null, null, $txt);
$node = new ProtocolNode("iq",
array(
"to" => Constants::WHATSAPP_SERVER,
"type" => "set",
"id" => $this->createMsgId(),
"xmlns" => "status"
), array($child), null);
$this->sendNode($node);
$this->eventManager()->fire("onSendStatusUpdate",
array(
$this->phoneNumber,
$txt
));
}
/**
* Send a vCard to the user/group.
*
* @param string $to The recipient to send.
* @param string $name The contact name.
* @param object $vCard The contact vCard to send.
* @return string Message ID
*/
public function sendVcard($to, $name, $vCard)
{
$vCardNode = new ProtocolNode("vcard",
array(
"name" => $name
), null, $vCard);
$mediaNode = new ProtocolNode("media",
array(
"type" => "vcard"
), array($vCardNode), "");
// Return message ID. Make pull request for this.
return $this->sendMessageNode($to, $mediaNode);
}
/**
* Send a vCard to the user/group as Broadcast.
*
* @param array $targets An array of recipients to send to.
* @param string $name The vCard contact name.
* @param object $vCard The contact vCard to send.
* @return string Message ID
*/
public function sendBroadcastVcard($targets, $name, $vCard)
{
$vCardNode = new ProtocolNode("vcard",
array(
"name" => $name
), null, $vCard);
$mediaNode = new ProtocolNode("media",
array(
"type" => "vcard"
), array($vCardNode), "");
// Return message ID. Make pull request for this.
return $this->sendBroadcast($targets, $mediaNode, "media");
}
/**
* Sets the bind of the new message.
*
* @param $bind
*/
public function setNewMessageBind($bind)
{
$this->newMsgBind = $bind;
}
/**
* Wait for WhatsApp server to acknowledge *it* has received message.
* @param string $id The id of the node sent that we are awaiting acknowledgement of.
* @param int $timeout
*/
public function waitForServer($id, $timeout = 5)
{
$time = time();
$this->serverReceivedId = false;
do {
$this->pollMessage();
} while ($this->serverReceivedId !== $id && time() - $time < $timeout);
}
/**
* Authenticate with the WhatsApp Server.
*
* @return string Returns binary string
*/
protected function authenticate()
{
$keys = KeyStream::GenerateKeys(base64_decode($this->password), $this->challengeData);
$this->inputKey = new KeyStream($keys[2], $keys[3]);
$this->outputKey = new KeyStream($keys[0], $keys[1]);
$array = "\0\0\0\0" . $this->phoneNumber . $this->challengeData;// . time() . Constants::WHATSAPP_USER_AGENT . " MccMnc/" . str_pad($phone["mcc"], 3, "0", STR_PAD_LEFT) . "001";
$response = $this->outputKey->EncodeMessage($array, 0, 4, strlen($array) - 4);
return $response;
}
/**
* Add the authentication nodes.
*
* @return ProtocolNode Returns an authentication node.
*/
protected function createAuthNode()
{
$data = $this->createAuthBlob();
$node = new ProtocolNode("auth", array(
'mechanism' => 'WAUTH-2',
'user' => $this->phoneNumber
), null, $data);
return $node;
}
protected function createAuthBlob()
{
if ($this->challengeData) {
$key = wa_pbkdf2('sha1', base64_decode($this->password), $this->challengeData, 16, 20, true);
$this->inputKey = new KeyStream($key[2], $key[3]);
$this->outputKey = new KeyStream($key[0], $key[1]);
$this->reader->setKey($this->inputKey);
//$this->writer->setKey($this->outputKey);
$phone = $this->dissectPhone();
$array = "\0\0\0\0" . $this->phoneNumber . $this->challengeData . time();
$this->challengeData = null;
return $this->outputKey->EncodeMessage($array, 0, strlen($array), false);
}
return null;
}
/**
* Add the auth response to protocoltreenode.
*
* @return ProtocolNode Returns a response node.
*/
protected function createAuthResponseNode()
{
return new ProtocolNode("response", null, null, $this->authenticate());
}
/**
* Add stream features.
*
* @return ProtocolNode Return itself.
*/
protected function createFeaturesNode()
{
$readreceipts = new ProtocolNode("readreceipts", null, null, null);
$groupsv2 = new ProtocolNode("groups_v2", null, null, null);
$privacy = new ProtocolNode("privacy", null, null, null);
$presencev2 = new ProtocolNode("presence", null, null, null);
$parent = new ProtocolNode("stream:features", null, array($readreceipts, $groupsv2, $privacy, $presencev2), null);
return $parent;
}
/**
* Create a unique msg id.
*
* @return string
* A message id string.
*/
protected function createMsgId()
{
$msgid = $this->messageCounter;
$this->messageCounter++;
return $this->loginTime . "-" . $msgid;
}
/**
* Print a message to the debug console.
*
* @param mixed $debugMsg The debug message.
* @return bool
*/
protected function debugPrint($debugMsg)
{
if ($this->debug) {
if (is_array($debugMsg) || is_object($debugMsg)) {
print_r($debugMsg);
}
else {
echo $debugMsg;
}
return true;
}
return false;
}
/**
* Dissect country code from phone number.
*
* @return array
* An associative array with country code and phone number.
* - country: The detected country name.
* - cc: The detected country code (phone prefix).
* - phone: The phone number.
* - ISO3166: 2-Letter country code
* - ISO639: 2-Letter language code
* Return false if country code is not found.
*/
protected function dissectPhone()
{
if (($handle = fopen(dirname(__FILE__).'/countries.csv', 'rb')) !== false) {
while (($data = fgetcsv($handle, 1000)) !== false) {
if (strpos($this->phoneNumber, $data[1]) === 0) {
// Return the first appearance.
fclose($handle);
$mcc = explode("|", $data[2]);
$mcc = $mcc[0];
//hook:
//fix country code for North America
if ($data[1][0] == "1") {
$data[1] = "1";
}
$phone = array(
'country' => $data[0],
'cc' => $data[1],
'phone' => substr($this->phoneNumber, strlen($data[1]), strlen($this->phoneNumber)),
'mcc' => $mcc,
'ISO3166' => @$data[3],
'ISO639' => @$data[4],
'mnc' => $data[5]
);
$this->eventManager()->fire("onDissectPhone",
array(
$this->phoneNumber,
$phone['country'],
$phone['cc'],
$phone['phone'],
$phone['mcc'],
$phone['ISO3166'],
$phone['ISO639'],
$phone['mnc']
)
);
return $phone;
}
}
fclose($handle);
}
$this->eventManager()->fire("onDissectPhoneFailed",
array(
$this->phoneNumber
));
return false;
}
/**
* Detects mnc from specified carrier.
*
* @param string $lc LangCode
* @param string $carrierName Name of the carrier
* @return string
*
* Returns mnc value
*/
protected function detectMnc($lc, $carrierName)
{
$fp = fopen(__DIR__ . DIRECTORY_SEPARATOR . 'networkinfo.csv', 'r');
$mnc = null;
while ($data = fgetcsv($fp, 0, ',')) {
if ($data[4] === $lc && $data[7] === $carrierName) {
$mnc = $data[2];
break;
}
}
if ($mnc == null) {
$mnc = '000';
}
fclose($fp);
return $mnc;
}
/**
* Send the nodes to the WhatsApp server to log in.
*
* @throws Exception
*/
protected function doLogin()
{
if ($this->isLoggedIn()) {
return true;
}
$this->writer->resetKey();
$this->reader->resetKey();
$resource = Constants::WHATSAPP_DEVICE . '-' . Constants::WHATSAPP_VER . '-' . Constants::PORT;
$data = $this->writer->StartStream(Constants::WHATSAPP_SERVER, $resource);
$feat = $this->createFeaturesNode();
$auth = $this->createAuthNode();
$this->sendData($data);
$this->sendNode($feat);
$this->sendNode($auth);
$this->pollMessage();
$this->pollMessage();
$this->pollMessage();
if ($this->challengeData != null) {
$data = $this->createAuthResponseNode();
$this->sendNode($data);
$this->reader->setKey($this->inputKey);
$this->writer->setKey($this->outputKey);
$this->pollMessage();
}
if ($this->loginStatus === Constants::DISCONNECTED_STATUS) {
throw new LoginFailureException();
}
$this->eventManager()->fire("onLogin",
array(
$this->phoneNumber
));
$this->sendAvailableForChat();
$this->loginTime = time();
return true;
}
/**
* Have we an active connection with WhatsAPP AND a valid login already?
*
* @return bool
*/
protected function isLoggedIn(){
//If you aren't connected you can't be logged in! ($this->isConnected())
//We are connected - but are we logged in? (the rest)
return ($this->isConnected() && !empty($this->loginStatus) && $this->loginStatus === Constants::CONNECTED_STATUS);
}
/**
* Create an identity string
*
* @param string $identity Identity.
* @return string Correctly formatted identity
*
* @throws Exception Error when cannot write identity data to file.
*/
protected function buildIdentity()
{
$identity_file = sprintf('%s%s%sid.%s.dat', __DIR__, DIRECTORY_SEPARATOR, Constants::DATA_FOLDER . DIRECTORY_SEPARATOR, $this->phoneNumber);
if (is_readable($identity_file)) {
$data = urldecode(file_get_contents($identity_file));
$length = strlen($data);
if ($length == 20 || $length == 16) {
return $data;
}
}
$bytes = strtolower(openssl_random_pseudo_bytes(20));
if (file_put_contents($identity_file, urlencode($bytes)) === false) {
throw new Exception('Unable to write identity file to ' . $identity_file);
}
return $bytes;
}
public function sendSync(array $numbers, array $deletedNumbers = null, $syncType = 4, $index = 0, $last = true)
{
$users = array();
for ($i=0; $i<count($numbers); $i++) { // number must start with '+' if international contact
$users[$i] = new ProtocolNode("user", null, null, (substr($numbers[$i], 0, 1) != '+')?('+' . $numbers[$i]):($numbers[$i]));
}
if ($deletedNumbers != null || count($deletedNumbers)) {
for ($j=0; $j<count($deletedNumbers); $j++, $i++) {
$users[$i] = new ProtocolNode("user", array("jid" => $this->getJID($deletedNumbers[$j]), "type" => "delete"), null, null);
}
}
switch($syncType)
{
case 0:
$mode = "full";
$context = "registration";
break;
case 1:
$mode = "full";
$context = "interactive";
break;
case 2:
$mode = "full";
$context = "background";
break;
case 3:
$mode = "delta";
$context = "interactive";
break;
case 4:
$mode = "delta";
$context = "background";
break;
case 5:
$mode = "query";
$context = "interactive";
break;
case 6:
$mode = "chunked";
$context = "registration";
break;
case 7:
$mode = "chunked";
$context = "interactive";
break;
case 8:
$mode = "chunked";
$context = "background";
break;
default:
$mode = "delta";
$context = "background";
}
$id = $this->createMsgId();
$node = new ProtocolNode("iq",
array(
"id" => $id,
"xmlns" => "urn:xmpp:whatsapp:sync",
"type" => "get"
), array(
new ProtocolNode("sync",
array(
"mode" => $mode,
"context" => $context,
"sid" => "".((time() + 11644477200) * 10000000),
"index" => "".$index,
"last" => $last ? "true" : "false"
), $users, null)
), null);
$this->sendNode($node);
$this->waitForServer($id);
return $id;
}
public function setMessageStore(MessageStoreInterface $messageStore)
{
$this->messageStore = $messageStore;
}
/**
* Process number/jid and turn it into a JID if necessary
*
* @param string $number
* Number to process
* @return string
*/
protected function getJID($number)
{
if (!stristr($number, '@')) {
//check if group message
if (stristr($number, '-')) {
//to group
$number .= "@" . Constants::WHATSAPP_GROUP_SERVER;
} else {
//to normal user
$number .= "@" . Constants::WHATSAPP_SERVER;
}
}
return $number;
}
/**
* Retrieves media file and info from either a URL or localpath
*
* @param string $filepath The URL or path to the mediafile you wish to send
* @param integer $maxsizebytes The maximum size in bytes the media file can be. Default 1MB
*
* @return bool false if file information can not be obtained.
*/
protected function getMediaFile($filepath, $maxsizebytes = 1048576)
{
if (filter_var($filepath, FILTER_VALIDATE_URL) !== false) {
$this->mediaFileInfo = array();
$this->mediaFileInfo['url'] = $filepath;
//File is a URL. Create a curl connection but DON'T download the body content
//because we want to see if file is too big.
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, "$filepath");
curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11");
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_NOBODY, true);
if (curl_exec($curl) === false) {
return false;
}
//While we're here, get mime type and filesize and extension
$info = curl_getinfo($curl);
$this->mediaFileInfo['filesize'] = $info['download_content_length'];
$this->mediaFileInfo['filemimetype'] = $info['content_type'];
$this->mediaFileInfo['fileextension'] = pathinfo(parse_url($this->mediaFileInfo['url'], PHP_URL_PATH), PATHINFO_EXTENSION);
//Only download file if it's not too big
//TODO check what max file size whatsapp server accepts.
if ($this->mediaFileInfo['filesize'] < $maxsizebytes) {
//Create temp file in media folder. Media folder must be writable!
$this->mediaFileInfo['filepath'] = tempnam(__DIR__ . DIRECTORY_SEPARATOR . Constants::DATA_FOLDER . DIRECTORY_SEPARATOR . Constants::MEDIA_FOLDER, 'WHA');
$fp = fopen($this->mediaFileInfo['filepath'], 'w');
if ($fp) {
curl_setopt($curl, CURLOPT_NOBODY, false);
curl_setopt($curl, CURLOPT_BUFFERSIZE, 1024);
curl_setopt($curl, CURLOPT_FILE, $fp);
curl_exec($curl);
fclose($fp);
} else {
unlink($this->mediaFileInfo['filepath']);
curl_close($curl);
return false;
}
//Success
curl_close($curl);
return true;
} else {
//File too big. Don't Download.
curl_close($curl);
return false;
}
} else if (file_exists($filepath)) {
//Local file
$this->mediaFileInfo['filesize'] = filesize($filepath);
if ($this->mediaFileInfo['filesize'] < $maxsizebytes) {
$this->mediaFileInfo['filepath'] = $filepath;
$this->mediaFileInfo['fileextension'] = pathinfo($filepath, PATHINFO_EXTENSION);
$this->mediaFileInfo['filemimetype'] = get_mime($filepath);
return true;
} else {
//File too big
return false;
}
}
//Couldn't tell what file was, local or URL.
return false;
}
/**
* Get a decoded JSON response from Whatsapp server
*
* @param string $host The host URL
* @param array $query A associative array of keys and values to send to server.
*
* @return null|object NULL if the json cannot be decoded or if the encoded data is deeper than the recursion limit
*/
protected function getResponse($host, $query)
{
// Build the url.
$url = $host . '?' . http_build_query($query);
// Open connection.
$ch = curl_init();
// Configure the connection.
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_USERAGENT, Constants::WHATSAPP_USER_AGENT);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: text/json'));
// This makes CURL accept any peer!
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// Get the response.
$response = curl_exec($ch);
// Close the connection.
curl_close($ch);
return json_decode($response);
}
/**
* Process the challenge.
*
* @param ProtocolNode $node The node that contains the challenge.
*/
protected function processChallenge($node)
{
$this->challengeData = $node->getData();
}
/**
* Process inbound data.
*
* @param $data
* @param bool $autoReceipt
* @param $type
*
* @throws Exception
*/
protected function processInboundData($data, $autoReceipt = true, $type = "read")
{
$node = $this->reader->nextTree($data);
if ($node != null) {
$this->processInboundDataNode($node, $autoReceipt, $type);
}
}
/**
* Will process the data from the server after it's been decrypted and parsed.
*
* This also provides a convenient method to use to unit test the event framework.
* @param ProtocolNode $node
* @param bool $autoReceipt
* @param $type
*
* @throws Exception
*/
protected function processInboundDataNode(ProtocolNode $node, $autoReceipt = true, $type = "read") {
$this->debugPrint($node->nodeString("rx ") . "\n");
$this->serverReceivedId = $node->getAttribute('id');
if ($node->getTag() == "challenge") {
$this->processChallenge($node);
} elseif ($node->getTag() == "failure") {
$this->loginStatus = Constants::DISCONNECTED_STATUS;
$this->eventManager()->fire("onLoginFailed",
array(
$this->phoneNumber,
$node->getChild(0)->getTag()
));
} elseif ($node->getTag() == "success") {
if ($node->getAttribute("status") == "active") {
$this->loginStatus = Constants::CONNECTED_STATUS;
$challengeData = $node->getData();
file_put_contents($this->challengeFilename, $challengeData);
$this->writer->setKey($this->outputKey);
$this->eventManager()->fire("onLoginSuccess",
array(
$this->phoneNumber,
$node->getAttribute("kind"),
$node->getAttribute("status"),
$node->getAttribute("creation"),
$node->getAttribute("expiration")
));
} elseif ($node->getAttribute("status") == "expired") {
$this->eventManager()->fire("onAccountExpired",
array(
$this->phoneNumber,
$node->getAttribute("kind"),
$node->getAttribute("status"),
$node->getAttribute("creation"),
$node->getAttribute("expiration")
));
}
} elseif ($node->getTag() == 'ack' && $node->getAttribute("class") == "message") {
$this->eventManager()->fire("onMessageReceivedServer",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('class'),
$node->getAttribute('t')
));
} elseif ($node->getTag() == 'receipt') {
if ($node->hasChild("list")) {
foreach ($node->getChild("list")->getChildren() as $child) {
$this->eventManager()->fire("onMessageReceivedClient",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$child->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('participant')
));
}
}
$this->eventManager()->fire("onMessageReceivedClient",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('participant')
));
$ackNode = new ProtocolNode("ack",
array(
"to" => $node->getAttribute('from'),
"id" => $node->getAttribute('id'),
"type" => $type,
"t" => time()
), null, null);
$this->sendNode($ackNode);
}
if ($node->getTag() == "message") {
array_push($this->messageQueue, $node);
if ($node->hasChild('x') && $this->lastId == $node->getAttribute('id')) {
$this->sendNextMessage();
}
if ($this->newMsgBind && ($node->getChild('body') || $node->getChild('media'))) {
$this->newMsgBind->process($node);
}
if ($node->getAttribute("type") == "text" && $node->getChild('body') != null) {
$author = $node->getAttribute("participant");
if ($author == "") {
//private chat message
$this->eventManager()->fire("onGetMessage",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute("notify"),
$node->getChild("body")->getData()
));
if ($this->messageStore !== null) {
$this->messageStore->saveMessage($node->getAttribute('from'), $this->phoneNumber, $node->getChild("body")->getData(), $node->getAttribute('id'), $node->getAttribute('t'));
}
} else {
//group chat message
$this->eventManager()->fire("onGetGroupMessage",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$author,
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute("notify"),
$node->getChild("body")->getData()
));
}
if ($autoReceipt) {
$this->sendMessageReceived($node, $type, $author);
}
}
if ($node->getAttribute("type") == "text" && $node->getChild(0)->getTag() == 'enc') {
// TODO
if ($autoReceipt) {
$this->sendMessageReceived($node, $type);
}
}
if ($node->getAttribute("type") == "media" && $node->getChild('media') != null) {
if ($node->getChild("media")->getAttribute('type') == 'image') {
if ($node->getAttribute("participant") == null) {
$this->eventManager()->fire("onGetImage",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('notify'),
$node->getChild("media")->getAttribute('size'),
$node->getChild("media")->getAttribute('url'),
$node->getChild("media")->getAttribute('file'),
$node->getChild("media")->getAttribute('mimetype'),
$node->getChild("media")->getAttribute('filehash'),
$node->getChild("media")->getAttribute('width'),
$node->getChild("media")->getAttribute('height'),
$node->getChild("media")->getData(),
$node->getChild("media")->getAttribute('caption')
));
} else {
$this->eventManager()->fire("onGetGroupImage",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('participant'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('notify'),
$node->getChild("media")->getAttribute('size'),
$node->getChild("media")->getAttribute('url'),
$node->getChild("media")->getAttribute('file'),
$node->getChild("media")->getAttribute('mimetype'),
$node->getChild("media")->getAttribute('filehash'),
$node->getChild("media")->getAttribute('width'),
$node->getChild("media")->getAttribute('height'),
$node->getChild("media")->getData(),
$node->getChild("media")->getAttribute('caption')
));
}
} elseif ($node->getChild("media")->getAttribute('type') == 'video') {
if ($node->getAttribute("participant") == null) {
$this->eventManager()->fire("onGetVideo",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('notify'),
$node->getChild("media")->getAttribute('url'),
$node->getChild("media")->getAttribute('file'),
$node->getChild("media")->getAttribute('size'),
$node->getChild("media")->getAttribute('mimetype'),
$node->getChild("media")->getAttribute('filehash'),
$node->getChild("media")->getAttribute('duration'),
$node->getChild("media")->getAttribute('vcodec'),
$node->getChild("media")->getAttribute('acodec'),
$node->getChild("media")->getData(),
$node->getChild("media")->getAttribute('caption')
));
} else {
$this->eventManager()->fire("onGetGroupVideo",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('participant'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('notify'),
$node->getChild("media")->getAttribute('url'),
$node->getChild("media")->getAttribute('file'),
$node->getChild("media")->getAttribute('size'),
$node->getChild("media")->getAttribute('mimetype'),
$node->getChild("media")->getAttribute('filehash'),
$node->getChild("media")->getAttribute('duration'),
$node->getChild("media")->getAttribute('vcodec'),
$node->getChild("media")->getAttribute('acodec'),
$node->getChild("media")->getData(),
$node->getChild("media")->getAttribute('caption')
));
}
} elseif ($node->getChild("media")->getAttribute('type') == 'audio') {
$author = $node->getAttribute("participant");
$this->eventManager()->fire("onGetAudio",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('notify'),
$node->getChild("media")->getAttribute('size'),
$node->getChild("media")->getAttribute('url'),
$node->getChild("media")->getAttribute('file'),
$node->getChild("media")->getAttribute('mimetype'),
$node->getChild("media")->getAttribute('filehash'),
$node->getChild("media")->getAttribute('seconds'),
$node->getChild("media")->getAttribute('acodec'),
$author,
));
} elseif ($node->getChild("media")->getAttribute('type') == 'vcard') {
if ($node->getChild("media")->hasChild('vcard')) {
$name = $node->getChild("media")->getChild("vcard")->getAttribute('name');
$data = $node->getChild("media")->getChild("vcard")->getData();
} else {
$name = "NO_NAME";
$data = $node->getChild("media")->getData();
}
$author = $node->getAttribute("participant");
$this->eventManager()->fire("onGetvCard",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('notify'),
$name,
$data,
$author
));
} elseif ($node->getChild("media")->getAttribute('type') == 'location') {
$url = $node->getChild("media")->getAttribute('url');
$name = $node->getChild("media")->getAttribute('name');
$author = $node->getAttribute("participant");
$this->eventManager()->fire("onGetLocation",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('notify'),
$name,
$node->getChild("media")->getAttribute('longitude'),
$node->getChild("media")->getAttribute('latitude'),
$url,
$node->getChild("media")->getData(),
$author
));
}
if ($autoReceipt) {
$this->sendMessageReceived($node, $type);
}
}
if ($node->getChild('received') != null) {
$this->eventManager()->fire("onMessageReceivedClient",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('type'),
$node->getAttribute('t'),
$node->getAttribute('participant')
));
}
}
if ($node->getTag() == "presence" && $node->getAttribute("status") == "dirty") {
//clear dirty
$categories = array();
if (count($node->getChildren()) > 0) {
foreach ($node->getChildren() as $child) {
if ($child->getTag() == "category") {
$categories[] = $child->getAttribute("name");
}
}
}
$this->sendClearDirty($categories);
}
if (strcmp($node->getTag(), "presence") == 0
&& strncmp($node->getAttribute('from'), $this->phoneNumber, strlen($this->phoneNumber)) != 0
&& strpos($node->getAttribute('from'), "-") === false) {
$presence = array();
if ($node->getAttribute('type') == null) {
$this->eventManager()->fire("onPresenceAvailable",
array(
$this->phoneNumber,
$node->getAttribute('from'),
));
} else {
$this->eventManager()->fire("onPresenceUnavailable",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('last')
));
}
}
if ($node->getTag() == "presence"
&& strncmp($node->getAttribute('from'), $this->phoneNumber, strlen($this->phoneNumber)) != 0
&& strpos($node->getAttribute('from'), "-") !== false
&& $node->getAttribute('type') != null) {
$groupId = Constants::parseJID($node->getAttribute('from'));
if ($node->getAttribute('add') != null) {
$this->eventManager()->fire("onGroupsParticipantsAdd",
array(
$this->phoneNumber,
$groupId,
Constants::parseJID($node->getAttribute('add'))
));
} elseif ($node->getAttribute('remove') != null) {
$this->eventManager()->fire("onGroupsParticipantsRemove",
array(
$this->phoneNumber,
$groupId,
Constants::parseJID($node->getAttribute('remove'))
));
}
}
if (strcmp($node->getTag(), "chatstate") == 0
&& strncmp($node->getAttribute('from'), $this->phoneNumber, strlen($this->phoneNumber)) != 0
&& strpos($node->getAttribute('from'), "-") === false) {
if($node->getChild(0)->getTag() == "composing"){
$this->eventManager()->fire("onMessageComposing",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
"composing",
$node->getAttribute('t')
));
} else {
$this->eventManager()->fire("onMessagePaused",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
"paused",
$node->getAttribute('t')
));
}
}
if ($node->getTag() == "iq"
&& $node->getAttribute('type') == "get"
&& $node->getAttribute('xmlns') == "urn:xmpp:ping") {
$this->eventManager()->fire("onPing",
array(
$this->phoneNumber,
$node->getAttribute('id')
));
$this->sendPong($node->getAttribute('id'));
}
if ($node->getTag() == "iq"
&& $node->getChild("sync") != null) {
//sync result
$sync = $node->getChild('sync');
$existing = $sync->getChild("in");
$nonexisting = $sync->getChild("out");
//process existing first
$existingUsers = array();
if (!empty($existing)) {
foreach ($existing->getChildren() as $child) {
$existingUsers[$child->getData()] = $child->getAttribute("jid");
}
}
//now process failed numbers
$failedNumbers = array();
if (!empty($nonexisting)) {
foreach ($nonexisting->getChildren() as $child) {
$failedNumbers[] = str_replace('+', '', $child->getData());
}
}
$index = $sync->getAttribute("index");
$result = new SyncResult($index, $sync->getAttribute("sid"), $existingUsers, $failedNumbers);
$this->eventManager()->fire("onGetSyncResult",
array(
$result
));
}
if ($node->getTag() == "receipt") {
$this->eventManager()->fire("onGetReceipt",
array(
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('offline'),
$node->getAttribute('retry')
));
}
if ($node->getTag() == "iq"
&& $node->getAttribute('type') == "result") {
if ($node->getChild("query") != null) {
if (isset($this->nodeId['privacy']) && ($this->nodeId['privacy'] == $node->getAttribute('id'))) {
$listChild = $node->getChild(0)->getChild(0);
foreach ($listChild->getChildren() as $child) {
$blockedJids[] = $child->getAttribute('value');
}
$this->eventManager()->fire("onGetPrivacyBlockedList",
array(
$this->phoneNumber,
$blockedJids
));
}
$this->eventManager()->fire("onGetRequestLastSeen",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getChild(0)->getAttribute('seconds')
));
array_push($this->messageQueue, $node);
}
if ($node->getChild("props") != null) {
//server properties
$props = array();
foreach($node->getChild(0)->getChildren() as $child) {
$props[$child->getAttribute("name")] = $child->getAttribute("value");
}
$this->eventManager()->fire("onGetServerProperties",
array(
$this->phoneNumber,
$node->getChild(0)->getAttribute("version"),
$props
));
}
if ($node->getChild("picture") != null) {
$this->eventManager()->fire("onGetProfilePicture",
array(
$this->phoneNumber,
$node->getAttribute("from"),
$node->getChild("picture")->getAttribute("type"),
$node->getChild("picture")->getData()
));
}
if ($node->getChild("media") != null || $node->getChild("duplicate") != null) {
$this->processUploadResponse($node);
}
if (strpos($node->getAttribute("from"), Constants::WHATSAPP_GROUP_SERVER) !== false) {
//There are multiple types of Group reponses. Also a valid group response can have NO children.
//Events fired depend on text in the ID field.
$groupList = array();
$groupNodes = array();
if ($node->getChild(0) != null && $node->getChild(0)->getChildren() != null) {
foreach ($node->getChild(0)->getChildren() as $child) {
$groupList[] = $child->getAttributes();
$groupNodes[] = $child;
}
}
if ($this->nodeId['groupcreate'] == $node->getAttribute('id')) {
$this->groupId = $node->getChild(0)->getAttribute('id');
$this->eventManager()->fire("onGroupsChatCreate",
array(
$this->phoneNumber,
$this->groupId
));
}
if ($this->nodeId['leavegroup'] == $node->getAttribute('id')) {
$this->groupId = $node->getChild(0)->getChild(0)->getAttribute('id');
$this->eventManager()->fire("onGroupsChatEnd",
array(
$this->phoneNumber,
$this->groupId
));
}
if ($this->nodeId['getgroups'] == $node->getAttribute('id')) {
$this->eventManager()->fire("onGetGroups",
array(
$this->phoneNumber,
$groupList
));
//getGroups returns a array of nodes which are exactly the same as from getGroupV2Info
//so lets call this event, we have all data at hand, no need to call getGroupV2Info for every
//group we are interested
foreach ($groupNodes AS $groupNode) {
$this->handleGroupV2InfoResponse($groupNode, true);
}
}
if ($this->nodeId['get_groupv2_info'] == $node->getAttribute('id')) {
$groupChild = $node->getChild(0);
if ($groupChild != null) {
$this->handleGroupV2InfoResponse($groupChild);
}
}
}
if (isset($this->nodeId['get_lists']) && ($this->nodeId['get_lists'] == $node->getAttribute('id'))) {
$broadcastLists = array();
if ($node->getChild(0) != null) {
$childArray = $node->getChildren();
foreach ($childArray as $list) {
if ($list->getChildren() != null) {
foreach ( $list->getChildren() as $sublist) {
$id = $sublist->getAttribute("id");
$name = $sublist->getAttribute("name");
$broadcastLists[$id]['name'] = $name;
$recipients = array();
foreach ($sublist->getChildren() as $recipient) {
array_push($recipients, $recipient->getAttribute('jid'));
}
$broadcastLists[$id]['recipients'] = $recipients;
}
}
}
}
$this->eventManager()->fire("onGetBroadcastLists",
array(
$this->phoneNumber,
$broadcastLists
));
}
if ($node->getChild("pricing") != null) {
$this->eventManager()->fire("onGetServicePricing",
array(
$this->phoneNumber,
$node->getChild(0)->getAttribute("price"),
$node->getChild(0)->getAttribute("cost"),
$node->getChild(0)->getAttribute("currency"),
$node->getChild(0)->getAttribute("expiration")
));
}
if ($node->getChild("extend") != null) {
$this->eventManager()->fire("onGetExtendAccount",
array(
$this->phoneNumber,
$node->getChild("account")->getAttribute("kind"),
$node->getChild("account")->getAttribute("status"),
$node->getChild("account")->getAttribute("creation"),
$node->getChild("account")->getAttribute("expiration")
));
}
if ($node->getChild("normalize") != null) {
$this->eventManager()->fire("onGetNormalizedJid",
array(
$this->phoneNumber,
$node->getChild(0)->getAttribute("result")
));
}
if ($node->getChild("status") != null) {
$child = $node->getChild("status");
foreach($child->getChildren() as $status)
{
$this->eventManager()->fire("onGetStatus",
array(
$this->phoneNumber,
$status->getAttribute("jid"),
"requested",
$node->getAttribute("id"),
$status->getAttribute("t"),
$status->getData()
));
}
}
}
if ($node->getTag() == "iq" && $node->getAttribute('type') == "error") {
$this->eventManager()->fire("onGetError",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getChild(0)
));
}
if ($node->getTag() == "message" && $node->getAttribute('type') == "media" && $node->getChild(0)->getAttribute('type') == "image" ) {
$msgId = $this->createMsgId();
$ackNode = new ProtocolNode("ack",
array(
"url" => $node->getChild(0)->getAttribute('url')
), null, null);
$iqNode = new ProtocolNode("iq",
array(
"id" => $msgId,
"xmlns" => "w:m",
"type" => "set",
"to" => Constants::WHATSAPP_SERVER
), array($ackNode), null);
$this->sendNode($iqNode);
}
$children = $node->getChild(0);
if ($node->getTag() == "stream:error" && !empty($children) && $node->getChild(0)->getTag() == "system-shutdown")
{
$this->eventManager()->fire("onStreamError",
array(
$node->getChild(0)->getTag()
));
}
if ($node->getTag() == "stream:error") {
$this->eventManager()->fire("onStreamError",
array(
$node->getChild(0)->getTag()
));
}
if ($node->getTag() == "notification") {
$name = $node->getAttribute("notify");
$type = $node->getAttribute("type");
switch($type)
{
case "status":
$this->eventManager()->fire("onGetStatus",
array(
$this->phoneNumber, //my number
$node->getAttribute("from"),
$node->getChild(0)->getTag(),
$node->getAttribute("id"),
$node->getAttribute("t"),
$node->getChild(0)->getData()
));
break;
case "picture":
if ($node->hasChild('set')) {
$this->eventManager()->fire("onProfilePictureChanged",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('t')
));
} else if ($node->hasChild('delete')) {
$this->eventManager()->fire("onProfilePictureDeleted",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('id'),
$node->getAttribute('t')
));
}
//TODO
break;
case "contacts":
//TODO
break;
case "encrypt":
$value = $node->getChild(0)->getAttribute('value');
if (is_numeric($value)) {
$this->eventManager()->fire("onGetKeysLeft",
array(
$this->phoneNumber,
$node->getChild(0)->getAttribute('value')
));
}
else {
echo "Corrupt Stream: value " . $value . "is not numeric";
}
break;
case "w:gp2":
if ($node->hasChild('remove')) {
if ($node->getChild(0)->hasChild('participant'))
$this->eventManager()->fire("onGroupsParticipantsRemove",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getChild(0)->getChild(0)->getAttribute('jid')
));
} else if ($node->hasChild('add')) {
$this->eventManager()->fire("onGroupsParticipantsAdd",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getChild(0)->getChild(0)->getAttribute('jid')
));
}
else if ($node->hasChild('create')) {
$groupMembers = array();
foreach ($node->getChild(0)->getChild(0)->getChildren() AS $cn) {
$groupMembers[] = $cn->getAttribute('jid');
}
$this->eventManager()->fire("onGroupisCreated",
array(
$this->phoneNumber,
$node->getChild(0)->getChild(0)->getAttribute('creator'),
$node->getChild(0)->getChild(0)->getAttribute('id'),
$node->getChild(0)->getChild(0)->getAttribute('subject'),
$node->getAttribute('participant'),
$node->getChild(0)->getChild(0)->getAttribute('creation'),
$groupMembers
));
}
else if ($node->hasChild('subject')) {
$this->eventManager()->fire("onGetGroupsSubject",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getAttribute('t'),
$node->getAttribute('participant'),
$node->getAttribute('notify'),
$node->getChild(0)->getAttribute('subject')
));
}
else if ($node->hasChild('promote')) {
$promotedJIDs = array();
foreach ($node->getChild(0)->getChildren() AS $cn) {
$promotedJIDs[] = $cn->getAttribute('jid');
}
$this->eventManager()->fire("onGroupsParticipantsPromote",
array(
$this->phoneNumber,
$node->getAttribute('from'), //Group-JID
$node->getAttribute('t'), //Time
$node->getAttribute('participant'), //Issuer-JID
$node->getAttribute('notify'), //Issuer-Name
$promotedJIDs,
)
);
}
break;
case "account":
if (($node->getChild(0)->getAttribute('author')) == "")
$author = "Paypal";
else
$author = $node->getChild(0)->getAttribute('author');
$this->eventManager()->fire("onPaidAccount",
array(
$this->phoneNumber,
$author,
$node->getChild(0)->getChild(0)->getAttribute('kind'),
$node->getChild(0)->getChild(0)->getAttribute('status'),
$node->getChild(0)->getChild(0)->getAttribute('creation'),
$node->getChild(0)->getChild(0)->getAttribute('expiration')
));
break;
case "features":
if ($node->getChild(0)->getChild(0) == "encrypt") {
$this->eventManager()->fire("onGetFeature",
array(
$this->phoneNumber,
$node->getAttribute('from'),
$node->getChild(0)->getChild(0)->getAttribute('value'),
));
}
break;
default:
throw new Exception("Method $type not implemented");
}
$this->sendNotificationAck($node);
}
if ($node->getTag() == "ib")
{
foreach($node->getChildren() as $child)
{
switch($child->getTag())
{
case "dirty":
$this->sendClearDirty(array($child->getAttribute("type")));
break;
case "account":
$this->eventManager()->fire("onPaymentRecieved",
array(
$this->phoneNumber,
$child->getAttribute("kind"),
$child->getAttribute("status"),
$child->getAttribute("creation"),
$child->getAttribute("expiration")
));
break;
case "offline":
break;
default:
throw new Exception("ib handler for " . $child->getTag() . " not implemented");
}
}
}
// Disconnect socket on stream error.
if ($node->getTag() == "stream:error")
{
$this->disconnect();
}
}
/**
* @param $node ProtocolNode
*/
protected function sendNotificationAck($node)
{
$from = $node->getAttribute("from");
$to = $node->getAttribute("to");
$participant = $node->getAttribute("participant");
$id = $node->getAttribute("id");
$type = $node->getAttribute("type");
$attributes = array();
if ($to)
$attributes["from"] = $to;
if ($participant)
$attributes["participant"] = $participant;
$attributes["to"] = $from;
$attributes["class"] = "notification";
$attributes["id"] = $id;
$attributes["type"] = $type;
$ack = new ProtocolNode("ack", $attributes, null, null);
$this->sendNode($ack);
}
/**
* Process and save media image.
*
* @param ProtocolNode $node ProtocolNode containing media
*/
protected function processMediaImage($node)
{
$media = $node->getChild("media");
if ($media != null) {
$filename = $media->getAttribute("file");
$url = $media->getAttribute("url");
//save thumbnail
file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . Constants::DATA_FOLDER . DIRECTORY_SEPARATOR . Constants::MEDIA_FOLDER . DIRECTORY_SEPARATOR . 'thumb_' . $filename, $media->getData());
//download and save original
file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . Constants::DATA_FOLDER . DIRECTORY_SEPARATOR . Constants::MEDIA_FOLDER . DIRECTORY_SEPARATOR . $filename, file_get_contents($url));
}
}
/**
* Processes received picture node.
*
* @param ProtocolNode $node ProtocolNode containing the picture
*/
protected function processProfilePicture($node)
{
$pictureNode = $node->getChild("picture");
if ($pictureNode != null) {
if ($pictureNode->getAttribute("type") == "preview") {
$filename = __DIR__ . DIRECTORY_SEPARATOR . Constants::DATA_FOLDER . DIRECTORY_SEPARATOR . Constants::PICTURES_FOLDER . DIRECTORY_SEPARATOR . 'preview_' . $node->getAttribute('from') . 'jpg';
} else {
$filename = __DIR__ . DIRECTORY_SEPARATOR . Constants::DATA_FOLDER . DIRECTORY_SEPARATOR . Constants::PICTURES_FOLDER . DIRECTORY_SEPARATOR . $node->getAttribute('from') . '.jpg';
}
file_put_contents($filename, $pictureNode->getData());
}
}
/**
* If the media file was originally from a URL, this function either deletes it
* or renames it depending on the user option.
*
* @param bool $storeURLmedia Save or delete the media file from local server
*/
protected function processTempMediaFile($storeURLmedia)
{
if (isset($this->mediaFileInfo['url'])) {
if ($storeURLmedia) {
if (is_file($this->mediaFileInfo['filepath'])) {
rename($this->mediaFileInfo['filepath'], $this->mediaFileInfo['filepath'] . $this->mediaFileInfo['fileextension']);
}
} else {
if (is_file($this->mediaFileInfo['filepath'])) {
unlink($this->mediaFileInfo['filepath']);
}
}
}
}
/**
* Process media upload response
*
* @param ProtocolNode $node Message node
* @return bool
*/
protected function processUploadResponse($node)
{
$id = $node->getAttribute("id");
$messageNode = @$this->mediaQueue[$id];
if ($messageNode == null) {
//message not found, can't send!
$this->eventManager()->fire("onMediaUploadFailed",
array(
$this->phoneNumber,
$id,
$node,
$messageNode,
"Message node not found in queue"
));
return false;
}
$duplicate = $node->getChild("duplicate");
if ($duplicate != null) {
//file already on whatsapp servers
$url = $duplicate->getAttribute("url");
$filesize = $duplicate->getAttribute("size");
// $mimetype = $duplicate->getAttribute("mimetype");
$filehash = $duplicate->getAttribute("filehash");
$filetype = $duplicate->getAttribute("type");
// $width = $duplicate->getAttribute("width");
// $height = $duplicate->getAttribute("height");
$exploded = explode("/", $url);
$filename = array_pop($exploded);
} else {
//upload new file
$json = WhatsMediaUploader::pushFile($node, $messageNode, $this->mediaFileInfo, $this->phoneNumber);
if (!$json) {
//failed upload
$this->eventManager()->fire("onMediaUploadFailed",
array(
$this->phoneNumber,
$id,
$node,
$messageNode,
"Failed to push file to server"
));
return false;
}
$url = $json->url;
$filesize = $json->size;
// $mimetype = $json->mimetype;
$filehash = $json->filehash;
$filetype = $json->type;
// $width = $json->width;
// $height = $json->height;
$filename = $json->name;
}
$mediaAttribs = array();
$mediaAttribs["type"] = $filetype;
$mediaAttribs["url"] = $url;
$mediaAttribs["encoding"] = "raw";
$mediaAttribs["file"] = $filename;
$mediaAttribs["size"] = $filesize;
if ($this->mediaQueue[$id]['caption'] != '') {
$mediaAttribs["caption"] = $this->mediaQueue[$id]['caption'];
}
$filepath = $this->mediaQueue[$id]['filePath'];
$to = $this->mediaQueue[$id]['to'];
$icon = "";
switch ($filetype) {
case "image":
$caption = $this->mediaQueue[$id]['caption'];
$icon = createIcon($filepath);
break;
case "video":
$caption = $this->mediaQueue[$id]['caption'];
$icon = createVideoIcon($filepath);
break;
default:
$caption = '';
$icon = '';
break;
}
//Retrieve Message ID
$message_id = $messageNode['message_id'];
$mediaNode = new ProtocolNode("media", $mediaAttribs, null, $icon);
if (is_array($to)) {
$this->sendBroadcast($to, $mediaNode, "media");
} else {
$this->sendMessageNode($to, $mediaNode, $message_id);
}
$this->eventManager()->fire("onMediaMessageSent",
array(
$this->phoneNumber,
$to,
$id,
$filetype,
$url,
$filename,
$filesize,
$filehash,
$caption,
$icon
));
return true;
}
/**
* Read 1024 bytes from the whatsapp server.
*
* @throws Exception
*/
public function readStanza()
{
$buff = '';
if ($this->socket != null) {
$header = @socket_read($this->socket, 3);//read stanza header
if ($header === false) {
$error = "socket EOF, closing socket...";
socket_close($this->socket);
$this->socket = null;
$this->eventManager()->fire("onClose",
array(
$this->phoneNumber,
$error
)
);
}
if (strlen($header) == 0) {
//no data received
return;
}
if (strlen($header) != 3) {
throw new ConnectionException("Failed to read stanza header");
}
$treeLength = (ord($header[0]) & 0x0F) << 16;
$treeLength |= ord($header[1]) << 8;
$treeLength |= ord($header[2]) << 0;
//read full length
$buff = socket_read($this->socket, $treeLength);
//$trlen = $treeLength;
$len = strlen($buff);
//$prev = 0;
while (strlen($buff) < $treeLength) {
$toRead = $treeLength - strlen($buff);
$buff .= socket_read($this->socket, $toRead);
if ($len == strlen($buff)) {
//no new data read, fuck it
break;
}
$len = strlen($buff);
}
if (strlen($buff) != $treeLength) {
throw new ConnectionException("Tree length did not match received length (buff = " . strlen($buff) . " & treeLength = $treeLength)");
}
$buff = $header . $buff;
} else {
$this->eventManager()->fire("onDisconnect",
array(
$this->phoneNumber,
$this->socket
));
}
return $buff;
}
/**
* Checks that the media file to send is of allowable filetype and within size limits.
*
* @param string $filepath The URL/URI to the media file
* @param int $maxSize Maximum filesize allowed for media type
* @param string $to Recipient ID/number
* @param string $type media filetype. 'audio', 'video', 'image'
* @param array $allowedExtensions An array of allowable file types for the media file
* @param bool $storeURLmedia Keep a copy of the media file
* @param string $caption *
* @return string|null Message ID if successfully, null if not.
*/
protected function sendCheckAndSendMedia($filepath, $maxSize, $to, $type, $allowedExtensions, $storeURLmedia, $caption = "")
{
if ($this->getMediaFile($filepath, $maxSize) == true) {
if (in_array($this->mediaFileInfo['fileextension'], $allowedExtensions)) {
$b64hash = base64_encode(hash_file("sha256", $this->mediaFileInfo['filepath'], true));
//request upload and get Message ID
$id =$this->sendRequestFileUpload($b64hash, $type, $this->mediaFileInfo['filesize'], $this->mediaFileInfo['filepath'], $to, $caption);
$this->processTempMediaFile($storeURLmedia);
// Return message ID. Make pull request for this.
return $id;
} else {
//Not allowed file type.
$this->processTempMediaFile($storeURLmedia);
return null;
}
} else {
//Didn't get media file details.
return null;
}
}
/**
* Send a broadcast
* @param array $targets Array of numbers to send to
* @param object $node
* @param $type
* @return string
*/
protected function sendBroadcast($targets, $node, $type)
{
if (!is_array($targets)) {
$targets = array($targets);
}
$toNodes = array();
foreach ($targets as $target) {
$jid = $this->getJID($target);
$hash = array("jid" => $jid);
$toNode = new ProtocolNode("to", $hash, null, null);
$toNodes[] = $toNode;
}
$broadcastNode = new ProtocolNode("broadcast", null, $toNodes, null);
$msgId = $this->createMsgId();
$messageNode = new ProtocolNode("message",
array(
"to" => time()."@broadcast",
"type" => $type,
"id" => $msgId
), array($node, $broadcastNode), null);
$this->sendNode($messageNode);
$this->waitForServer($msgId);
//listen for response
$this->eventManager()->fire("onSendMessage",
array(
$this->phoneNumber,
$targets,
$msgId,
$node
));
return $msgId;
}
/**
* Send data to the WhatsApp server.
* @param string $data
*
* @throws Exception
*/
protected function sendData($data)
{
if ($this->socket != null) {
if (socket_write($this->socket, $data, strlen($data)) === false) {
$this->disconnect();
throw new ConnectionException('Connection Closed!');
}
}
}
/**
* Send the getGroupList request to WhatsApp
* @param string $type Type of list of groups to retrieve. "owning" or "participating"
*/
protected function sendGetGroupsFiltered($type)
{
$msgID = $this->nodeId['getgroups'] = $this->createMsgId();
$child = new ProtocolNode($type, null, null, null);
$node = new ProtocolNode("iq",
array(
"id" => $msgID,
"type" => "get",
"xmlns" => "w:g2",
"to" => Constants::WHATSAPP_GROUP_SERVER
), array($child), null);
$this->sendNode($node);
$this->waitForServer($msgID);
}
/**
* Change participants of a group.
*
* @param string $groupId The group ID.
* @param array $participants An array with the participants.
* @param string $tag The tag action. 'add' or 'remove'
* @param $id
*/
protected function sendGroupsChangeParticipants($groupId, $participants, $tag, $id)
{
$_participants = array();
foreach ($participants as $participant) {
$_participants[] = new ProtocolNode("participant", array("jid" => $this->getJID($participant)), null, "");
}
$childHash = array();
$child = new ProtocolNode($tag, $childHash, $_participants, "");
$node = new ProtocolNode("iq",
array(
"id" => $id,
"type" => "set",
"xmlns" => "w:g2",
"to" => $this->getJID($groupId)
), array($child), "");
$this->sendNode($node);
$this->waitForServer($id);
}
/**
* Send node to the servers.
*
* @param $to
* @param ProtocolNode $node
* @param null $id
*
* @return string Message ID.
*/
protected function sendMessageNode($to, $node, $id = null)
{
$msgId = ($id == null) ? $this->createMsgId() : $id;
$to = $this->getJID($to);
$messageNode = new ProtocolNode("message", array(
'to' => $to,
'type' => ($node->getTag() == "body") ? 'text' : 'media',
'id' => $msgId,
't' => time()
), array($node), "");
$this->sendNode($messageNode);
$this->eventManager()->fire("onSendMessage",
array(
$this->phoneNumber,
$to,
$msgId,
$node
));
$this->waitForServer($msgId);
return $msgId;
}
/**
* Tell the server we received the message.
*
* @param ProtocolNode $msg The ProtocolTreeNode that contains the message.
* @param string $type
* @param string $participant
*/
protected function sendMessageReceived($msg, $type = "read", $participant = null)
{
$messageHash = array();
if ($type == "read") {
$messageHash["type"] = $type;
}
if ($participant != null) {
$messageHash["participant"] = $participant;
}
$messageHash["to"] = $msg->getAttribute("from");
$messageHash["id"] = $msg->getAttribute("id");
$messageHash["t"] = time();
$messageNode = new ProtocolNode("receipt", $messageHash, null, null);
$this->sendNode($messageNode);
$this->eventManager()->fire("onSendMessageReceived",
array(
$this->phoneNumber,
$msg->getAttribute("id"),
$msg->getAttribute("from"),
$type
));
}
/**
* Send node to the WhatsApp server.
* @param ProtocolNode $node
* @param bool $encrypt
*/
protected function sendNode($node, $encrypt = true)
{
$this->debugPrint($node->nodeString("tx ") . "\n");
$this->sendData($this->writer->write($node, $encrypt));
}
/**
* Send request to upload file
*
* @param string $b64hash A base64 hash of file
* @param string $type File type
* @param string $size File size
* @param string $filepath Path to image file
* @param mixed $to Recipient(s)
* @param string $caption
* @return string Message ID
*/
protected function sendRequestFileUpload($b64hash, $type, $size, $filepath, $to, $caption = "")
{
$id = $this->createMsgId();
if (!is_array($to)) {
$to = $this->getJID($to);
}
$mediaNode = new ProtocolNode("media", array(
'hash' => $b64hash,
'type' => $type,
'size' => $size
), null, null);
$node = new ProtocolNode("iq", array(
'id' => $id,
'to' => Constants::WHATSAPP_SERVER,
'type' => 'set',
'xmlns' => 'w:m'
), array($mediaNode), null);
//add to queue
$messageId = $this->createMsgId();
$this->mediaQueue[$id] = array(
"messageNode" => $node,
"filePath" => $filepath,
"to" => $to,
"message_id" => $messageId,
"caption" => $caption
);
$this->sendNode($node);
$this->waitForServer($id);
// Return message ID. Make pull request for this.
return $messageId;
}
/**
* Set your profile picture
*
* @param string $jid
* @param string $filepath URL or localpath to image file
*/
protected function sendSetPicture($jid, $filepath)
{
$nodeID = $this->createMsgId();
$data = preprocessProfilePicture($filepath);
$preview = createIconGD($filepath, 96, true);
$picture = new ProtocolNode("picture", array("type" => "image"), null, $data);
$preview = new ProtocolNode("picture", array("type" => "preview"), null, $preview);
$node = new ProtocolNode("iq", array(
'id' => $nodeID,
'to' => $this->getJID($jid),
'type' => 'set',
'xmlns' => 'w:profile:picture'
), array($picture, $preview), null);
$this->sendNode($node);
$this->waitForServer($nodeID);
}
/**
* Parse the message text for emojis
*
* This will look for special strings in the message text
* that need to be replaced with a unicode character to show
* the corresponding emoji.
*
* Emojis should be entered in the message text either as the
* correct unicode character directly, or if this isn't possible,
* by putting a placeholder of ##unicodeNumber## in the message text.
* Include the surrounding ##
* eg:
* ##1f604## this will show the smiling face
* ##1f1ec_1f1e7## this will show the UK flag.
*
* Notice that if 2 unicode characters are required they should be joined
* with an underscore.
*
*
* @param string $txt The message to be parsed for emoji code.
*
* @return string
*/
private function parseMessageForEmojis($txt)
{
$matches = null;
preg_match_all('/##(.*?)##/', $txt, $matches, PREG_SET_ORDER);
if (is_array($matches)) {
foreach ($matches as $emoji) {
$txt = str_ireplace($emoji[0], $this->unichr((string) $emoji[1]), $txt);
}
}
return $txt;
}
/**
* Creates the correct unicode character from the unicode code point
*
* @param integer $int
*
* @return string
*/
private function unichr($int)
{
$string = null;
$multiChars = explode('_', $int);
foreach ($multiChars as $char) {
$string .= mb_convert_encoding('&#' . intval($char, 16) . ';', 'UTF-8', 'HTML-ENTITIES');
}
return $string;
}
/**
* @param string $jid
*
* @return string
*/
public static function parseJID($jid)
{
$parts = explode('@', $jid);
$parts = reset($parts);
return $parts;
}
/**
* @param ProtocolNode $groupNode
*/
protected function handleGroupV2InfoResponse(ProtocolNode $groupNode, $fromGetGroups = false)
{
$creator = $groupNode->getAttribute('creator');
$creation = $groupNode->getAttribute('creation');
$subject = $groupNode->getAttribute('subject');
$groupID = $groupNode->getAttribute('id');
$participants = array();
$admins = array();
if ($groupNode->getChild(0) != null) {
foreach ($groupNode->getChildren() as $child) {
$participants[] = $child->getAttribute('jid');
if ($child->getAttribute('type') == "admin")
$admins[] = $child->getAttribute('jid');
}
}
$this->eventManager()->fire("onGetGroupV2Info",
array(
$this->phoneNumber,
$groupID,
$creator,
$creation,
$subject,
$participants,
$admins,
$fromGetGroups
)
);
}
}
Jump to Line
Something went wrong with that request. Please try again.