Permalink
Browse files

Lots of changes, new outqueue system, auto msgid, auto send nickname …

…and presence, node processing, and more see testprotocol.php for more details
  • Loading branch information...
1 parent 952e377 commit ca4be92b37044e028bb01e349734d5fc33233fc3 @beldar beldar committed Nov 27, 2012
Showing with 173 additions and 54 deletions.
  1. +19 −1 src/php/protocol.class.php
  2. +45 −19 src/php/testprotocol.php
  3. +108 −34 src/php/whatsprot.class.php
  4. +1 −0 tests/Gift.jpgb64
View
20 src/php/protocol.class.php
@@ -21,7 +21,7 @@ public function getInput()
class ProtocolNode
{
- public $_tag;
+ public $_tag;
public $_attributeHash;
public $_children;
public $_data;
@@ -91,6 +91,24 @@ public function getChild($tag)
}
return NULL;
}
+
+ public function hasChild($tag)
+ {
+ return $this->getChild($tag)==null ? false : true;
+ }
+
+ public function refreshTimes($offset=0)
+ {
+ if(isset($this->_attributeHash['id']))
+ {
+ $id = $this->_attributeHash['id'];
+ $parts = explode('-',$id);
+ $parts[0] = time()+$offset;
+ $this->_attributeHash['id'] = implode('-',$parts);
+ }
+ if(isset($this->_attributeHash['t']))
+ $this->_attributeHash['t'] = time();
+ }
}
class BinTreeNodeReader
View
64 src/php/testprotocol.php
@@ -5,36 +5,62 @@
# IMEI here as it is!
$options = getopt("d::", array("debug::"));
$debug = (array_key_exists("debug", $options) || array_key_exists("d", $options)) ? true : false;
-$w = new WhatsProt("*****", "****", "John Doe", $debug);
+# Target phone number
+$target = "**********";
+
+$w = new WhatsProt("************", "********", "John Doe", true);
$w->Connect();
+# Now Login function sends Nickname and (Available) Presence
$w->Login();
-$w->SendPrecense('available');
-$w->Message(time() . "-1", "****", "Hola");
-$w->WaitforReceipt();
-# To send an image, put the image on the internet somewhere and send it with the MessageImage function.
-# if you want to be real fancy then take a scaled down version (100pixel) and base64_encode it,
-# and send that as the last parameter to the MessageImage function.
-# I wrote a quick function (funcs.php->createIcon) to do all this for you.
-# You can also just leave the last param empty to send no icon
-# Obviously this needs better integration... but this is a start...
-# Also thumb.jpgb64 is just for an example!
-$iconfile = "../../tests/thumb.jpgb64";
-$fp = fopen($iconfile, "r");
-$icon = fread($fp, filesize($iconfile));
-fclose($fp);
+# Implemented out queue messages and auto msgid
+$w->Message($target, "1");
+$w->Message($target, "2");
+$w->Message($target, "3");
+$w->Message($target, "4");
+$w->Message($target, "5");
+# You can create a ProcessNode class (or whatever name you want) that has a process($node) function
+# and pass it through setNewMessageBind, that way everytime the class receives a text message it will run
+# the process function to it.
+$pn = new ProcessNode($w,$target);
+$w->setNewMessageBind($pn);
-$w->MessageImage(time() . "-1", "*****", "https://lh3.googleusercontent.com/-vT0wjhrlTaQ/T_bwd4_PUYI/AAAAAAAABog/oKPZ6ssJqC0/s673/DSC02471.JPG", "DSC02471.jpg", 55508, $icon);
-$w->WaitforReceipt();
-$w->MessageImage(time() . "-1", "******", "https://lh3.googleusercontent.com/-vT0wjhrlTaQ/T_bwd4_PUYI/AAAAAAAABog/oKPZ6ssJqC0/s673/DSC02471.JPG", "DSC02471.jpg", 55508, $icon);
while(1)
{
$w->PollMessages();
$msgs = $w->GetMessages();
foreach ($msgs as $m)
{
# process inbound messages
- print($m->NodeString("") . "\n");
+ //print($m->NodeString("") . "\n");
+ }
+}
+
+class ProcessNode{
+ protected $_wp = false;
+ protected $_target = false;
+ function __construct($wp,$target)
+ {
+ $this->_wp = $wp;
+ $this->_target = $target;
+ }
+ public function process($node)
+ {
+ # Example of process function, you have to guess a number (psss it's 5)
+ # If you guess it right you get a gift
+ $text = $node->getChild('body');
+ $text = $text->_data;
+ if($text && ($text == "5" || trim($text)=="5")){
+ $iconfile = "../../tests/Gift.jpgb64";
+ $fp = fopen($iconfile, "r");
+ $icon = fread($fp, filesize($iconfile));
+ fclose($fp);
+ $this->_wp->MessageImage($this->_target, "https://mms604.whatsapp.net/d11/26/09/8/5/85a13e7812a5e7ad1f8071319d9d1b43.jpg", "hero.jpg", 84712, $icon);
+ $this->_wp->Message($this->_target, "¡Congratulations you guessed the right number!");
+ }else{
+ $this->_wp->Message($this->_target, "¡I'm sorry, try again!");
+ }
}
+
}
?>
View
142 src/php/whatsprot.class.php
@@ -24,7 +24,9 @@ class WhatsProt
protected $_accountinfo;
protected $_messageQueue = array();
-
+ protected $_outQueue = array();
+ protected $_lastId = false;
+ protected $_msgCounter = 1;
protected $_socket;
protected $_writer;
protected $_reader;
@@ -68,11 +70,12 @@ protected function addAuth()
public function encryptPassword()
{
if(stripos($this->_imei, ":") !== false){
- $this->_imei = strtoupper($this->_imei);
- return md5($this->_imei.$this->_imei);
+ $this->_imei = strtoupper($this->_imei);
+ return md5($this->_imei.$this->_imei);
}
- else {
- return md5(strrev($this->_imei));
+ else
+ {
+ return md5(strrev($this->_imei));
}
}
@@ -85,6 +88,16 @@ protected function authenticate()
$response = $this->_outputKey->encode($array, 0, strlen($array), false);
return $response;
}
+
+ public function setNewMessageBind($bind)
+ {
+ $this->_newmsgBind = $bind;
+ }
+
+ public function addOutQueue($node)
+ {
+ $this->_outQueue[] = $node;
+ }
protected function addAuthResponse()
{
@@ -126,25 +139,23 @@ protected function processChallenge($node)
protected function sendMessageReceived($msg)
{
$requestNode = $msg->getChild("request");
- if ($requestNode != null)
+ $receivedNode = $msg->getChild("received");
+ if ($requestNode != null || $receivedNode != null)
{
- $xmlnsAttrib = $requestNode->getAttribute("xmlns");
- if (strcmp($xmlnsAttrib, "urn:xmpp:receipts") == 0)
- {
- $recievedHash = array();
- $recievedHash["xmlns"] = "urn:xmpp:receipts";
- $receivedNode = new ProtocolNode("received", $recievedHash, null, "");
-
- $messageHash = array();
- $messageHash["to"] = $msg->getAttribute("from");
- $messageHash["type"] = "chat";
- $messageHash["id"] = $msg->getAttribute("id");
- $messageNode = new ProtocolNode("message", $messageHash, array($receivedNode), "");
- $this->sendNode($messageNode);
- }
+ $recievedHash = array();
+ $recievedHash["xmlns"] = "urn:xmpp:receipts";
+ $receivedNode = new ProtocolNode("received", $recievedHash, null, "");
+
+ $messageHash = array();
+ $messageHash["to"] = $msg->getAttribute("from");
+ $messageHash["type"] = "chat";
+ $messageHash["id"] = $msg->getAttribute("id");
+ $messageHash["t"] = time();
+ $messageNode = new ProtocolNode("message", $messageHash, array($receivedNode), "");
+ $this->sendNode($messageNode);
}
}
-
+
protected function processInboundData($data)
{
try
@@ -166,6 +177,10 @@ protected function processInboundData($data)
{
array_push($this->_messageQueue, $node);
$this->sendMessageReceived($node);
+ if($node->hasChild('x') && $this->_lastId==$node->getAttribute('id'))
+ $this->sendNext();
+ if($this->_newmsgBind && $node->getChild('body'))
+ $this->_newmsgBind->process($node);
}
if (strcmp($node->_tag, "iq") == 0 AND strcmp($node->_attributeHash['type'], "get") == 0 AND strcmp($node->_children[0]->_tag, "ping") == 0)
{
@@ -184,12 +199,39 @@ protected function processInboundData($data)
}
}
+ public function sendNext()
+ {
+ if(count($this->_outQueue)>0)
+ {
+ $msgnode = array_shift($this->_outQueue);
+ $msgnode->refreshTimes();
+ $this->_lastId = $msgnode->getAttribute('id');
+ $this->sendNode($msgnode);
+ }else
+ $this->_lastId = false;
+ }
+
+ public function sendComposing($msg)
+ {
+ $comphash = array();
+ $comphash['xmlns'] = "http://jabber.org/protocol/chatstates";
+ $compose = new ProtocolNode("composing", $comphash, null, "");
+ $messageHash = array();
+ $messageHash["to"] = $msg->getAttribute("from");
+ $messageHash["type"] = "chat";
+ $messageHash["id"] = time().'-'.$this->_msgCounter;
+ $messageHash["t"] = time();
+ $this->_msgCounter++;
+ $messageNode = new ProtocolNode("message", $messageHash, array($compose), "");
+ $this->sendNode($messageNode);
+ }
+
public function accountInfo(){
if(is_array($this->_accountinfo)){
- print_r($this->_accountinfo);
+ print_r($this->_accountinfo);
}
else{
- echo "No information available";
+ echo "No information available";
}
}
@@ -220,6 +262,8 @@ public function Login()
{
$this->processInboundData($this->readData());
} while (($cnt++ < 100) && (strcmp($this->_loginStatus, $this->_disconnectedStatus) == 0));
+ $this->sendNickname();
+ $this->SendPresence();
}
# Pull from the socket, and place incoming messages in the message queue
@@ -250,13 +294,13 @@ public function WaitforReceipt()
$received = true;
}
}
- print($m->NodeString("") . "\n");
+ //print($m->NodeString("") . "\n");
}
}while(!$received);
- echo "Received node!!\n";
+ //echo "Received node!!\n";
}
- public function SendPrecense($type="available")
+ public function SendPresence($type="available")
{
$presence = array();
$presence['type'] = $type;
@@ -265,34 +309,41 @@ public function SendPrecense($type="available")
$this->sendNode($node);
}
- protected function SendMessageNode($msgid, $to, $node)
+ protected function SendMessageNode($to, $node)
{
$serverNode = new ProtocolNode("server", null, null, "");
$xHash = array();
$xHash["xmlns"] = "jabber:x:event";
$xNode = new ProtocolNode("x", $xHash, array($serverNode), "");
$notify = array();
- $notify['urn:xmpp:whatsapp'];
+ $notify['xmlns'] = 'urn:xmpp:whatsapp';
$notify['name'] = $this->_name;
$notnode = new ProtocolNode("notify", $notify, null, "");
$request = array();
$request['xmlns'] = "urn:xmpp:receipts";
$reqnode = new ProtocolNode("request", $request, null, "");
+ $msgid = time().'-'.$this->_msgCounter;
$messageHash = array();
$messageHash["to"] = $to . "@" . $this->_whatsAppServer;
$messageHash["type"] = "chat";
$messageHash["id"] = $msgid;
+ $messageHash["t"] = time();
+ $this->_msgCounter++;
$messsageNode = new ProtocolNode("message", $messageHash, array($xNode, $notnode,$reqnode,$node), "");
- $this->sendNode($messsageNode);
+ if(!$this->_lastId){
+ $this->_lastId = $msgid;
+ $this->sendNode($messsageNode);
+ }else
+ $this->_outQueue[] = $messsageNode;
}
- public function Message($msgid, $to, $txt)
+ public function Message($to, $txt)
{
$bodyNode = new ProtocolNode("body", null, null, $txt);
- $this->SendMessageNode($msgid, $to, $bodyNode);
+ $this->SendMessageNode($to, $bodyNode);
}
- public function MessageImage($msgid, $to, $url, $file, $size, $icon)
+ public function MessageImage($to, $url, $file, $size, $icon)
{
$mediaAttribs = array();
$mediaAttribs["xmlns"] = "urn:xmpp:whatsapp:mms";
@@ -302,7 +353,7 @@ public function MessageImage($msgid, $to, $url, $file, $size, $icon)
$mediaAttribs["size"] = $size;
$mediaNode = new ProtocolNode("media", $mediaAttribs, null, $icon);
- $this->SendMessageNode($msgid, $to, $mediaNode);
+ $this->SendMessageNode($to, $mediaNode);
}
public function Location($msgid, $to, $long, $lat)
@@ -325,7 +376,22 @@ public function Location($msgid, $to, $long, $lat)
$messsageNode = new ProtocolNode("message", $messageHash, array($mediaNode), "");
$this->sendNode($messsageNode);
}
-
+
+ public function sendStatusUpdate($msgid, $txt)
+ {
+ $bodyNode = new ProtocolNode("body", null, null, $txt);
+ $serverNode = new ProtocolNode("server", null, null, "");
+ $xHash = array();
+ $xHash["xmlns"] = "jabber:x:event";
+ $xNode = new ProtocolNode("x", $xHash, array($serverNode), "");
+ $messageHash = array();
+ $messageHash["to"] = 's.us';
+ $messageHash["type"] = "chat";
+ $messageHash["id"] = $msgid;
+ $messsageNode = new ProtocolNode("message", $messageHash, array($xNode, $bodyNode), "");
+ $this->sendNode($messsageNode);
+ }
+
public function Pong($msgid)
{
$whatsAppServer = $this->_whatsAppServer;
@@ -338,6 +404,14 @@ public function Pong($msgid)
$messsageNode = new ProtocolNode("iq", $messageHash, null, "");
$this->sendNode($messsageNode);
}
+
+ public function sendNickname()
+ {
+ $messageHash = array();
+ $messageHash["name"] = $this->_name;
+ $messsageNode = new ProtocolNode("presence", $messageHash, null, "");
+ $this->sendNode($messsageNode);
+ }
protected function DebugPrint($debugMsg)
{
View
1 tests/Gift.jpgb64
@@ -0,0 +1 @@
+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

0 comments on commit ca4be92

Please sign in to comment.