diff --git a/.gitignore b/.gitignore index 3e0e2af634..c9cdf333ac 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ gen/* *.orig .pydevproject .settings +*~ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..616f0bc0b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ + +all: + android update project --path . --target android-10 --name Xabber + ant release + diff --git a/project.properties b/project.properties index 4d7ea1e41e..ddd0fc4187 100644 --- a/project.properties +++ b/project.properties @@ -10,4 +10,4 @@ # Indicates whether an apk should be generated for each density. split.density=false # Project target. -target=android-9 +target=android-10 diff --git a/res/values/account_editor.xml b/res/values/account_editor.xml index ff4b40cbbe..bf67ed1d0c 100644 --- a/res/values/account_editor.xml +++ b/res/values/account_editor.xml @@ -25,6 +25,8 @@ username for gmail.com or Google Apps domain + Telephone number (with prefix) + username for livejournal.com username for qip.ru @@ -39,6 +41,8 @@ If you don\'t have Google account you may create one at http://mail.google.com\nAlso you can use your_user_name@your_google_domain + You need an already existing WhatsApp account. TODO: Add references on how to create/get password + If you don\'t have Livejournal account you may create one at http://livejournal.com If you don\'t have QIP account you may create one at http://qip.ru @@ -53,6 +57,8 @@ Google Talk + WhatsApp + LiveJournal QIP @@ -101,6 +107,8 @@ Google Talk + WhatsApp + WLM XMPP @@ -159,4 +167,4 @@ Are you sure you want to discard all the changes? Incorrect user name. Check help text below for details. - \ No newline at end of file + diff --git a/res/values/connections.xml b/res/values/connections.xml index 2e88520645..4e4042f6c1 100644 --- a/res/values/connections.xml +++ b/res/values/connections.xml @@ -16,6 +16,7 @@ xmpp gtalk wlm + wapp + + + + + + + + + diff --git a/src/com/xabber/android/data/account/AccountProtocol.java b/src/com/xabber/android/data/account/AccountProtocol.java index caee5ba0d6..661af1d148 100644 --- a/src/com/xabber/android/data/account/AccountProtocol.java +++ b/src/com/xabber/android/data/account/AccountProtocol.java @@ -33,6 +33,11 @@ public enum AccountProtocol { * GTalk. */ gtalk, + + /** + * WhatsApp. + */ + wapp, /** * Windows Live Messenger. @@ -54,6 +59,8 @@ public int getNameResource() { return R.string.account_type_names_xmpp; else if (this == gtalk) return R.string.account_type_names_gtalk; + else if (this == wapp) + return R.string.account_type_names_wapp; else if (this == wlm) return R.string.account_type_names_wlm; else @@ -68,6 +75,8 @@ public int getShortResource() { return R.string.account_protocol_xmpp_title; else if (this == gtalk) return R.string.account_protocol_gtalk_title; + else if (this == wapp) + return R.string.account_protocol_wapp_title; else if (this == wlm) return R.string.account_protocol_wlm_title; else diff --git a/src/com/xabber/android/data/connection/ConnectionItem.java b/src/com/xabber/android/data/connection/ConnectionItem.java index 4fbf4008b1..825b018452 100644 --- a/src/com/xabber/android/data/connection/ConnectionItem.java +++ b/src/com/xabber/android/data/connection/ConnectionItem.java @@ -14,7 +14,7 @@ */ package com.xabber.android.data.connection; -import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.Connection; import com.xabber.android.data.Application; import com.xabber.android.data.LogManager; @@ -99,7 +99,7 @@ public String getRealJid() { ConnectionThread connectionThread = getConnectionThread(); if (connectionThread == null) return null; - XMPPConnection xmppConnection = connectionThread.getXMPPConnection(); + Connection xmppConnection = connectionThread.getXMPPConnection(); if (xmppConnection == null) return null; String user = xmppConnection.getUser(); @@ -188,7 +188,7 @@ protected void disconnect(final ConnectionThread connectionThread) { Thread thread = new Thread("Disconnection thread for " + this) { @Override public void run() { - XMPPConnection xmppConnection = connectionThread + Connection xmppConnection = connectionThread .getXMPPConnection(); if (xmppConnection != null) try { @@ -261,7 +261,7 @@ protected void onAuthorized(ConnectionThread connectionThread) { * @return true if connection thread was managed. */ private boolean onDisconnect(ConnectionThread connectionThread) { - XMPPConnection xmppConnection = connectionThread.getXMPPConnection(); + Connection xmppConnection = connectionThread.getXMPPConnection(); boolean acceptable = isManaged(connectionThread); if (xmppConnection == null) LogManager.i(this, "onClose " + acceptable); diff --git a/src/com/xabber/android/data/connection/ConnectionManager.java b/src/com/xabber/android/data/connection/ConnectionManager.java index 0d42d0179f..f74bf183b8 100644 --- a/src/com/xabber/android/data/connection/ConnectionManager.java +++ b/src/com/xabber/android/data/connection/ConnectionManager.java @@ -25,7 +25,7 @@ import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.SASLAuthentication; import org.jivesoftware.smack.SmackConfiguration; -import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smack.packet.Packet; @@ -181,7 +181,7 @@ public void sendPacket(String account, Packet packet) || !connectionThread.getConnectionItem().getState() .isConnected()) throw new NetworkException(R.string.NOT_CONNECTED); - XMPPConnection xmppConnection = connectionThread.getXMPPConnection(); + Connection xmppConnection = connectionThread.getXMPPConnection(); try { xmppConnection.sendPacket(packet); } catch (IllegalStateException e) { diff --git a/src/com/xabber/android/data/connection/ConnectionThread.java b/src/com/xabber/android/data/connection/ConnectionThread.java index a57ce86d14..6cf16742bf 100644 --- a/src/com/xabber/android/data/connection/ConnectionThread.java +++ b/src/com/xabber/android/data/connection/ConnectionThread.java @@ -22,9 +22,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jivesoftware.smack.WAConnection; + import javax.net.ssl.SSLException; import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.filter.PacketFilter; @@ -67,7 +70,7 @@ public class ConnectionThread implements /** * SMACK connection. */ - private XMPPConnection xmppConnection; + private Connection xmppConnection; /** * Thread holder for this connection. @@ -144,7 +147,7 @@ public Thread newThread(Runnable runnable) { started = false; } - public XMPPConnection getXMPPConnection() { + public Connection getXMPPConnection() { return xmppConnection; } @@ -318,7 +321,13 @@ private void onReady(final InetAddress address, final int port) { connectionConfiguration.setSecurityMode(tlsMode.getSecurityMode()); connectionConfiguration.setCompressionEnabled(compression); - xmppConnection = new XMPPConnection(connectionConfiguration); + // Create different underlying classes depending on the protocol + AccountProtocol proto = connectionItem.getConnectionSettings().getProtocol(); + if (proto == AccountProtocol.wapp) { + xmppConnection = new WAConnection(this, connectionConfiguration); + }else{ + xmppConnection = new XMPPConnection(connectionConfiguration); + } xmppConnection.addPacketListener(this, ACCEPT_ALL); xmppConnection.forceAddConnectionListener(this); connectionItem.onSRVResolved(this); @@ -416,7 +425,8 @@ public void run() { */ private void connect(final String password) { try { - xmppConnection.connect(); + ConnectionSettings connectionSettings = connectionItem.getConnectionSettings(); + xmppConnection.connect(connectionSettings.getUserName(), password, connectionSettings.getResource()); } catch (XMPPException e) { checkForCertificateError(e); if (!checkForSeeOtherHost(e)) { diff --git a/src/com/xabber/android/data/extension/attention/AttentionManager.java b/src/com/xabber/android/data/extension/attention/AttentionManager.java index f995ba0c2e..2721759e36 100644 --- a/src/com/xabber/android/data/extension/attention/AttentionManager.java +++ b/src/com/xabber/android/data/extension/attention/AttentionManager.java @@ -18,7 +18,7 @@ import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.ConnectionCreationListener; -import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.PacketExtension; @@ -112,7 +112,7 @@ public void onSettingsChanged() { .getConnectionThread(); if (connectionThread == null) continue; - XMPPConnection xmppConnection = connectionThread + Connection xmppConnection = connectionThread .getXMPPConnection(); if (xmppConnection == null) continue; diff --git a/src/com/xabber/android/data/extension/muc/MUCManager.java b/src/com/xabber/android/data/extension/muc/MUCManager.java index 83065482fd..617382b23f 100644 --- a/src/com/xabber/android/data/extension/muc/MUCManager.java +++ b/src/com/xabber/android/data/extension/muc/MUCManager.java @@ -19,6 +19,7 @@ import java.util.Collections; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; @@ -129,7 +130,7 @@ private void onLoaded(Collection roomChats, * @param room * @return null if does not exists. */ - private RoomChat getRoomChat(String account, String room) { + public RoomChat getRoomChat(String account, String room) { AbstractChat chat = MessageManager.getInstance().getChat(account, room); if (chat != null && chat instanceof RoomChat) return (RoomChat) chat; @@ -219,19 +220,20 @@ public void run() { * @param password */ public void createRoom(String account, String room, String nickname, - String password, boolean join) { + String password, boolean join, String subject) { removeInvite(getInvite(account, room)); AbstractChat chat = MessageManager.getInstance().getChat(account, room); RoomChat roomChat; if (chat == null || !(chat instanceof RoomChat)) { if (chat != null) MessageManager.getInstance().removeChat(chat); - roomChat = new RoomChat(account, room, nickname, password); + roomChat = new RoomChat(account, room, nickname, password, subject); MessageManager.getInstance().addChat(roomChat); } else { roomChat = (RoomChat) chat; roomChat.setNickname(nickname); roomChat.setPassword(password); + roomChat.setSubject(subject); } requestToWriteRoom(account, room, nickname, password, join); if (join) @@ -279,7 +281,7 @@ public boolean inUse(final String account, final String room) { */ public void joinRoom(final String account, final String room, boolean requested) { - final XMPPConnection xmppConnection; + final Connection xmppConnection; final RoomChat roomChat; final String nickname; final String password; diff --git a/src/com/xabber/android/data/extension/muc/RoomChat.java b/src/com/xabber/android/data/extension/muc/RoomChat.java index 2825ec4cfc..7e3ca4c91d 100644 --- a/src/com/xabber/android/data/extension/muc/RoomChat.java +++ b/src/com/xabber/android/data/extension/muc/RoomChat.java @@ -82,6 +82,18 @@ public class RoomChat extends AbstractChat { * Invited user for the sent packet ID. */ private final Map invites; + + RoomChat(String account, String user, String nickname, String password, String subject) { + super(account, user); + this.nickname = nickname; + this.password = password; + requested = false; + state = RoomState.unavailable; + this.subject = subject; + multiUserChat = null; + occupants = new HashMap(); + invites = new HashMap(); + } RoomChat(String account, String user, String nickname, String password) { super(account, user); @@ -156,6 +168,10 @@ String getSubject() { return subject; } + void setSubject(String s) { + this.subject = s; + } + MultiUserChat getMultiUserChat() { return multiUserChat; } @@ -486,4 +502,4 @@ protected void onDisconnect() { setState(RoomState.waiting); } -} \ No newline at end of file +} diff --git a/src/com/xabber/android/ui/AccountEditor.java b/src/com/xabber/android/ui/AccountEditor.java index 22b73c5314..fe6dc0be86 100644 --- a/src/com/xabber/android/ui/AccountEditor.java +++ b/src/com/xabber/android/ui/AccountEditor.java @@ -75,6 +75,8 @@ protected void onInflate(Bundle savedInstanceState) { addPreferencesFromResource(R.xml.account_editor_xmpp); else if (protocol == AccountProtocol.gtalk) addPreferencesFromResource(R.xml.account_editor_xmpp); + else if (protocol == AccountProtocol.wapp) + addPreferencesFromResource(R.xml.account_editor_wapp); else if (protocol == AccountProtocol.wlm) addPreferencesFromResource(R.xml.account_editor_oauth); else diff --git a/src/com/xabber/android/ui/MUCEditor.java b/src/com/xabber/android/ui/MUCEditor.java index 4c69378319..bb38f4cc62 100644 --- a/src/com/xabber/android/ui/MUCEditor.java +++ b/src/com/xabber/android/ui/MUCEditor.java @@ -206,7 +206,7 @@ public void onClick(View view) { .removeMessageNotification(this.account, this.room); } MUCManager.getInstance() - .createRoom(account, room, nick, password, join); + .createRoom(account, room, nick, password, join, ""); finish(); break; default: diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java new file mode 100644 index 0000000000..3f11f6de4d --- /dev/null +++ b/src/net/davidgf/android/DataBuffer.java @@ -0,0 +1,286 @@ + +/* + * JAVA WhatsApp API implementation + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * Updated to WA protocol v1.4 + * + * Share and enjoy! + * + */ + +package net.davidgf.android; + +import java.util.*; + +public class DataBuffer { + private byte [] buffer; + + public DataBuffer (byte [] buf) { + buffer = new byte[buf.length]; + for (int c = 0; c < buf.length; c++) + buffer[c] = buf[c]; + } + public DataBuffer () { + buffer = new byte[0]; + } + public DataBuffer (DataBuffer other) { + buffer = new byte[other.buffer.length]; + for (int c = 0; c < other.buffer.length; c++) + buffer[c] = other.buffer[c]; + } + + DataBuffer addBuf(DataBuffer other) { + DataBuffer ret = new DataBuffer(); + ret.buffer = new byte[this.buffer.length + other.buffer.length]; + + for (int c = 0; c < this.buffer.length; c++) + ret.buffer[c] = this.buffer[c]; + for (int c = 0; c < other.buffer.length; c++) + ret.buffer[c+this.buffer.length] = other.buffer[c]; + + return ret; + } + DataBuffer decodedBuffer(RC4Decoder decoder, int clength) { + byte [] carray, array4; + carray = decoder.cipher(Arrays.copyOfRange(this.buffer,0,clength-4)); + array4 = Arrays.copyOfRange(this.buffer,clength-4,clength); + DataBuffer deco = new DataBuffer(carray); + DataBuffer extra = new DataBuffer(array4); + return deco.addBuf(extra); + } + DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout, int seq) { + DataBuffer deco = new DataBuffer(Arrays.copyOfRange(this.buffer,0,this.buffer.length)); + + deco.buffer = decoder.cipher(deco.buffer); + byte [] hmacint = KeyGenerator.calc_hmac(deco.buffer,key,seq); + DataBuffer hmac = new DataBuffer(hmacint); + + DataBuffer res; + if (dout) + return deco.addBuf(hmac); + else + return hmac.addBuf(deco); + } + byte [] getPtr() { + return buffer; + } + void addData(byte [] dta) { + byte [] newbuf = new byte[buffer.length + dta.length]; + for (int c = 0; c < buffer.length; c++) + newbuf[c] = buffer[c]; + for (int c = 0; c < dta.length; c++) + newbuf[c+buffer.length] = dta[c]; + + this.buffer = newbuf; + } + void popData(int size) { + if (buffer.length >= size) { + buffer = Arrays.copyOfRange(buffer,size,buffer.length); + } + } + void crunchData(int size) { + if (buffer.length >= size) { + byte [] newbuf = new byte[buffer.length - size]; + for (int c = 0; c < newbuf.length; c++) + newbuf[c] = buffer[c]; + + this.buffer = newbuf; + } + } + int getInt(int nbytes, int offset) { + //if (nbytes > blen) + // throw 0; + int ret = 0; + for (int i = 0; i < nbytes; i++) { + ret <<= 8; + ret |= (int)(buffer[i+offset] & 0xFF); + } + return ret; + } + void putInt(int value, int nbytes) { + //assert(nbytes > 0); + + byte [] out = new byte[nbytes]; + for (int i = 0; i < nbytes; i++) { + out[nbytes-i-1] = (byte)((value>>(i<<3)) & 0xFF); + } + this.addData(out); + } + int readInt(int nbytes) { + //if (nbytes > blen) + // throw 0; + int ret = getInt(nbytes,0); + popData(nbytes); + return ret; + } + int readListSize() { + //if (blen == 0) + // throw 0; + int ret; + if (buffer[0] == (byte)0xf8 || buffer[0] == (byte)0xf3) { + ret = (int)buffer[1]; + popData(2); + } + else if (buffer[0] == (byte)0xf9) { + ret = getInt(2,1); + popData(3); + } + else { + // FIXME throw 0 error + ret = -1; + //printf("Parse error!!\n"); + } + return ret; + } + void writeListSize(int size) { + if (size == 0) { + putInt(0,1); + } + else if (size < 256) { + putInt(0xf8,1); + putInt(size,1); + } + else { + putInt(0xf9,1); + putInt(size,2); + } + } + String readRawString(int size) { + //if (size < 0 or size > blen) + // throw 0; + String s = new String(buffer,0,size); + popData(size); + return s; + } + byte [] readRawByteString(int size) { + byte [] r = Arrays.copyOf(buffer,size); + popData(size); + return r; + } + byte [] readByteString() { + int type = readInt(1); + if (type > 2 && type <= 236) { + String ret = MiscUtil.getDecoded(type); + if (ret == null) + ret = MiscUtil.getDecodedExt(type, readInt(1)); + return ret.getBytes(); + } + else if (type == 252) { + int slen = readInt(1); + return readRawByteString(slen); + } + else if (type == 253) { + int slen = readInt(3); + return readRawByteString(slen); + } + else if (type == 250) { + String u = readString(); + String s = readString(); + + if (u.length() > 0 && s.length() > 0) + return (u + "@" + s).getBytes(); + else if (s.length() > 0) + return s.getBytes(); + } + return new byte [0]; + } + String readString() { + //if (blen == 0) + // throw 0; + int type = readInt(1); + if (type > 2 && type <= 236) { + String ret = MiscUtil.getDecoded(type); + if (ret == null) + ret = MiscUtil.getDecodedExt(type, readInt(1)); + return ret; + } + else if (type == 0) { + return ""; + } + else if (type == 252) { + int slen = readInt(1); + return readRawString(slen); + } + else if (type == 253) { + int slen = readInt(3); + return readRawString(slen); + } + else if (type == 250) { + String u = readString(); + String s = readString(); + + if (u.length() > 0 && s.length() > 0) + return u + "@" + s; + else if (s.length() > 0) + return s; + return ""; + } + return ""; + } + void putRawString(byte [] s) { + if (s.length < 256) { + putInt(0xfc,1); + putInt(s.length,1); + addData(s); + } + else { + putInt(0xfd,1); + putInt(s.length,3); + addData(s); + } + } + void putString(String s) { + int lu = MiscUtil.lookupDecoded(s); + int sub_dict = (lu >> 8); + + if (sub_dict != 0) + putInt(sub_dict + 236 - 1, 1); // Put dict byte first! + + if (lu != 0) { + putInt(lu,1); + } + else if (s.indexOf('@') >= 0) { + String p1 = s.substring(0,s.indexOf('@')); + String p2 = s.substring(s.indexOf('@')+1); + putInt(0xfa,1); + putString(p1); + putString(p2); + } + else if (s.length() < 256) { + putInt(0xfc,1); + putInt(s.length(),1); + addData(s.getBytes()); + } + else { + putInt(0xfd,1); + putInt(s.length(),3); + addData(s.getBytes()); + } + } + boolean isList() { + //if (blen == 0) + // throw 0; + return (buffer[0] == (byte)248 || buffer[0] == (byte)0 || buffer[0] == (byte)249); + } + Vector readList(WhatsappConnection c) { + Vector l = new Vector(); + int size = readListSize(); + while (size-- > 0) { + l.add(c.read_tree(this)); + } + return l; + } + int size() { + return buffer.length; + } + byte [] ToString() { + byte [] bb = new byte[buffer.length]; + for (int c = 0; c < buffer.length; c++) + bb[c] = buffer[c]; + return bb; + } +}; + + diff --git a/src/net/davidgf/android/KeyGenerator.java b/src/net/davidgf/android/KeyGenerator.java new file mode 100644 index 0000000000..37b0a136fc --- /dev/null +++ b/src/net/davidgf/android/KeyGenerator.java @@ -0,0 +1,68 @@ + +package net.davidgf.android; + +import java.security.*; +import java.util.*; +import javax.crypto.*; +import javax.crypto.spec.*; + +public class KeyGenerator { + + public static byte [] PKCS5_PBKDF2_HMAC_SHA1(byte [] pass, byte [] salt) throws Exception { + char [] password = new char[pass.length]; + for (int i = 0; i < pass.length; i++) + password[i] = (char)(pass[i]&0xFF); + + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + PBEKeySpec ks = new PBEKeySpec(password,salt,2,20*8); // 2 rounds, 20*8 bits output + SecretKey s = f.generateSecret(ks); + + return s.getEncoded(); + } + + private static byte [] hexmap = new byte[]{ '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; + + public static byte[][] generateKeyV14(String pw, byte [] salt) { + try { + byte [] decpass = MiscUtil.base64_decode(pw.getBytes()); + + byte [][] keys = new byte[4][]; + for (int i = 0; i < 4; i++) { + byte salt2 [] = Arrays.copyOf(salt,salt.length + 1); + salt2[salt.length] = (byte)(i+1); + keys[i] = PKCS5_PBKDF2_HMAC_SHA1 (decpass,salt2); + } + + return keys; + } + catch (Exception e) { + return new byte[0][0]; + } + } + public static byte [] calc_hmac(byte [] data, byte [] key, int seq) { + byte [] dataext = Arrays.copyOf(data, data.length + 4); + dataext[data.length +0] = (byte)(seq >> 24); + dataext[data.length +1] = (byte)(seq >> 16); + dataext[data.length +2] = (byte)(seq >> 8); + dataext[data.length +3] = (byte)(seq ); + + byte [] hash = HMAC_SHA1 (dataext,key); + + return Arrays.copyOf(hash,4); + } + + private static byte [] HMAC_SHA1(byte [] text, byte [] key) { + try { + SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA1"); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(signingKey); + // Compute the hmac on input data bytes + return mac.doFinal(text); + } catch (Exception e) { + return new byte[0]; + } + } + +}; + + diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java new file mode 100644 index 0000000000..ab808a4659 --- /dev/null +++ b/src/net/davidgf/android/MiscUtil.java @@ -0,0 +1,232 @@ + +package net.davidgf.android; + +import java.security.MessageDigest; +import java.math.BigInteger; +import java.util.*; + +public class MiscUtil { + + private static byte [] base64_chars = new byte []{'A','B','C','D','E','F','G','H','I','J','K','L', + 'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k', + 'l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'}; + + private static boolean is_base64(byte cc) { + for (int c = 0; c < base64_chars.length; c++) + if (base64_chars[c] == cc) + return true; + return false; + } + + private static byte findarray(byte what) { + for (int c = 0; c < base64_chars.length; c++) + if (base64_chars[c] == what) + return (byte)c; + return 0; + } + + private static byte [] concat(byte [] array, byte c) { + byte [] bb = new byte[array.length+1]; + for (int i = 0; i < array.length; i++) + bb[i] = array[i]; + + bb[array.length] = c; + + return bb; + } + + public static byte [] concat(byte [] array, byte [] array2) { + byte [] ret = Arrays.copyOf(array, array.length + array2.length); + System.arraycopy(array2,0, ret,ret.length, array2.length); + return ret; + } + + public static byte [] base64_decode(byte [] encoded_string) { + int in_len = encoded_string.length; + int i = 0; + int j = 0; + int in_ = 0; + byte [] char_array_4 = new byte [4]; + byte [] char_array_3 = new byte [3]; + byte [] ret = new byte[0]; + + while (in_len-- > 0 && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i ==4) { + for (i = 0; i <4; i++) + char_array_4[i] = findarray(char_array_4[i]); + + char_array_3[0] = (byte)((char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >>> 4)); + char_array_3[1] = (byte)(((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >>> 2)); + char_array_3[2] = (byte)(((char_array_4[2] & 0x3) << 6) + char_array_4[3]); + + for (i = 0; (i < 3); i++) + ret = concat(ret,char_array_3[i]); + i = 0; + } + } + + if (i != 0) { + for (j = i; j <4; j++) + char_array_4[j] = 0; + + for (j = 0; j <4; j++) + char_array_4[j] = findarray(char_array_4[j]); + + char_array_3[0] = (byte)((char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >>> 4)); + char_array_3[1] = (byte)(((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >>> 2)); + char_array_3[2] = (byte)(((char_array_4[2] & 0x3) << 6) + char_array_4[3]); + + for (j = 0; (j < i - 1); j++) + ret = concat(ret,char_array_3[j]); + } + + return ret; + } + + + private static String [] dictionary = new String[] { + "", "", "", "account", "ack", "action", "active", "add", "after", "all", "allow", "apple", + "auth", "author", "available", "bad-protocol", "bad-request", "before", "body", "broadcast", + "cancel", "category", "challenge", "chat", "clean", "code", "composing", "config", "contacts", + "count", "create", "creation", "debug", "default", "delete", "delivery", "delta", "deny", + "digest", "dirty", "duplicate", "elapsed", "enable", "encoding", "error", "event", + "expiration", "expired", "fail", "failure", "false", "favorites", "feature", "features", + "feature-not-implemented", "field", "first", "free", "from", "g.us", "get", "google", "group", + "groups", "http://etherx.jabber.org/streams", "http://jabber.org/protocol/chatstates", "ib", + "id", "image", "img", "index", "internal-server-error", "ip", "iq", "item-not-found", "item", + "jabber:iq:last", "jabber:iq:privacy", "jabber:x:event", "jid", "kind", "last", "leave", + "list", "max", "mechanism", "media", "message_acks", "message", "method", "microsoft", + "missing", "modify", "mute", "name", "nokia", "none", "not-acceptable", "not-allowed", + "not-authorized", "notification", "notify", "off", "offline", "order", "owner", "owning", + "p_o", "p_t", "paid", "participant", "participants", "participating", "paused", "picture", + "pin", "ping", "platform", "port", "presence", "preview", "probe", "prop", "props", "query", + "raw", "read", "reason", "receipt", "received", "relay", "remote-server-timeout", "remove", + "request", "required", "resource-constraint", "resource", "response", "result", "retry", + "rim", "s_o", "s_t", "s.us", "s.whatsapp.net", "seconds", "server-error", "server", + "service-unavailable", "set", "show", "silent", "stat", "status", "stream:error", + "stream:features", "subject", "subscribe", "success", "sync", "t", "text", "timeout", + "timestamp", "to", "true", "type", "unavailable", "unsubscribe", "uri", "url", + "urn:ietf:params:xml:ns:xmpp-sasl", "urn:ietf:params:xml:ns:xmpp-stanzas", + "urn:ietf:params:xml:ns:xmpp-streams", "urn:xmpp:ping", "urn:xmpp:receipts", + "urn:xmpp:whatsapp:account", "urn:xmpp:whatsapp:dirty", "urn:xmpp:whatsapp:mms", + "urn:xmpp:whatsapp:push", "urn:xmpp:whatsapp", "user", "user-not-found", "value", + "version", "w:g", "w:p:r", "w:p", "w:profile:picture", "w", "wait", "WAUTH-2", + "x", "xmlns:stream", "xmlns", "1", "chatstate", "crypto", "enc", "class", "off_cnt", + "w:g2", "promote", "demote", "creator" + }; + + private static String [] dictionary2 = new String[] { + "Bell.caf", "Boing.caf", "Glass.caf", "Harp.caf", "TimePassing.caf", "Tri-tone.caf", + "Xylophone.caf", "background", "backoff", "chunked", "context", "full", "in", "interactive", + "out", "registration", "sid", "urn:xmpp:whatsapp:sync", "flt", "s16", "u8", "adpcm", + "amrnb", "amrwb", "mp3", "pcm", "qcelp", "wma", "h263", "h264", "jpeg", "mpeg4", "wmv", + "audio/3gpp", "audio/aac", "audio/amr", "audio/mp4", "audio/mpeg", "audio/ogg", "audio/qcelp", + "audio/wav", "audio/webm", "audio/x-caf", "audio/x-ms-wma", "image/gif", "image/jpeg", + "image/png", "video/3gpp", "video/avi", "video/mp4", "video/mpeg", "video/quicktime", + "video/x-flv", "video/x-ms-asf", "302", "400", "401", "402", "403", "404", "405", "406", + "407", "409", "500", "501", "503", "504", "abitrate", "acodec", "app_uptime", "asampfmt", + "asampfreq", "audio", "bb_db", "clear", "conflict", "conn_no_nna", "cost", "currency", + "duration", "extend", "file", "fps", "g_notify", "g_sound", "gcm", "google_play", "hash", + "height", "invalid", "jid-malformed", "latitude", "lc", "lg", "live", "location", "log", + "longitude", "max_groups", "max_participants", "max_subject", "mimetype", "mode", + "napi_version", "normalize", "orighash", "origin", "passive", "password", "played", + "policy-violation", "pop_mean_time", "pop_plus_minus", "price", "pricing", "redeem", + "Replaced by new connection", "resume", "signature", "size", "sound", "source", + "system-shutdown", "username", "vbitrate", "vcard", "vcodec", "video", "width", + "xml-not-well-formed", "checkmarks", "image_max_edge", "image_max_kbytes", "image_quality", + "ka", "ka_grow", "ka_shrink", "newmedia", "library", "caption", "forward", "c0", "c1", "c2", + "c3", "clock_skew", "cts", "k0", "k1", "login_rtt", "m_id", "nna_msg_rtt", "nna_no_off_count", + "nna_offline_ratio", "nna_push_rtt", "no_nna_con_count", "off_msg_rtt", "on_msg_rtt", + "stat_name", "sts", "suspect_conn", "lists", "self", "qr", "web", "w:b", "recipient", + "w:stats", "forbidden", "aurora.m4r", "bamboo.m4r", "chord.m4r", "circles.m4r", "complete.m4r", + "hello.m4r", "input.m4r", "keys.m4r", "note.m4r", "popcorn.m4r", "pulse.m4r", "synth.m4r", + "filehash" + }; + + + public static String getDecoded(int n) { + if (n < 236) + return new String(dictionary[n & 255]); + return null; + } + public static String getDecodedExt(int n, int n2) { + if (n == 236) + return new String(dictionary2[n2 & 255]); + return ""; + } + public static int lookupDecoded(String value) { + for (int i = 0; i < dictionary.length; i++) { + if (dictionary[i].equals(value)) + return i; + } + for (int i = 0; i < dictionary2.length; i++) { + if (dictionary2[i].equals(value)) + return i | 0x100; + } + return 0; + } + + public static String bytesToUTF8(byte [] ba) { + try { + return new String(ba, "UTF-8"); + } + catch (Exception e) { + return new String(); + } + } + + public static byte [] UTF8ToBytes(String st) { + try { + return st.getBytes("UTF-8"); + } + catch (Exception e) { + return new byte [0]; + } + } + + public static String getUser(String user) { + return user.split("@")[0]; + } + + public static String getUserAndResource(String user) { + String [] re = user.split("/"); + return user.split("@")[0] + "/" + re[re.length-1]; + } + + public static String getEncodedSha1Sum( byte [] data ) { + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + md.update(data); + return new BigInteger(1, md.digest()).toString(16); + } + catch (Exception e) { + return new String(""); + } + } + + public static byte [] md5raw( byte [] data ) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(data); + return md.digest(); + } + catch (Exception e) { + return new byte[0]; + } + } + public static String md5hex( byte [] data ) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(data); + return new BigInteger(1, md.digest()).toString(16); + } + catch (Exception e) { + return new String(""); + } + } + + +} + + diff --git a/src/net/davidgf/android/RC4Decoder.java b/src/net/davidgf/android/RC4Decoder.java new file mode 100644 index 0000000000..6df280f213 --- /dev/null +++ b/src/net/davidgf/android/RC4Decoder.java @@ -0,0 +1,53 @@ + +/* + * Java WhatsApp API implementation + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + +package net.davidgf.android; + +public class RC4Decoder { + public int [] s; + public int i,j; + public void swap (int i, int j) { + int t = s[i]; + s[i] = s[j]; + s[j] = t; + } + public RC4Decoder(byte [] key, int drop) { + s = new int[256]; + for (int k = 0; k < 256; k++) s[k] = k; + i = j = 0; + do { + int k = key[i % key.length] & 0xFF; + j = (j + k + s[i]) & 0xFF; + swap(i,j); + i = (i+1) & 0xFF; + } while (i != 0); + i = j = 0; + + byte [] temp = new byte[drop]; + for (int k = 0; k < drop; k++) + temp[k] = (byte)0; + cipher(temp); + } + + public byte [] cipher (byte [] data) { + byte [] out = new byte[data.length]; + for (int c = 0; c < data.length; c++) { + i = (i+1) & 0xFF; + j = (j + s[i]) & 0xFF; + swap(i,j); + int idx = (s[i]+s[j]) & 0xFF; + out[c] = (byte)((data[c] ^ s[idx]) & 0xFF); + } + return out; + } +}; + + diff --git a/src/net/davidgf/android/Tree.java b/src/net/davidgf/android/Tree.java new file mode 100644 index 0000000000..faddcb179e --- /dev/null +++ b/src/net/davidgf/android/Tree.java @@ -0,0 +1,144 @@ + +/* + * Java WhatsApp API implementation. + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + +package net.davidgf.android; + +import java.util.*; + +public class Tree { + private Map < String, String > attributes; + private Vector < Tree > children; + private String tag; + private byte [] data; + private boolean forcedata; + + public Tree(String tag) { + this.tag = tag; + this.forcedata=false; + this.data = new byte[0]; + children = new Vector < Tree > (); + attributes = new HashMap < String, String > (); + } + public Tree(String tag, Map < String,String > attributes) { + this.tag = tag; + this.attributes = attributes; + this.forcedata = false; + this.data = new byte[0]; + children = new Vector < Tree > (); + } + public void forceDataWrite() { + forcedata=true; + } + public boolean forcedData() { + return forcedata; + } + public void addChild(Tree t) { + children.add(t); + } + public void setTag(String tag) { + this.tag = tag; + } + public void setAttributes(Map < String, String > attributes) { + this.attributes = attributes; + } + public void readAttributes(DataBuffer data, int size) { + int count = (size - 2 + (size % 2)) / 2; + while (count-- > 0) { + String key = data.readString(); + String value = data.readString(); + attributes.put(key,value); + } + } + public void writeAttributes(DataBuffer data) { + for (String key: attributes.keySet()) { + data.putString(key); + data.putString(attributes.get(key)); + } + } + public void setData(byte [] d) { + data = new byte[d.length]; + for (int c = 0; c < d.length; c++) + data[c] = d[c]; + } + public byte [] getData() { + byte [] b = new byte[data.length]; + for (int c = 0; c < data.length; c++) + b[c] = data[c]; + return b; + } + public String getTag() { + return tag; + } + public void setChildren(Vector < Tree > c) { + children = c; + } + public Vector < Tree > getChildren() { + return children; + } + public Map < String, String > getAttributes() { + return attributes; + } + public boolean hasAttributeValue(String at, String val) { + if (hasAttribute(at)) { + return (attributes.get(at).equals(val)); + } + return false; + } + public boolean hasAttribute(String at) { + return (attributes.containsKey(at)); + } + public String getAttribute(String at) { + if (attributes.containsKey(at)) + return (attributes.get(at)); + return ""; + } + + public Tree getChild(String tag) { + for (int i = 0; i < children.size(); i++) { + if (children.get(i).getTag().equals(tag)) + return children.get(i); + Tree t = children.get(i).getChild(tag); + if (t != null) + return t; + } + return null; + } + public boolean hasChild(String tag) { + for (int i = 0; i < children.size(); i++) { + if (children.get(i).getTag().equals(tag)) + return true; + if (children.get(i).hasChild(tag)) + return true; + } + return false; + } + + String toString(int sp) { + String ret = ""; + String spacing = ""; + for (int i = 0; i < sp*3; i++) + spacing += " "; + ret += spacing+"Tag: "+tag+"\n"; + for (String key: attributes.keySet()) { + ret += spacing+"at["+key+"]="+attributes.get(key)+"\n"; + } + ret += spacing+"Data: "+MiscUtil.bytesToUTF8(data)+"\n"; + + for (int i = 0; i < children.size(); i++) { + ret += children.get(i).toString(sp+1); + } + return ret; + } +}; + + + + diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java new file mode 100644 index 0000000000..71fbd1ceee --- /dev/null +++ b/src/net/davidgf/android/WAConnection.java @@ -0,0 +1,801 @@ + +/* + * WhatsApp API extension for smack-xabber + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + +package org.jivesoftware.smack; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Registration; +import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.IQ; +import com.xabber.xmpp.vcard.VCard; +import com.xabber.xmpp.vcard.VCardProperty; +import com.xabber.xmpp.vcard.BinaryPhoto; +import com.xabber.android.data.connection.ConnectionThread; +import com.xabber.android.data.account.AccountItem; +import com.xabber.android.data.connection.ConnectionItem; + +import net.davidgf.android.WhatsappConnection; +import net.davidgf.android.WAContacts; + +import org.apache.harmony.javax.security.auth.callback.Callback; +import org.apache.harmony.javax.security.auth.callback.CallbackHandler; +import org.apache.harmony.javax.security.auth.callback.PasswordCallback; +import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; +import java.util.*; +import java.util.concurrent.*; + +/** + * Creates a socket connection to a WA server. + * + * @see Connection + * @author David Guillen Fandos + */ +public class WAConnection extends Connection { + + private ConnectionThread cthread; + /** + * The socket which is used for this connection. + */ + protected Socket socket; + + String connectionID = null; + private String user = null; + private boolean connected = false; + private boolean waconnected = false; + private String account_name = null; + /** + * Flag that indicates if the user is currently authenticated with the server. + */ + private boolean authenticated = false; + + private OutputStream ostream; + private InputStream istream; + + private Thread writerThread, readerThread; + + byte [] inbuffer; + byte [] outbuffer; + byte [] outbuffer_mutex; + WhatsappConnection waconnection; + Semaphore readwait,writewait; + + private ExecutorService listenerExecutor; + int msgid; + + private static final String waUA = "Android-2.11.151-443"; + + private String nickname = ""; + Roster roster = null; + + /** + * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be + * performed to determine the IP address and port corresponding to the + * service name; if that lookup fails, it's assumed that server resides at + * serviceName with the default port of 443. + * This is the simplest constructor for connecting to an WA server. Alternatively, + * you can get fine-grained control over connection settings using the + * {@link #WAConnection(ConnectionConfiguration)} constructor.

+ *

+ * Note that WAConnection constructors do not establish a connection to the server + * and you must call {@link #connect()}.

+ *

+ * The CallbackHandler is ignored. + * + * @param serviceName the name of the WA server to connect to; e.g. example.com. + * @param callbackHandler ignored and should be set to null + */ + public WAConnection(ConnectionThread ct, String serviceName, CallbackHandler callbackHandler) { + // Create the configuration for this new connection + super(new ConnectionConfiguration(serviceName)); + config.setCompressionEnabled(false); + config.setSASLAuthenticationEnabled(true); + config.setDebuggerEnabled(DEBUG_ENABLED); + this.cthread = ct; + commonInit(); + } + + /** + * Creates a new WA connection in the same way {@link #WAConnection(String,CallbackHandler)} does, but + * with no callback handler for password prompting of the keystore. + * + * @param serviceName the name of the WA server to connect to; e.g. example.com. + */ + public WAConnection(ConnectionThread ct, String serviceName) { + // Create the configuration for this new connection + super(new ConnectionConfiguration(serviceName)); + config.setCompressionEnabled(false); + config.setSASLAuthenticationEnabled(true); + config.setDebuggerEnabled(DEBUG_ENABLED); + this.cthread = ct; + commonInit(); + } + + /** + * Creates a new WA connection in the same way {@link #WAConnection(ConnectionConfiguration,CallbackHandler)} does, but + * with no callback handler for password prompting of the keystore. + * + * @param config the connection configuration. + */ + public WAConnection(ConnectionThread ct, ConnectionConfiguration config) { + super(config); + this.cthread = ct; + commonInit(); + } + + public WAConnection(ConnectionThread ct, ConnectionConfiguration config, CallbackHandler callbackHandler) { + super(config); + config.setCallbackHandler(callbackHandler); + this.cthread = ct; + commonInit(); + } + + private void commonInit() { + outbuffer_mutex = new byte[1]; + readwait = new Semaphore(0); + writewait = new Semaphore(0); + + this.listenerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, + "WA Listener Processor"); + thread.setDaemon(true); + return thread; + } + }); + + ConnectionItem citem = cthread.getConnectionItem(); + AccountItem aitem = ((AccountItem)citem); + account_name = aitem.getAccount(); + + nickname = aitem.getConnectionSettings().getResource(); + } + + public String getConnectionID() { + if (!isConnected()) { + return null; + } + return connectionID; + } + + public String getUser() { + if (!isAuthenticated()) { + return null; + } + return user; + } + + @Override + public synchronized void login(String username, String password, String resource) throws XMPPException { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + if (authenticated) { + throw new IllegalStateException("Already logged in to server."); + } + // Do partial version of nameprep on the username. + username = username.toLowerCase().trim(); + + this.user = username + "@" + getServiceName(); + this.user += "/" + resource; + + // Indicate that we're now authenticated. + authenticated = true; + + if (config.isRosterLoadedAtLogin()) { + // Create the roster if it is not a reconnection or roster already + // created by getRoster() + if (this.roster == null) { + if (rosterStorage == null) { + this.roster = new Roster(this); + } else { + this.roster = new Roster(this, rosterStorage); + } + } + this.roster.reload(); + } + + // If debugging is enabled, change the the debug window title to include the + // name we are now logged-in as. + // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger + // will be null + if (config.isDebuggerEnabled() && debugger != null) { + debugger.userHasLogged(user); + } + + // Start login! + synchronized (waconnection) { + try { + waconnection.doLogin(waUA); + } + catch (Exception e) { + e.printStackTrace(); + } + } + popWriteData(); + } + + @Override + public synchronized void loginAnonymously() throws XMPPException { + // No anonymous login supported by WA + } + + public Roster getRoster() { + // synchronize against login() + synchronized(this) { + // if connection is authenticated the roster is already set by login() + // or a previous call to getRoster() + if (!isAuthenticated()) { + if (roster == null) { + roster = new Roster(this); + } + return roster; + } + } + + if (!config.isRosterLoadedAtLogin()) { + if (roster == null) { + roster = new Roster(this); + } + roster.reload(); + } + // If this is the first time the user has asked for the roster after calling + // login, we want to wait for the server to send back the user's roster. This + // behavior shields API users from having to worry about the fact that roster + // operations are asynchronous, although they'll still have to listen for + // changes to the roster. Note: because of this waiting logic, internal + // Smack code should be wary about calling the getRoster method, and may need to + // access the roster object directly. + if (!roster.rosterInitialized) { + try { + synchronized (roster) { + long waitTime = SmackConfiguration.getPacketReplyTimeout(); + long start = System.currentTimeMillis(); + while (!roster.rosterInitialized) { + if (waitTime <= 0) { + break; + } + roster.wait(waitTime); + long now = System.currentTimeMillis(); + waitTime -= now - start; + start = now; + } + } + } + catch (InterruptedException ie) { + // Ignore. + } + } + return roster; + } + + /** + * Returns roster immediately without waiting for initialization. + * + * @return the user's roster, or null if the user has not logged in + * or has not received roster yet. + */ + public Roster getRosterImmediately() { + return roster; + } + + public boolean isConnected() { + return connected; + } + + /** + * + * Returns true if the connection to the server is using legacy SSL or has successfully + * negotiated TLS. Once TLS has been negotiatied the connection has been secured. @see #isUsingTLS. @see #isUsingSSL. + * + * @return true if a secure connection to the server. + */ + public boolean isSecureConnection() { + return false; + } + + public boolean isAuthenticated() { + return authenticated; + } + + public boolean isAnonymous() { + return false; + } + + /** + * Closes the connection by setting presence to unavailable then closing the stream to + * the XMPP server. The shutdown logic will be used during a planned disconnection or when + * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's + * packet reader, packet writer, and {@link Roster} will not be removed; thus + * connection's state is kept. + * + * @param unavailablePresence the presence packet to send during shutdown. + */ + protected void shutdown(Presence unavailablePresence) { + // Close connection + istream = null; + ostream = null; + writerThread = null; + readerThread = null; + outbuffer = null; + inbuffer = null; + + // Set status + authenticated = false; + connected = false; + waconnected = false; + + // Socket close + try { + if (socket != null) + socket.close(); + } + catch (Exception e) { + // Ignore. + } + socket = null; + } + + public void disconnect(Presence unavailablePresence) { + shutdown(unavailablePresence); + + if (roster != null) { + roster.cleanup(); + roster = null; + } + } + + public void sendPacket(Packet packet) { + this.firePacketInterceptors(packet); + + // If the packet if a Message, serialize and send it! + if (packet instanceof Message) { + Message m = (Message)packet; + synchronized (outbuffer_mutex) { + msgid++; + byte [] msg = waconnection.serializeMessage(m.getTo(), m.getBody(null), msgid); + + // Put data in the output buffer + outbuffer = Arrays.copyOf(outbuffer, outbuffer.length + msg.length); + System.arraycopy(msg,0, outbuffer,outbuffer.length-msg.length, msg.length); + writewait.release(); + } + } + if (packet instanceof Registration) { + Registration r = (Registration)packet; + System.out.println(r.getChildElementXML()); + } + if (packet instanceof VCard) { + // The request for a VCard has been queued, now we should provide the Avatar! + packet.setFrom(packet.getTo()); + VCard vc = (VCard)packet; + + // Last seen at notes + vc.getProperties().put(VCardProperty.NOTE,waconnection.getNotes(packet.getFrom())); + + BinaryPhoto bp = new BinaryPhoto(); + bp.setData(waconnection.getUserAvatar(packet.getFrom())); + vc.getPhotos().add(bp); + submitPacket(packet); + } + if (packet instanceof Presence) { + // Sending our presence :) + String status = "unavailable"; + Presence pres = (Presence)packet; + if (pres.getMode() == Presence.Mode.chat || pres.getMode() == Presence.Mode.available) + status = "available"; + + waconnection.setMyPresence(status, pres.getStatus()); + } + if (packet instanceof RosterPacket) { + RosterPacket r = (RosterPacket)packet; + // Check Add/Remove Contact/Group: + if (r.getType() == IQ.Type.SET) { + Collection items = r.getRosterItems(); + if (items.size() == 1) { + RosterPacket.Item it = ((RosterPacket.Item)(items.toArray()[0])); + if (it.getItemType() == RosterPacket.ItemType.remove) { + // Bypasss roster send/rcv for WA, as we do not store them in the server + submitPacket(r); + // Remove fromm storage + WAContacts.getInstance().removeContact(config.getUsername(), it.getUser()); + }else{ + // Adding contact, notify underlying connection for status query + waconnection.addContact(it.getUser(),true); + // Bypasss roster send/rcv for WA, as we do not store them in the server + submitPacket(r); + // Add the contact to the storage + WAContacts.getInstance().addContact(config.getUsername(), it.getUser(), it.getName()); + } + } + } + // Query contacts! + if (r.getType() == IQ.Type.GET) { + RosterPacket rr = new RosterPacket(); + rr.setType(IQ.Type.SET); + + // Add saved contacts! + Vector < Vector < String > > saved_contacts = WAContacts.getInstance().getContacts(config.getUsername()); + for (int i = 0; i < saved_contacts.size(); i++) { + String user = saved_contacts.get(i).get(0); + String name = saved_contacts.get(i).get(1); + rr.addRosterItem(new RosterPacket.Item(user, name)); + // Subscribe presence + waconnection.addContact(user,true); + } + + submitPacket(rr); + waconnection.pushGroupUpdate(); + } + + System.out.println(r.toXML()); + } + + // Notify others + this.firePacketSendingListeners(packet); + + // DO stuff again + processLoop(); + } + + private void popWriteData() { + // Fill outbuffer with fresh data ready to be written + byte [] data; + synchronized (waconnection) { + data = waconnection.getWriteData(); + } + synchronized (outbuffer_mutex) { + outbuffer = Arrays.copyOf(outbuffer, outbuffer.length + data.length); + System.arraycopy(data,0, outbuffer,outbuffer.length-data.length, data.length); + writewait.release(); + } + } + + /** + * Registers a packet interceptor with this connection. The interceptor will be + * invoked every time a packet is about to be sent by this connection. Interceptors + * may modify the packet to be sent. A packet filter determines which packets + * will be delivered to the interceptor. + * + * @param packetInterceptor the packet interceptor to notify of packets about to be sent. + * @param packetFilter the packet filter to use. + * @deprecated replaced by {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}. + */ + public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor, + PacketFilter packetFilter) { + addPacketInterceptor(packetInterceptor, packetFilter); + } + + /** + * Removes a packet interceptor. + * + * @param packetInterceptor the packet interceptor to remove. + * @deprecated replaced by {@link Connection#removePacketInterceptor(PacketInterceptor)}. + */ + public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) { + removePacketInterceptor(packetInterceptor); + } + + /** + * Registers a packet listener with this connection. The listener will be + * notified of every packet that this connection sends. A packet filter determines + * which packets will be delivered to the listener. Note that the thread + * that writes packets will be used to invoke the listeners. Therefore, each + * packet listener should complete all operations quickly or use a different + * thread for processing. + * + * @param packetListener the packet listener to notify of sent packets. + * @param packetFilter the packet filter to use. + * @deprecated replaced by {@link #addPacketSendingListener(PacketListener, PacketFilter)}. + */ + public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) { + addPacketSendingListener(packetListener, packetFilter); + } + + /** + * Removes a packet listener for sending packets from this connection. + * + * @param packetListener the packet listener to remove. + * @deprecated replaced by {@link #removePacketSendingListener(PacketListener)}. + */ + public void removePacketWriterListener(PacketListener packetListener) { + removePacketSendingListener(packetListener); + } + + private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException { + String host = config.getHost(); + int port = config.getPort(); + + // Create WA connection API object + // FIXME: Set proper nickname + msgid = 0; + waconnection = new WhatsappConnection(config.getUsername(), config.getPassword(), this.nickname, account_name); + + try { + if (config.getSocketFactory() == null) { + this.socket = new Socket(host, port); + } + else { + this.socket = config.getSocketFactory().createSocket(host, port); + } + } + catch (UnknownHostException uhe) { + String errorMessage = "Could not connect to " + host + ":" + port + "."; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.remote_server_timeout, errorMessage), + uhe); + } + catch (IOException ioe) { + String errorMessage = "XMPPError connecting to " + host + ":" + + port + "."; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.remote_server_error, errorMessage), ioe); + } + System.out.println("Connected to " + host + " " + String.valueOf(port)); + initConnection(); + connected = true; + } + + /** + * Initializes the connection by creating a packet reader and writer and opening a + * XMPP stream to the server. + * + * @throws XMPPException if establishing a connection to the server fails. + */ + private void initConnection() throws XMPPException { + // Set the stream reader/writer + initReaderAndWriter(); + outbuffer = new byte[0]; + inbuffer = new byte[0]; + + // Spawn reader and writer threads + writerThread = new Thread() { + public void run() { + writePackets(this,ostream); + } + }; + writerThread.setName("Socket data writer"); + writerThread.setDaemon(true); + writerThread.start(); + + readerThread = new Thread() { + public void run() { + readPackets(this,istream); + } + }; + readerThread.setName("Socket data reader"); + readerThread.setDaemon(true); + readerThread.start(); + + // Make note of the fact that we're now connected. + connected = true; + + for (ConnectionCreationListener listener : getConnectionCreationListeners()) { + listener.connectionCreated(this); + } + } + + private void initReaderAndWriter() throws XMPPException { + try { + istream = socket.getInputStream(); + ostream = socket.getOutputStream(); + } + catch (IOException ioe) { + throw new XMPPException( + "XMPPError establishing connection with server.", + new XMPPError(XMPPError.Condition.remote_server_error, + "XMPPError establishing connection with server."), + ioe); + } + + //reader = new AliveReader(reader); + } + + public boolean isUsingCompression() { + return false; + } + + private void writePackets(Thread thisThread, OutputStream ostream) { + try { + while (outbuffer != null && ostream == this.ostream) { + if (outbuffer.length > 0) { + // Try to write the whole buffer + System.out.println("Writing packets...\n"); + byte [] t; + synchronized (outbuffer_mutex) { + t = Arrays.copyOf(outbuffer,outbuffer.length); + } + ostream.write(t,0,t.length); + synchronized (outbuffer_mutex) { + // Pop the written data (the outbuffer may grow while writing t + outbuffer = Arrays.copyOfRange(outbuffer,t.length,outbuffer.length); + } + } + System.out.println("Sleeping while no packets are availbles...\n"); + writewait.acquire(); + } + }catch (Exception e) { + System.out.println("Error!\n" + e.toString()); + } + System.out.println("Exiting writepackets thread (WA)\n"); + } + + private void submitPacket(Packet p) { + System.out.println("Received message!\n"); + for (PacketCollector collector: getPacketCollectors()) { + collector.processPacket(p); + } + listenerExecutor.submit(new ListenerNotification(p)); + } + + private void readPackets(Thread thisThread, InputStream istream) { + try { + int r; + do { + System.out.println("Reading packets...\n"); + byte [] buf = new byte[1024]; + r = istream.read(buf,0,buf.length); + if (r > 0) { + synchronized (inbuffer) { + // Extend the array size and add the new bytes + inbuffer = Arrays.copyOf(inbuffer, inbuffer.length + r); + System.arraycopy(buf,0, inbuffer,inbuffer.length-r, r); + } + } + + processLoop(); + } while (r >= 0); + }catch (IOException e) { + System.out.println("Error!\n" + e.toString()); + } + System.out.println("Exiting readpackets thread (WA)\n"); + disconnect(new Presence(Presence.Type.unavailable)); + cthread.connectionClosed(); + // Signal the writer thread so it can also end + writewait.release(); + } + + private void processLoop() { + // Proceed to push data to underlying connection class + if (inbuffer == null) return; + + synchronized ( waconnection ) { + synchronized (inbuffer) { + int used = waconnection.pushIncomingData(inbuffer); + inbuffer = Arrays.copyOfRange(inbuffer, used, inbuffer.length); + } + } + + // Process stuff + this.popWriteData(); // Ready data might be waiting ... + + if (waconnection.isConnected() && !waconnected) { + connectionOK(); // Notify the connection status + waconnected = true; + } + + if (listenerExecutor == null) { + System.out.println("Null!!! Shit\n"); + } + + Packet p; + synchronized (waconnection) { + p = waconnection.getNextPacket(); + } + while (p != null) { + submitPacket(p); + synchronized (waconnection) { + p = waconnection.getNextPacket(); + } + } + } + + /** + * Establishes a connection to the XMPP server and performs an automatic login + * only if the previous connection state was logged (authenticated). It basically + * creates and maintains a socket connection to the server.

+ *

+ * Listeners will be preserved from a previous connection if the reconnection + * occurs after an abrupt termination. + * + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + + // Specify pass at connect time + @Override + public void connect(final String user, final String pass, final String res) throws XMPPException { + config.setLoginInfo(user, pass, res); + connect(); + } + + public void connect() throws XMPPException { + // Stablishes the connection, readers and writers + connectUsingConfiguration(config); + // Automatically makes the login if the user was previouslly connected successfully + // to the server and the connection was terminated abruptly + } + + public void connectionOK() { + for (ConnectionListener listener : getConnectionListeners()) { + try { + listener.reconnectionSuccessful(); + } + catch (Exception e) { + // Catch and print any exception so we can recover + // from a faulty listener + e.printStackTrace(); + } + } + } + + @Override + public void setRosterStorage(RosterStorage storage) + throws IllegalStateException { + if(roster!=null){ + throw new IllegalStateException("Roster is already initialized"); + } + this.rosterStorage = storage; + } + + /** + * Returns whether connection with server is alive. + * + * @return false if timeout occur. + */ + @Override + public boolean isAlive() { + //PacketWriter packetWriter = this.packetWriter; + //return packetWriter == null || packetWriter.isAlive(); + // FIXME + return true; + } + + + /** + * A runnable to notify all listeners of a packet. + */ + private class ListenerNotification implements Runnable { + + private Packet packet; + + public ListenerNotification(Packet packet) { + this.packet = packet; + } + + public void run() { + for (ListenerWrapper listenerWrapper : recvListeners.values()) { + try { + listenerWrapper.notifyListener(packet); + } catch (RuntimeException e) { + e.printStackTrace(); + } + } + } + } + +} + diff --git a/src/net/davidgf/android/WAContacts.java b/src/net/davidgf/android/WAContacts.java new file mode 100644 index 0000000000..aa4349b8a8 --- /dev/null +++ b/src/net/davidgf/android/WAContacts.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2013, Redsolution LTD. All rights reserved. + * + * This file is part of Xabber project; you can redistribute it and/or + * modify it under the terms of the GNU General Public License, Version 3. + * + * Xabber is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License, + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package net.davidgf.android; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; + +import com.xabber.android.data.DatabaseManager; +import com.xabber.android.data.entity.AbstractAccountTable; +import java.util.*; + +/** + * Storage for whatsapp contacts + * + * @author david.guillen + */ +public class WAContacts extends AbstractAccountTable { + + private static final class Fields implements AbstractAccountTable.Fields { + + private Fields() { + } + + public static final String ACCOUNT = "account"; + public static final String USER_PHONE = "user_phone"; + public static final String USER_NAME = "user_name"; + } + + private static final String NAME = "wa_contacts"; + private static final String[] PROJECTION = new String[] { Fields.ACCOUNT, Fields.USER_PHONE, Fields.USER_NAME }; + static final boolean DEFAULT_EXPANDED = true; + + private final DatabaseManager databaseManager; + private SQLiteStatement writeStatement; + private final Object writeLock; + + private final static WAContacts instance; + + static { + instance = new WAContacts(DatabaseManager.getInstance()); + DatabaseManager.getInstance().addTable(instance); + } + + public static WAContacts getInstance() { + return instance; + } + + private WAContacts(DatabaseManager databaseManager) { + this.databaseManager = databaseManager; + writeStatement = null; + writeLock = new Object(); + } + + @Override + public void create(SQLiteDatabase db) { + String sql = "CREATE TABLE IF NOT EXISTS " + NAME + " (" + + Fields.ACCOUNT + " TEXT," + + Fields.USER_PHONE + " TEXT," + + Fields.USER_NAME + " TEXT);"; + DatabaseManager.execSQL(db, sql); + } + + @Override + public void migrate(SQLiteDatabase db, int toVersion) { + super.migrate(db, toVersion); + // No migration at this time + } + + public void addContact(String account, String user, String name) { + synchronized (writeLock) { + if (writeStatement == null) { + SQLiteDatabase db = databaseManager.getWritableDatabase(); + create(db); + writeStatement = db.compileStatement("INSERT OR REPLACE INTO " + + NAME + " (" + Fields.ACCOUNT + ", " + + Fields.USER_PHONE + ", " + + Fields.USER_NAME + ") VALUES (?, ?, ?);"); + } + writeStatement.bindString(1, account); + writeStatement.bindString(2, user); + writeStatement.bindString(3, name); + writeStatement.execute(); + } + } + + public void removeContact(String account, String user) { + synchronized (writeLock) { + if (writeStatement == null) { + SQLiteDatabase db = databaseManager.getWritableDatabase(); + create(db); + writeStatement = db.compileStatement("DELETE FROM " + + NAME + " WHERE " + Fields.USER_PHONE + " = " + + "? AND " + Fields.ACCOUNT + " = ?;"); + } + writeStatement.bindString(1, user); + writeStatement.bindString(2, account); + writeStatement.execute(); + } + } + + @Override + protected String getTableName() { + return NAME; + } + + @Override + protected String[] getProjection() { + return PROJECTION; + } + + public Vector < Vector > getContacts(String account) { + Vector < Vector > ret = new Vector < Vector >(); + + SQLiteDatabase db = databaseManager.getWritableDatabase(); + create(db); + Cursor c = db.query(NAME, PROJECTION, "account = '" + account + "'", null, null, null, null); + + if (c.moveToFirst()) { + do { + String user = c.getString(1); + String name = c.getString(2); + Vector < String > e = new Vector (); + e.add(user); + e.add(name); + ret.add(e); + } while(c.moveToNext()); + } + + return ret; + } + +} + diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java new file mode 100644 index 0000000000..121227306e --- /dev/null +++ b/src/net/davidgf/android/WhatsappConnection.java @@ -0,0 +1,922 @@ + +/* + * Java WhatsApp API implementation. + * Written by David Guillen Fandos (david@davidgf.net) based + * on the sources of WhatsAPI PHP implementation and whatsapp + * for libpurple. + * + * Share and enjoy! + * + */ + +package net.davidgf.android; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.packet.MessageEvent; +import org.jivesoftware.smackx.packet.Nick; +import org.jivesoftware.smackx.packet.MUCUser; +import org.jivesoftware.smackx.packet.DelayInformation; +import org.jivesoftware.smackx.packet.MUCInitialPresence; +import com.xabber.android.data.extension.muc.MUCManager; +import com.xabber.xmpp.vcard.VCard; +import com.xabber.xmpp.avatar.VCardUpdate; + +import java.net.*; +import java.util.*; +import java.io.*; + +public class WhatsappConnection { + private RC4Decoder in, out; + private byte session_key[][]; + private DataBuffer outbuffer; + private byte []challenge_data; + private int frame_seq; + + private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChallenge, SessionWaitingAuthOK, SessionConnected }; + private SessionStatus conn_status; + + private String phone, password, nickname; + private static String whatsappserver = "s.whatsapp.net"; + private static String whatsappservergroup = "g.us"; + + private String account_type, account_status, account_expiration, account_creation; + private String mypresence; + + private Vector received_packets; + private Vector contacts; + private Map groups; + boolean groups_updated; + int gq_stat; + int gw1,gw2; + + private int iqid; + private String mymessage = ""; + private String account_name = null; + + // HTTPS interface + private String sslnonce = ""; + private Thread http_thread; + + public WhatsappConnection(String phone, String pass, String nick, String aname) { + session_key = new byte[4][20]; + + this.phone = phone; + this.password = pass.trim(); + this.conn_status = SessionStatus.SessionNone; + this.nickname = nick; + this.mypresence = "available"; + outbuffer = new DataBuffer(); + received_packets = new Vector (); + contacts = new Vector (); + iqid = 0; + + groups = new HashMap (); + groups_updated = false; + gq_stat = 0; + gw1 = gw2 = 0; + + account_name = aname; + } + + public Tree read_tree(DataBuffer data) { + int lsize = data.readListSize(); + int type = data.getInt(1,0); + if (type == 1) { + data.popData(1); + Tree t = new Tree("start"); + t.readAttributes(data,lsize); + return t; + }else if (type == 2) { + data.popData(1); + return new Tree("treeerr"); // No data in this tree... + } + + Tree t = new Tree(data.readString()); + t.readAttributes(data,lsize); + + if ((lsize & 1) == 1) { + return t; + } + + if (data.isList()) { + t.setChildren(data.readList(this)); + }else{ + t.setData(data.readByteString()); + } + + return t; + } + + Tree parse_tree(DataBuffer data) { + int bflag = (data.getInt(1,0) & 0xF0)>>4; + int bsize = data.getInt(2,1); + if (bsize > data.size()-3) { + return new Tree("treeerr"); // Next message incomplete, return consumed data + } + data.popData(3); + + if ((bflag & 0x8) != 0) { + // Decode data, buffer conversion + if (this.in != null) { + DataBuffer decoded_data = data.decodedBuffer(this.in,bsize); + + Tree tt = read_tree(decoded_data); + + data.popData(bsize); // Pop data unencrypted for next parsing! + + // Remove hash + decoded_data.popData(4); + + return tt; + }else{ + data.popData(bsize); + return new Tree("treeerr"); + } + }else{ + return read_tree(data); + } + } + + public int pushIncomingData(byte [] data) { + if (data.length < 3) return 0; + + DataBuffer db = new DataBuffer(data); + + Tree t; + do { + t = this.parse_tree(db); + if (!t.getTag().equals("treeerr")) + this.processPacket(t); + + System.out.println(t.toString(0)); + } while (!t.getTag().equals("treeerr") && db.size() >= 3); + + return data.length - db.size(); + } + + public boolean isConnected() { + return conn_status == SessionStatus.SessionConnected; + } + + private void processPacket(Tree t) { + // Now process the tree list! + if (t.getTag().equals("challenge")) { + // Generate a session key using the challege & the password + assert(conn_status == SessionStatus.SessionWaitingChallenge); + + // Update key generation to V1.4 + session_key = KeyGenerator.generateKeyV14(password,t.getData()); + System.out.println(password); + + this.in = new RC4Decoder(session_key[2], 768); + this.out = new RC4Decoder(session_key[0], 768); + + conn_status = SessionStatus.SessionWaitingAuthOK; + challenge_data = t.getData(); + + this.sendAuthResponse(); + } + else if (t.getTag().equals("success")) { + // Notifies the success of the auth + conn_status = SessionStatus.SessionConnected; + if (t.hasAttribute("status")) + this.account_status = t.getAttributes().get("status"); + if (t.hasAttribute("kind")) + this.account_type = t.getAttributes().get("kind"); + if (t.hasAttribute("expiration")) + this.account_expiration = t.getAttributes().get("expiration"); + if (t.hasAttribute("creation")) + this.account_creation = t.getAttributes().get("creation"); + + this.notifyMyPresence(); + //this->sendInitial(); + this.updateGroups(); + + // Resend contact status query (for already added contacts) + for (int i = 0; i < contacts.size(); i++) { + subscribePresence(contacts.get(i).phone); + queryPreview(contacts.get(i).phone); + getLastSeen(contacts.get(i).phone); + } + } + else if (t.getTag().equals("notification")) { + DataBuffer reply = generateResponse(t.getAttribute("from"), + t.getAttribute("type"), + t.getAttribute("id")); + outbuffer = outbuffer.addBuf(reply); + } + else if (t.getTag().equals("presence")) { + // Receives the presence of the user + if ( t.hasAttribute("from") ) { + Presence.Mode mode = Presence.Mode.available; + if (t.hasAttribute("type")) + if (t.getAttribute("type").equals("unavailable")) + mode = Presence.Mode.away; + + Presence presp = new Presence(Presence.Type.available); + presp.setMode(mode); + presp.setFrom(MiscUtil.getUser(t.getAttribute("from"))); + presp.setTo(MiscUtil.getUser(phone)); + received_packets.add(presp); + + // Schedule the last seen querying + getLastSeen(t.getAttribute("from")); + } + } + else if (t.getTag().equals("iq")) { + // PING + if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("urn:xmpp:ping")) { + this.doPong(t.getAttribute("id"),t.getAttribute("from")); + } + + // Preview query + if (t.hasAttributeValue("type","result") && t.hasAttribute("from")) { + Tree tb = t.getChild("picture"); + if (tb != null) { + if (tb.hasAttributeValue("type","preview")) + this.addPreviewPicture(t.getAttribute("from"),tb.getData()); + } + tb = t.getChild("query"); + if (tb != null) { + if (tb.hasAttribute("seconds")) { + this.notifyLastSeen(t.getAttribute("from"),tb.getAttribute("seconds")); + } + } + } + + // Status message + if (t.hasChild("status")) { + Vector childs = t.getChildren(); + for (int j = 0; j < childs.size(); j++) { + if (childs.get(j).getTag().equals("user")) + this.notifyStatus(childs.get(j).getAttribute("jid"),MiscUtil.bytesToUTF8(childs.get(j).getData())); + } + } + + // Group stuff + Vector childs = t.getChildren(); + int acc = 0; + for (int j = 0; j < childs.size(); j++) { + if (childs.get(j).getTag().equals("group")) { + boolean rep = groups.containsKey(MiscUtil.getUser(childs.get(j).getAttribute("id"))); + if (!rep) { + groups.put( + MiscUtil.getUser(childs.get(j).getAttribute("id")), + new Group( MiscUtil.getUser(childs.get(j).getAttribute("id")), + childs.get(j).getAttribute("subject"), + MiscUtil.getUser(childs.get(j).getAttribute("owner")) ) ); + + // Query group participants + final String iid = String.valueOf(++iqid); + final String pid = childs.get(j).getAttribute("id"); + final String subj = childs.get(j).getAttribute("subject"); + Tree iq = new Tree("list"); + Tree req = new Tree("iq", + new HashMap < String,String >(){{ + put("id",iid); + put("type","get"); + put("to",pid+"@g.us"); + put("xmlns","w:g"); + }}); + req.addChild(iq); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + + // Add group as a contact + pushGroupUpdate(); + } + } + else if (childs.get(j).getTag().equals("participant")) { + String gid = MiscUtil.getUser(t.getAttribute("from")); + String pt = MiscUtil.getUser(childs.get(j).getAttribute("jid")); + if (groups.containsKey(gid)) { + groups.get(gid).participants.add(pt); + + pushGroupUpdate(); + } + } + } + + Tree tb = t.getChild("group"); + if (tb != null) { + if (tb.hasAttributeValue("type","preview")) + this.addPreviewPicture(t.getAttribute("from"),t.getData()); + } + } + else if (t.getTag().equals("message")) { + if (t.hasAttribute("from") && + (t.hasAttributeValue("type","text") || t.hasAttributeValue("type","media")) ) { + long time = 0; + if (t.hasAttribute("t")) + time = Integer.parseInt(t.getAttribute("t")); + String from = t.getAttribute("from"); + String id = t.getAttribute("id"); + String author = t.getAttribute("participant"); + + // Group nickname + if (from.contains("@g.us") && t.hasChild("notify")) { + author = t.getChild("notify").getAttribute("name"); + from = from + "/" + author; + } + + Tree tb = t.getChild("body"); + if (tb != null) { + // TODO: Add user here and at UI in case we don't have it + this.receiveMessage( + new ChatMessage(from,time,id,MiscUtil.bytesToUTF8(tb.getData()),author)); + addContact(from,false); + } + + tb = t.getChild("media"); + if (tb != null) { + // Photo/audio + if (tb.hasAttributeValue("type","image") || + tb.hasAttributeValue("type","audio") || + tb.hasAttributeValue("type","video")) { + + this.receiveMessage( + new ImageMessage(from,time,id,tb.getAttribute("url"),tb.getData(),author)); + addContact(from,false); + } + } + } + else if (t.hasAttributeValue("type", "notification") && t.hasAttribute("from")) { + /* If the nofitication comes from a group, assume we have to reload groups ;) */ + updateGroups(); + } + + // Received ACK + if (t.hasAttribute("type") && t.hasAttribute("from")) { + DataBuffer reply = generateResponse(t.getAttribute("from"), + t.getAttribute("type"), + t.getAttribute("id")); + outbuffer = outbuffer.addBuf(reply); + + } + } + else if (t.getTag().equals("chatstate")) { + if (t.hasChild("composing")) { + gotTyping(t.getAttribute("from"),true); + } + if (t.hasChild("paused")) { + gotTyping(t.getAttribute("from"),false); + } + } + else if (t.getTag().equals("receipt")) { + final String pid = t.getAttribute("id"); + final String typed = t.getAttribute("type"); + final String typef = (typed.equals("") ? "delivery" : typed); + Tree req = new Tree("ack", + new HashMap < String,String >(){{ + put("id",pid); + put("type",typef); + put("class","receipt"); + }}); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + + /*else if (treelist[i].getTag() == "failure") { + if (conn_status == SessionWaitingAuthOK) + this->notifyError(errorAuth); + else + this->notifyError(errorUnknown); + }*/ + } + + private String last_seen_text(long t) { + if (t < 60) + return String.valueOf(t) + " seconds ago"; + else if (t < 60*60) + return String.valueOf(t/60) + " minute(s) ago"; + else if (t < 60*60*24) + return String.valueOf(t/60/60) + " hour(s) ago"; + else if (t < 48*60*60) + return "yesterday"; + else + return String.valueOf(t/60/60/24) + " day(s) ago"; + } + + private void notifyLastSeen(final String from, final String seconds) { + final String u = MiscUtil.getUser(from); + final long sec = Integer.parseInt(seconds); + + // Save last seen time + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(u)) { + contacts.get(i).last_seen = sec; + break; + } + } + + requestVCardUpdate(u); + } + + private void notifyStatus(final String from, final String status) { + final String u = MiscUtil.getUser(from); + + // Save last seen time + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(u)) { + contacts.get(i).status = status; + break; + } + } + + requestVCardUpdate(u); + } + + public String getNotes(String u) { + u = MiscUtil.getUser(u); + + String note = ""; + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(u)) { + note = "Last seen: " + last_seen_text(contacts.get(i).last_seen); + break; + } + } + + return note; + } + + public void pushGroupUpdate() { + for (Map.Entry entry : groups.entrySet()) { + // Create room + MUCManager.getInstance().createRoom( + account_name, entry.getValue().id, phone, "", false, entry.getValue().subject); + + for (int i = 0; i < entry.getValue().participants.size(); i++) { + // Send the room status (chek MultiUserChat.java),matched the filter + Presence.Mode mode = Presence.Mode.chat; + Presence presp = new Presence(Presence.Type.available); + presp.setMode(mode); + presp.setFrom(entry.getValue().id + "/" + entry.getValue().participants.get(i)); + presp.setTo(MiscUtil.getUser(phone)); + // Add MUC User + MUCUser user = new MUCUser(); + user.setItem(new MUCUser.Item("member","participant")); + presp.addExtension(user); + received_packets.add(presp); + } + } + } + + private DataBuffer generateResponse(final String from, final String type, final String id) { + Tree mes = new Tree("receipt",new HashMap < String,String >() {{ + put("to",from); put("id",id); }} ); + return serialize_tree(mes,true); + } + + private void queryPreview(final String user) { + final String fuser = user+"@"+whatsappserver; + final String reqid = String.valueOf(++iqid); + Tree pic = new Tree ("picture", + new HashMap < String,String >() {{ put("type","preview"); }} ); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ + put("id",reqid); + put("type","get"); + put("to",fuser); + put("xmlns","w:profile:picture"); + }} + ); + + req.addChild(pic); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + private void updateGroups() { + groups.clear(); + { + final String reqid = String.valueOf(++iqid); + gw1 = iqid; + Tree iq = new Tree("list",new HashMap < String,String >() {{ put("type","owning");}} ); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us"); put("xmlns","w:g"); }} ); + + req.addChild(iq); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + { + final String reqid = String.valueOf(++iqid); + gw2 = iqid; + Tree iq = new Tree("list", + new HashMap < String,String >() {{ put("type","participating");}} ); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us"); put("xmlns","w:g"); }} ); + req.addChild(iq); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + gq_stat = 1; // Queried the groups + } + + private void manageParticipant(final String group, final String participant, final String command) { + Tree part = new Tree("participant",new HashMap < String,String >() {{ put("jid",participant); }} ); + Tree iq = new Tree(command); + iq.addChild(part); + final String reqid = String.valueOf(++iqid); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ + put("id",reqid); put("type","set"); put("to",group+"@g.us"); put("xmlns","w:g"); + }} + ); + req.addChild(iq); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + private void leaveGroup(final String group) { + Tree gr = new Tree("group", new HashMap < String,String >() {{ put("id",group+"@g.us"); }} ); + Tree iq = new Tree("leave"); + iq.addChild(gr); + final String reqid = String.valueOf(++iqid); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us"); put("xmlns","w:g"); }} ); + req.addChild(iq); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + void addGroup(final String subject) { + Tree gr = new Tree("group", + new HashMap < String,String >() {{ put("action","create"); put("subject",subject);}} ); + final String reqid = String.valueOf(++iqid); + Tree req = new Tree("iq", + new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us"); put("xmlns","w:g"); }} ); + req.addChild(gr); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true))); + } + + public byte[] getUserAvatar(String user) { + // Look for preview + user = MiscUtil.getUser(user); + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(user)) { + return contacts.get(i).ppprev; + } + } + return new byte[0]; + } + + private void addPreviewPicture(String user, byte [] picture) { + user = MiscUtil.getUser(user); + + // Save preview + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(user)) { + contacts.get(i).ppprev = picture; + break; + } + } + + requestVCardUpdate(user); + } + + private void requestVCardUpdate(String user) { + user = MiscUtil.getUser(user); + + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).phone.equals(user)) { + VCardUpdate vc = new VCardUpdate(); + vc.setPhotoHash(MiscUtil.getEncodedSha1Sum(contacts.get(i).ppprev)); + Presence p = new Presence(Presence.Type.subscribed); + p.setTo(phone); + p.setFrom(user); + p.addExtension(vc); + received_packets.add(p); + break; + } + } + } + + private void receiveMessage(AbstractMessage msg) { + received_packets.add(msg.serializePacket()); + } + + private void gotTyping(final String user, boolean typing) { + Message msg = new Message(); + msg.setTo(MiscUtil.getUser(phone)); + msg.setFrom(MiscUtil.getUser(user)); + + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setComposing(true); + msg.addExtension(messageEvent); + // Send the packet + received_packets.add(msg); + } + + public Packet getNextPacket() { + if (received_packets.size() == 0) + return null; + Packet r = received_packets.get(0); + received_packets.remove(0); + + return r; + } + + public void setMyPresence(String pres, String msg) { + mypresence = pres; + mymessage = msg; + if (conn_status == SessionStatus.SessionConnected) + notifyMyPresence(); + } + + private void notifyMyPresence() { + // Send the nickname and the current status + Tree pres = new Tree("presence", new HashMap < String,String >() {{ put("name",nickname); put("type",mypresence); }} ); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(pres,true))); + } + + void doPong(final String id, final String from) { + Tree t = new Tree("iq",new HashMap < String,String >() {{ put("to",from); put("id",id); put("type","result"); }} ); + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,true))); + } + + public DataBuffer write_tree(Tree tree) { + DataBuffer bout = new DataBuffer(); + int len = 1; + + if (tree.getAttributes().size() != 0) len += tree.getAttributes().size()*2; + if (tree.getChildren().size() != 0) len++; + if (tree.getData().length != 0 || tree.forcedData()) len++; + + bout.writeListSize(len); + if (tree.getTag().equals("start")) bout.putInt(1,1); + else bout.putString(tree.getTag()); + tree.writeAttributes(bout); + + if (tree.getData().length > 0 || tree.forcedData()) + bout.putRawString(tree.getData()); + if (tree.getChildren().size() > 0) { + bout.writeListSize(tree.getChildren().size()); + + for (int i = 0; i < tree.getChildren().size(); i++) { + DataBuffer tt = write_tree(tree.getChildren().get(i)); + bout = bout.addBuf(tt); + } + } + return bout; + } + + public DataBuffer serialize_tree(Tree tree, boolean crypt) { + + System.out.println("OUT"); + System.out.println(tree.toString(0)); + + DataBuffer data = write_tree(tree); + int flag = 0; + if (crypt) { + data = data.encodedBuffer(this.out,this.session_key[1],true,frame_seq++); + flag = 0x80; + } + + System.out.println("OUTDONE"); + + DataBuffer ret = new DataBuffer(); + ret.putInt(flag,1); + ret.putInt(data.size(),2); + return ret.addBuf(data); + } + + + public void doLogin(String useragent) { + DataBuffer first = new DataBuffer(); + + { + Map < String,String > auth = new HashMap (); + auth.put("resource", useragent); + auth.put("to", whatsappserver); + Tree t = new Tree("start",auth); + first.addData( new byte [] {'W','A',1,4} ); + first = first.addBuf(serialize_tree(t,false)); + } + + // Send features + { + Tree p = new Tree("stream:features"); + first = first.addBuf(serialize_tree(p,false)); + } + + // Send auth request + { + Map < String,String > auth = new HashMap (); + auth.put("mechanism", "WAUTH-2"); + auth.put("user", phone); + Tree t = new Tree("auth",auth); + t.forceDataWrite(); + first = first.addBuf(serialize_tree(t,false)); + } + + conn_status = SessionStatus.SessionWaitingChallenge; + outbuffer = first; + } + + void sendAuthResponse() { + Tree t = new Tree("response"); + + long epoch = System.currentTimeMillis()/1000; + String stime = String.valueOf(epoch); + DataBuffer eresponse = new DataBuffer(); + eresponse.addData(phone.getBytes()); + eresponse.addData(challenge_data); + eresponse.addData(stime.getBytes()); + + eresponse = eresponse.encodedBuffer(this.out,this.session_key[1],false, frame_seq++); + t.setData(eresponse.getPtr()); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,false))); + } + + public void addContact(String user, boolean user_request) { + user = MiscUtil.getUser(user); + if (user.contains("-")) return; // Do not add groups as contacts + + boolean found = false; + for (int i = 0; i < contacts.size(); i++) + if (contacts.get(i).phone.equals(user)) { + found = true; + return; + } + + if (!found) { + Contact c = new Contact(user, user_request); + contacts.add(c); + } + + if (conn_status == SessionStatus.SessionConnected) { + subscribePresence(user); + queryPreview(user); + getLastSeen(user); + } + } + + public void subscribePresence(String user) { + final String username = MiscUtil.getUser(user)+"@"+whatsappserver; + Tree request = new Tree("presence", + new HashMap < String,String >() {{ put("type","subscribe"); put("to",username); }} ); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(request,true))); + + // Meanwhile add the user presence... + Presence.Mode mode = Presence.Mode.away; + Presence presp = new Presence(Presence.Type.available); + presp.setMode(mode); + presp.setFrom(MiscUtil.getUser(user)); + presp.setTo(MiscUtil.getUser(phone)); + received_packets.add(presp); + } + + private void getLastSeen(final String user) { + final String id = String.valueOf(++iqid); + final String fuser = MiscUtil.getUser(user)+"@"+whatsappserver; + Tree iq = new Tree("iq", + new HashMap < String,String >() {{ + put("id",id); put("type","get"); put("to",fuser); put("xmlns","jabber:iq:last"); + }} + ); + iq.addChild(new Tree("query")); + + outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(iq,true))); + } + + public byte [] getWriteData() { + byte [] r = outbuffer.getPtr(); + System.out.println("Sending some bytes ... " + String.valueOf(r.length)); + outbuffer = new DataBuffer(); + return r; + } + + // Helper for Message class + public byte [] serializeMessage(final String to, String message, int id) { + try { + Tree tbody = new Tree("body"); + tbody.setData(message.getBytes("UTF-8")); + + long epoch = System.currentTimeMillis()/1000; + String stime = String.valueOf(epoch); + Map < String,String > attrs = new HashMap (); + String full_to = to + "@" + (to.contains("-") ? whatsappservergroup : whatsappserver); + attrs.put("to",full_to); + attrs.put("type","chat"); + attrs.put("id",stime+"-"+String.valueOf(id)); + attrs.put("t",stime); + + Tree mes = new Tree("message",attrs); + mes.addChild(tbody); + + return serialize_tree(mes,true).getPtr(); + }catch (Exception e) { + return new byte[0]; + } + } + + public abstract class AbstractMessage { + protected String from, id, author; + protected long time; + + public AbstractMessage(String from, long time, String id, String author) { + this.from = from; + this.id = id; + this.author = author; + this.time = time; + } + + public abstract Packet serializePacket(); + } + + public class ChatMessage extends AbstractMessage { + private String message; + public ChatMessage(String from, long time, String id, String message, String author) { + super(from, time, id, author); + this.message = message; + } + + public Packet serializePacket() { + Message message = new Message(); + message.setTo(MiscUtil.getUser(phone)); + message.setFrom(MiscUtil.getUserAndResource(this.from)); + message.setType(Message.Type.chat); + message.setBody(this.message); + + // XXX: Criteria for adding Delay info is + // if the timestamp and the current time differ in more than 10 seconds + DelayInformation d = new DelayInformation(new Date(time*1000)); + long epoch = System.currentTimeMillis()/1000; + if (Math.abs(time - epoch) > 10) + message.addExtension(d); + + return message; + } + } + public class ImageMessage extends AbstractMessage { + private String url; + private byte [] preview; + public ImageMessage(String from, long time, String id, String url, byte [] prev, String author) { + super(from, time, id, author); + this.url = url; + this.preview = prev; + } + + public Packet serializePacket() { + Message message = new Message(); + message.setTo(MiscUtil.getUser(phone)); + message.setFrom(MiscUtil.getUserAndResource(this.from)); + message.setType(Message.Type.chat); + message.setBody(url); + + if (author != null && author.length() != 0) + message.addExtension(new Nick(author)); + + // XXX: Criteria for adding Delay info is + // if the timestamp and the current time differ in more than 10 seconds + DelayInformation d = new DelayInformation(new Date(time*1000)); + long epoch = System.currentTimeMillis()/1000; + if (Math.abs(time - epoch) > 10) + message.addExtension(d); + + return message; + } + } + + + public class Contact { + String phone, name; + String presence, typing; + String status; + long last_seen, last_status; + boolean mycontact; + byte[] ppprev, pppicture; + boolean subscribed; + + Contact(String phone, boolean myc) { + this.phone = phone; + this.mycontact = myc; + this.last_seen = 0; + this.subscribed = false; + this.typing = "paused"; + this.status = ""; + } + }; + + public class Group { + String id, subject, owner; + Vector participants; + + Group(String id, String subject, String owner) { + this.id = id; + this.subject = subject; + this.owner = owner; + participants = new Vector (); + } + }; + +} + + diff --git a/src/org/jivesoftware/smack/Connection.java b/src/org/jivesoftware/smack/Connection.java index a96567f2d1..f3fd899388 100644 --- a/src/org/jivesoftware/smack/Connection.java +++ b/src/org/jivesoftware/smack/Connection.java @@ -226,6 +226,10 @@ protected ConnectionConfiguration getConfiguration() { public String getServiceName() { return config.getServiceName(); } + + public boolean isAlive() { + return true; + } /** * Returns the host name of the server where the XMPP server is running. This would be the @@ -329,6 +333,10 @@ protected boolean isReconnectionAllowed() { * @throws XMPPException if an error occurs while trying to establish the connection. */ public abstract void connect() throws XMPPException; + + public void connect(final String user, final String pass, final String res) throws XMPPException { + connect(); + } /** * Logs in to the server using the strongest authentication mode supported by diff --git a/src/org/jivesoftware/smack/XMPPConnection.java b/src/org/jivesoftware/smack/XMPPConnection.java index 809955207b..aadd182793 100644 --- a/src/org/jivesoftware/smack/XMPPConnection.java +++ b/src/org/jivesoftware/smack/XMPPConnection.java @@ -1095,6 +1095,7 @@ public void setRosterStorage(RosterStorage storage) * * @return false if timeout occur. */ + @Override public boolean isAlive() { PacketWriter packetWriter = this.packetWriter; return packetWriter == null || packetWriter.isAlive();