From 886b8ce988c7f649beb9a2c867407891e83ec3e3 Mon Sep 17 00:00:00 2001 From: moyamo Date: Thu, 26 Feb 2015 20:58:25 +0200 Subject: [PATCH 01/51] Add more dependencies to README --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 3326f27..286f631 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,15 @@ Use my patched version at https://github.com/stv0g/yowsup #### Google Atom and GData Python wrappers required for Google contacts import +#### MySQLdb +required + +#### Google protobuf +required + +#### date.util +required + ## Contribute Pull requests, bug reports etc. are welcome. From 8dcc0285484929a7d9ff687dd70d4ca9b81d2299 Mon Sep 17 00:00:00 2001 From: moyamo Date: Thu, 26 Feb 2015 22:52:29 +0200 Subject: [PATCH 02/51] Add Yowsup stack to session Try to replace the old yowsup library by using a yowsup 2 stack. --- session.py | 70 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/session.py b/session.py index e1d8e61..e5e48ea 100644 --- a/session.py +++ b/session.py @@ -27,7 +27,21 @@ import logging import urllib import time -from Yowsup.connectionmanager import YowsupConnectionManager +from yowsup.stacks import YowStack +from yowsup.layers import YowLayerEvent +from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, + AuthError) +from yowsup.layers.coder import YowCoderLayer +from yowsup.layers.network import YowNetworkLayer +from yowsup.layers.protocol_messages import YowMessagesProtocolLayer +from yowsup.layers.protocol_media import YowMediaProtocolLayer +from yowsup.layers.stanzaregulator import YowStanzaRegulator +from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer +from yowsup.layers.protocol_acks import YowAckProtocolLayer +from yowsup.layers.logger import YowLoggerLayer +from yowsup.common import YowConstants +from yowsup import env + from Spectrum2 import protocol_pb2 from buddy import BuddyList @@ -60,38 +74,23 @@ class Session: self.initialized = False self.buddies = BuddyList(legacyName, db) - self.frontend = YowsupConnectionManager() self.bot = Bot(self) - # Events - self.listen("auth_success", self.onAuthSuccess) - self.listen("auth_fail", self.onAuthFailed) - self.listen("disconnected", self.onDisconnected) + env.CURRENT_ENV = env.S40YowsupEnv() + layers = ( + (YowAuthenticationProtocolLayer, + YowMessagesProtocolLayer, + YowReceiptProtocolLayer, + YowAckProtocolLayer, + YowMediaProtocolLayer), + YowCoderLayer, + YowCryptLayer, + YowStanzaRegulator, + YowNetworkLayer + ) + self.stack = YowStack(layers) - self.listen("contact_typing", self.onContactTyping) - self.listen("contact_paused", self.onContactPaused) - - self.listen("presence_updated", self.onPrecenceUpdated) - self.listen("presence_available", self.onPrecenceAvailable) - self.listen("presence_unavailable", self.onPrecenceUnavailable) - - self.listen("message_received", self.onMessageReceived) - self.listen("image_received", self.onMediaReceived) - self.listen("video_received", self.onMediaReceived) - self.listen("audio_received", self.onMediaReceived) - self.listen("location_received", self.onLocationReceived) - self.listen("vcard_received", self.onVcardReceived) - - self.listen("group_messageReceived", self.onGroupMessageReceived) - self.listen("group_gotInfo", self.onGroupGotInfo) - self.listen("group_gotParticipants", self.onGroupGotParticipants) - self.listen("group_subjectReceived", self.onGroupSubjectReceived) - - self.listen("notification_groupParticipantAdded", self.onGroupParticipantAdded) - self.listen("notification_groupParticipantRemoved", self.onGroupParticipantRemoved) - self.listen("notification_contactProfilePictureUpdated", self.onContactProfilePictureUpdated) - self.listen("notification_groupPictureUpdated", self.onGroupPictureUpdated) def __del__(self): # handleLogoutRequest self.logout() @@ -108,8 +107,17 @@ class Session: self.call("disconnect", ("logout",)) def login(self, password): - self.password = utils.decodePassword(password) - self.call("auth_login", (self.legacyName, self.password)) + self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, + password) + self.stack.setProp(YowNetworkLayer.PROP_ENDPOINT, + YowConstants.ENDPOINTS[0]) + self.stack.setProp(YowCoderLayer.PROP_DOMAIN, + YowConstants.DOMAIN) + self.stack.setProp(YowCoderLayer.PROP_RESOURCE, + env.CURRENT_ENV.getResource()) + self.stack.broadcastEvent( + YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) + self.stack.loop() def updateRoomList(self): rooms = [] From 1878a30fb61802429cad6656156ae1190fa87631 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 14:33:55 +0200 Subject: [PATCH 03/51] Authenticate user with legacyName and password I forgot to send the legacyName with the password when authenticating. --- session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session.py b/session.py index e5e48ea..ce930f5 100644 --- a/session.py +++ b/session.py @@ -108,7 +108,7 @@ class Session: def login(self, password): self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, - password) + (self.legacyName, password)) self.stack.setProp(YowNetworkLayer.PROP_ENDPOINT, YowConstants.ENDPOINTS[0]) self.stack.setProp(YowCoderLayer.PROP_DOMAIN, From 9fc0dc73331837beb78f72282e0db685a14277e5 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 14:58:32 +0200 Subject: [PATCH 04/51] Add logout event and disable call --- session.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/session.py b/session.py index ce930f5..064bbfe 100644 --- a/session.py +++ b/session.py @@ -29,6 +29,7 @@ import time from yowsup.stacks import YowStack from yowsup.layers import YowLayerEvent +from yowsup.layers import YowParallelLayer from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, AuthError) from yowsup.layers.coder import YowCoderLayer @@ -79,11 +80,11 @@ class Session: env.CURRENT_ENV = env.S40YowsupEnv() layers = ( - (YowAuthenticationProtocolLayer, + YowParallelLayer((YowAuthenticationProtocolLayer, YowMessagesProtocolLayer, YowReceiptProtocolLayer, YowAckProtocolLayer, - YowMediaProtocolLayer), + YowMediaProtocolLayer)), YowCoderLayer, YowCryptLayer, YowStanzaRegulator, @@ -98,13 +99,10 @@ class Session: def call(self, method, args = ()): args = [str(s) for s in args] self.logger.debug("%s(%s)", method, ", ".join(args)) - self.frontend.methodInterface.call(method, args) - - def listen(self, event, callback): - self.frontend.signalInterface.registerListener(event, callback) +# self.frontend.methodInterface.call(method, args) def logout(self): - self.call("disconnect", ("logout",)) + self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT)) def login(self, password): self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, From 1ebf4e5cc9884a1219415c11db89343f0b4d93e5 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 16:14:01 +0200 Subject: [PATCH 05/51] Add SpectrumLayer to YowStack and handle Auth Things that Session handled should be handled by a YowInterfaceLayer. SpectrumLayer is such a layer. Paramaters that Session's __init__ is passed into SpectrumLayer via Props. --- session.py | 115 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 47 deletions(-) diff --git a/session.py b/session.py index 064bbfe..7cecebc 100644 --- a/session.py +++ b/session.py @@ -28,8 +28,8 @@ import urllib import time from yowsup.stacks import YowStack -from yowsup.layers import YowLayerEvent -from yowsup.layers import YowParallelLayer +from yowsup.layers import YowLayerEvent, YowParallelLayer +from yowsup.layers.interface import YowInterfaceLayer, PrototocolEntityCallback from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, AuthError) from yowsup.layers.coder import YowCoderLayer @@ -42,6 +42,7 @@ from yowsup.layers.protocol_acks import YowAckProtocolLayer from yowsup.layers.logger import YowLoggerLayer from yowsup.common import YowConstants from yowsup import env +from yowsup.layers.protocol_presence import * from Spectrum2 import protocol_pb2 @@ -51,7 +52,7 @@ from group import Group from bot import Bot from constants import * -class Session: +class Session(): def __init__(self, backend, user, legacyName, extra, db): self.logger = logging.getLogger(self.__class__.__name__) @@ -74,12 +75,11 @@ class Session: self.password = None self.initialized = False - self.buddies = BuddyList(legacyName, db) self.bot = Bot(self) env.CURRENT_ENV = env.S40YowsupEnv() - layers = ( + layers = (SpectrumLayer, YowParallelLayer((YowAuthenticationProtocolLayer, YowMessagesProtocolLayer, YowReceiptProtocolLayer, @@ -91,7 +91,9 @@ class Session: YowNetworkLayer ) self.stack = YowStack(layers) - + self.stack.setProp(SpectrumLayer.PROP_BACKEND, self.backend) + self.stack.setProp(SpectrumLayer.PROP_USER, self.user) + self.stack.setProp(SpectrumLayer.PROP_DB, self.db) def __del__(self): # handleLogoutRequest self.logout() @@ -237,47 +239,6 @@ class Session: else: self.logger.warn("Room doesn't exist: %s", room) - def updateRoster(self): - self.logger.debug("Update roster") - - old = self.buddies.keys() - self.buddies.load() - new = self.buddies.keys() - - add = set(new) - set(old) - remove = set(old) - set(new) - - self.logger.debug("Roster remove: %s", str(list(remove))) - self.logger.debug("Roster add: %s", str(list(add))) - - for number in remove: - self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE) - self.backend.handleBuddyRemoved(self.user, number) - self.call("presence_unsubscribe", (number + "@s.whatsapp.net",)) - - for number in add: - buddy = self.buddies[number] - self.backend.handleBuddyChanged(self.user, number, buddy.nick, buddy.groups, protocol_pb2.STATUS_NONE) - self.call("presence_request", (number + "@s.whatsapp.net",)) # includes presence_subscribe - - - # yowsup Signals - def onAuthSuccess(self, user): - self.logger.info("Auth success: %s", user) - - self.backend.handleConnected(self.user) - self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE) - - self.updateRoster() - - self.call("ready") - self.call("group_getGroups", ("participating",)) - - def onAuthFailed(self, user, reason): - self.logger.info("Auth failed: %s (%s)", user, reason) - self.backend.handleDisconnected(self.user, 0, reason) - self.password = None - def onDisconnected(self, reason): self.logger.info("Disconnected from whatsapp: %s (%s)", self.legacyName, reason) self.backend.handleDisconnected(self.user, 0, reason) @@ -443,3 +404,63 @@ class Session: def onGroupPictureUpdated(self, jid, author, timestamp, messageId, pictureId, receiptRequested): # TODO if receiptRequested: self.call("notification_ack", (jid, messageId)) + +class SpectrumLayer(YowInterfaceLayer): + PROP_BACKEND = "yowsup.prop.SpectrumLayer.backend" + PROP_USER = "yowsup.prop.SpectrumLayer.user" + PROP_DB = "yowsup.prop.SpectrumLayer.db" + PROP_LEGACYNAME = "yowsup.prop.SpectrumLayer.legacyName" + + def __init__(self): + super(SpectrumLayer, self).__init__() + self.backend = self.getProp(SpectrumLayer.PROP_BACKEND) + self.user = self.getProp(SpectrumLayer.PROP_USER) + db = self.getProp(SpectrumLayer.PROP_DB) + self.buddies = BuddyList(legacyName, db) + self.legacyName(self.getProp(SpectrumLayer.PROP_LEGACYNAME)) + self.bot = Bot(self) + + @PrototocolEntityCallback("success") + def onAuthSuccess(self, entity): + self.logger.info("Auth success: %s", self.user) + + self.backend.handleConnected(self.user) + self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE) + + self.updateRoster() + + self.call("ready") + self.call("group_getGroups", ("participating",)) + + @PrototocolEntityCallback("failed") + def onAuthFailed(self, entity): + self.logger.info("Auth failed: %s (%s)", self.user, entity.getReason()) + self.backend.handleDisconnected(self.user, 0, reason) + self.password = None + + def updateRoster(self): + self.logger.debug("Update roster") + + old = self.buddies.keys() + self.buddies.load() + new = self.buddies.keys() + + add = set(new) - set(old) + remove = set(old) - set(new) + + self.logger.debug("Roster remove: %s", str(list(remove))) + self.logger.debug("Roster add: %s", str(list(add))) + + for number in remove: + self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE) + self.backend.handleBuddyRemoved(self.user, number) + self.broadcastEvent(YowNetworkLayer) + entity = UnsubscribePresenceProtocolEntity(number + "@s.whatsapp.net") + self.toLower(entity) + + for number in add: + buddy = self.buddies[number] + entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") + self.toLower(entity) + + From 74a09ad07f69fbb3b966134ee4d0db9d4600c025 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 16:19:51 +0200 Subject: [PATCH 06/51] Fix typo --- session.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/session.py b/session.py index 7cecebc..5e1a4c3 100644 --- a/session.py +++ b/session.py @@ -29,7 +29,7 @@ import time from yowsup.stacks import YowStack from yowsup.layers import YowLayerEvent, YowParallelLayer -from yowsup.layers.interface import YowInterfaceLayer, PrototocolEntityCallback +from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, AuthError) from yowsup.layers.coder import YowCoderLayer @@ -420,7 +420,7 @@ class SpectrumLayer(YowInterfaceLayer): self.legacyName(self.getProp(SpectrumLayer.PROP_LEGACYNAME)) self.bot = Bot(self) - @PrototocolEntityCallback("success") + @ProtocolEntityCallback("success") def onAuthSuccess(self, entity): self.logger.info("Auth success: %s", self.user) @@ -432,7 +432,7 @@ class SpectrumLayer(YowInterfaceLayer): self.call("ready") self.call("group_getGroups", ("participating",)) - @PrototocolEntityCallback("failed") + @ProtocolEntityCallback("failed") def onAuthFailed(self, entity): self.logger.info("Auth failed: %s (%s)", self.user, entity.getReason()) self.backend.handleDisconnected(self.user, 0, reason) From ae826b1341d23790faa4e24c276d4bb52facb826 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 16:57:00 +0200 Subject: [PATCH 07/51] Use YowLayerEvent instead of Prop We cannot use Prop in the __init__ of a YowLayer, since Prop is initialised only after all the YowLayers are initialized. Instead, we send a YowLayerEvent containing the values. --- session.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/session.py b/session.py index 5e1a4c3..ebfb3c7 100644 --- a/session.py +++ b/session.py @@ -91,9 +91,14 @@ class Session(): YowNetworkLayer ) self.stack = YowStack(layers) - self.stack.setProp(SpectrumLayer.PROP_BACKEND, self.backend) - self.stack.setProp(SpectrumLayer.PROP_USER, self.user) - self.stack.setProp(SpectrumLayer.PROP_DB, self.db) + self.stack.broadcastEvent( + YowLayerEvent(SpectrumLayer.EVENT_START, + backend = self.backend, + user = self.user, + db = self.db, + legacyName = self.legacyName + ) + ) def __del__(self): # handleLogoutRequest self.logout() @@ -406,19 +411,18 @@ class Session(): if receiptRequested: self.call("notification_ack", (jid, messageId)) class SpectrumLayer(YowInterfaceLayer): - PROP_BACKEND = "yowsup.prop.SpectrumLayer.backend" - PROP_USER = "yowsup.prop.SpectrumLayer.user" - PROP_DB = "yowsup.prop.SpectrumLayer.db" - PROP_LEGACYNAME = "yowsup.prop.SpectrumLayer.legacyName" + EVENT_START = "transwhat.event.SpectrumLayer.start" - def __init__(self): - super(SpectrumLayer, self).__init__() - self.backend = self.getProp(SpectrumLayer.PROP_BACKEND) - self.user = self.getProp(SpectrumLayer.PROP_USER) - db = self.getProp(SpectrumLayer.PROP_DB) - self.buddies = BuddyList(legacyName, db) - self.legacyName(self.getProp(SpectrumLayer.PROP_LEGACYNAME)) - self.bot = Bot(self) + def onEvent(self, layerEvent): + # We cannot use __init__, since it can take no arguments + if layerEvent.name == SpectrumLayer.EVENT_START: + self.backend = layerEvent.getArg("backend") + self.user = layerEvent.getArg("user") + self.legacyName = layerEvent.getArg("legacyName") + db = layerEvent.getArg("db") + + self.buddies = BuddyList(legacyName, db) + self.bot = Bot(self) @ProtocolEntityCallback("success") def onAuthSuccess(self, entity): From 05525a8a3a57aef9b45dd3f118320b88cad89459 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 17:13:48 +0200 Subject: [PATCH 08/51] Fix bugs --- session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/session.py b/session.py index ebfb3c7..183b838 100644 --- a/session.py +++ b/session.py @@ -419,9 +419,9 @@ class SpectrumLayer(YowInterfaceLayer): self.backend = layerEvent.getArg("backend") self.user = layerEvent.getArg("user") self.legacyName = layerEvent.getArg("legacyName") - db = layerEvent.getArg("db") + self.db = layerEvent.getArg("db") - self.buddies = BuddyList(legacyName, db) + self.buddies = BuddyList(self.legacyName, self.db) self.bot = Bot(self) @ProtocolEntityCallback("success") From 479efedc261efe34bef49c0ff30039c984a0e4a2 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 17:25:45 +0200 Subject: [PATCH 09/51] Add logger remove calls --- session.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/session.py b/session.py index 183b838..9f37c01 100644 --- a/session.py +++ b/session.py @@ -416,6 +416,7 @@ class SpectrumLayer(YowInterfaceLayer): def onEvent(self, layerEvent): # We cannot use __init__, since it can take no arguments if layerEvent.name == SpectrumLayer.EVENT_START: + self.logger = logging.getLogger(self.__class__.__name__) self.backend = layerEvent.getArg("backend") self.user = layerEvent.getArg("user") self.legacyName = layerEvent.getArg("legacyName") @@ -433,9 +434,6 @@ class SpectrumLayer(YowInterfaceLayer): self.updateRoster() - self.call("ready") - self.call("group_getGroups", ("participating",)) - @ProtocolEntityCallback("failed") def onAuthFailed(self, entity): self.logger.info("Auth failed: %s (%s)", self.user, entity.getReason()) @@ -458,7 +456,6 @@ class SpectrumLayer(YowInterfaceLayer): for number in remove: self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE) self.backend.handleBuddyRemoved(self.user, number) - self.broadcastEvent(YowNetworkLayer) entity = UnsubscribePresenceProtocolEntity(number + "@s.whatsapp.net") self.toLower(entity) From b651ba8c3776eb877a6acca4d7c3e35ee487c741 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 17:33:08 +0200 Subject: [PATCH 10/51] Stop Start-Event from propogating --- session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/session.py b/session.py index 9f37c01..d0eebeb 100644 --- a/session.py +++ b/session.py @@ -424,6 +424,8 @@ class SpectrumLayer(YowInterfaceLayer): self.buddies = BuddyList(self.legacyName, self.db) self.bot = Bot(self) + return True + return False @ProtocolEntityCallback("success") def onAuthSuccess(self, entity): @@ -434,7 +436,7 @@ class SpectrumLayer(YowInterfaceLayer): self.updateRoster() - @ProtocolEntityCallback("failed") + @ProtocolEntityCallback("failure") def onAuthFailed(self, entity): self.logger.info("Auth failed: %s (%s)", self.user, entity.getReason()) self.backend.handleDisconnected(self.user, 0, reason) From 9e35017523d8bde5411ceb5267c4f42a40a215be Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 17:35:32 +0200 Subject: [PATCH 11/51] Fix bug --- session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session.py b/session.py index d0eebeb..80ede3a 100644 --- a/session.py +++ b/session.py @@ -439,7 +439,7 @@ class SpectrumLayer(YowInterfaceLayer): @ProtocolEntityCallback("failure") def onAuthFailed(self, entity): self.logger.info("Auth failed: %s (%s)", self.user, entity.getReason()) - self.backend.handleDisconnected(self.user, 0, reason) + self.backend.handleDisconnected(self.user, 0, entity.getReason()) self.password = None def updateRoster(self): From 467cc80b0d8359ecbeae682126f92b25809bc278 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 17:40:34 +0200 Subject: [PATCH 12/51] Catch AuthError When authentication fails, yowsup throws and AuthError. --- session.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/session.py b/session.py index 80ede3a..0d09706 100644 --- a/session.py +++ b/session.py @@ -122,7 +122,11 @@ class Session(): env.CURRENT_ENV.getResource()) self.stack.broadcastEvent( YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) - self.stack.loop() + try: + self.stack.loop() + except AuthError as e: # For some reason Yowsup throws an exception + self.logger.debug("Auth error -> user: %s; details: %s;", + self.user, e) def updateRoomList(self): rooms = [] From c06bfac0a963a9300a18b45eeb4c0e26bd4e54ca Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 18:06:30 +0200 Subject: [PATCH 13/51] Add logging to IOChannel --- Spectrum2/iochannel.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Spectrum2/iochannel.py b/Spectrum2/iochannel.py index 0b5ffad..05ab3c0 100644 --- a/Spectrum2/iochannel.py +++ b/Spectrum2/iochannel.py @@ -1,8 +1,10 @@ import asyncore, socket +import logging class IOChannel(asyncore.dispatcher): def __init__(self, host, port, callback): asyncore.dispatcher.__init__(self) + self.logger = logging.getLogger(self.__class__.__name__) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((host, port)) @@ -12,6 +14,7 @@ class IOChannel(asyncore.dispatcher): def sendData(self, data): self.buffer += data + self.logger.debug("Added Data to buffer. len(buffer) == %d", len(self.buffer)) def handle_connect(self): pass @@ -26,6 +29,7 @@ class IOChannel(asyncore.dispatcher): def handle_write(self): sent = self.send(self.buffer) self.buffer = self.buffer[sent:] + self.logger.debug("Flush buffer. len(buffer) == %d", len(self.buffer)) def writable(self): return (len(self.buffer) > 0) From a496e019671dc800e0ae6d803742846ee4d7c8a5 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 18:40:27 +0200 Subject: [PATCH 14/51] Handle disconnect from Yowsup --- session.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/session.py b/session.py index 0d09706..b21b94e 100644 --- a/session.py +++ b/session.py @@ -419,7 +419,7 @@ class SpectrumLayer(YowInterfaceLayer): def onEvent(self, layerEvent): # We cannot use __init__, since it can take no arguments - if layerEvent.name == SpectrumLayer.EVENT_START: + if layerEvent.getName() == SpectrumLayer.EVENT_START: self.logger = logging.getLogger(self.__class__.__name__) self.backend = layerEvent.getArg("backend") self.user = layerEvent.getArg("user") @@ -429,6 +429,10 @@ class SpectrumLayer(YowInterfaceLayer): self.buddies = BuddyList(self.legacyName, self.db) self.bot = Bot(self) return True + elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: + reason = layerEvent.getArg("reason") + self.logger.info("Disconnected: %s (%s)", self.user, reason) + self.backend.handleDisconnected(sefl.user, 0, reason) return False @ProtocolEntityCallback("success") @@ -469,5 +473,3 @@ class SpectrumLayer(YowInterfaceLayer): buddy = self.buddies[number] entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") self.toLower(entity) - - From 99db6cce76f339e0a0e98d92b8144d5cdf24b04f Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 19:05:55 +0200 Subject: [PATCH 15/51] Disable googleclient Google client is causing problems. Disable it for now. --- googleclient.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/googleclient.py b/googleclient.py index 1454ce3..899bbd0 100644 --- a/googleclient.py +++ b/googleclient.py @@ -37,12 +37,12 @@ class GoogleClient(): def __init__(self): self.client = gdata.contacts.client.ContactsClient() - self.token = gdata.gauth.OAuth2Token( - client_id = GOOGLE_CLIENT_ID, - client_secret = GOOGLE_CLIENT_SECRET, - scope = 'https://www.google.com/m8/feeds/contacts', - user_agent = 'whatTrans' - ) + # self.token = gdata.gauth.OAuth2Token( + # client_id = GOOGLE_CLIENT_ID, + # client_secret = GOOGLE_CLIENT_SECRET, + # scope = 'https://www.google.com/m8/feeds/contacts', + # user_agent = 'whatTrans' + # ) def getTokenUrl(self, uri = 'urn:ietf:wg:oauth:2.0:oob'): return self.token.generate_authorize_url(redirect_uri=uri) From e9f40dbed94ac62410a43ad17359828676bd2348 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 19:24:57 +0200 Subject: [PATCH 16/51] Respond to presence updates --- session.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/session.py b/session.py index b21b94e..ef94444 100644 --- a/session.py +++ b/session.py @@ -43,6 +43,7 @@ from yowsup.layers.logger import YowLoggerLayer from yowsup.common import YowConstants from yowsup import env from yowsup.layers.protocol_presence import * +from yowsup.layers.protocol_presence.protocolentities import * from Spectrum2 import protocol_pb2 @@ -106,7 +107,7 @@ class Session(): def call(self, method, args = ()): args = [str(s) for s in args] self.logger.debug("%s(%s)", method, ", ".join(args)) -# self.frontend.methodInterface.call(method, args) + self.stack.broadcastEvent(YowLayerEvent(method, **args)) def logout(self): self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT)) @@ -209,7 +210,7 @@ class Session(): def changeStatusMessage(self, statusMessage): if (statusMessage != self.statusMessage) or (self.initialized == False): self.statusMessage = statusMessage - self.call("profile_setStatus", (statusMessage.encode("utf-8"),)) + self.call("profile_setStatus", message = statusMessage.encode("utf-8")) self.logger.info("Status message changed: %s", statusMessage) if self.initialized == False: @@ -419,6 +420,7 @@ class SpectrumLayer(YowInterfaceLayer): def onEvent(self, layerEvent): # We cannot use __init__, since it can take no arguments + retval = False if layerEvent.getName() == SpectrumLayer.EVENT_START: self.logger = logging.getLogger(self.__class__.__name__) self.backend = layerEvent.getArg("backend") @@ -428,12 +430,25 @@ class SpectrumLayer(YowInterfaceLayer): self.buddies = BuddyList(self.legacyName, self.db) self.bot = Bot(self) - return True + retval = True elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: reason = layerEvent.getArg("reason") self.logger.info("Disconnected: %s (%s)", self.user, reason) self.backend.handleDisconnected(sefl.user, 0, reason) - return False + elif layerEvent.getName() == 'presence_sendAvailable': + entity = AvailablePresenceProtocolEntity() + self.toLower(entity) + retval = True + elif layerEvent.getName() == 'presence_sendUnavailable': + entity = UnavailablePresenceProtocolEntity() + self.toLower(entity) + retval = True + elif layerEvent.getName() == 'profile_setStatus': + entity = PresenceProtocolEntity(name = layerEvent.getArg('message')) + self.toLower(entity) + retval = True + self.logger.debug("EVENT %s", layerEvent.getName()) + return retval @ProtocolEntityCallback("success") def onAuthSuccess(self, entity): From 97c3aba3ecb1593e0d11e1c85bb63812ea08a2f5 Mon Sep 17 00:00:00 2001 From: moyamo Date: Fri, 27 Feb 2015 19:28:36 +0200 Subject: [PATCH 17/51] Session.call takes in kwargs instead of args --- session.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/session.py b/session.py index ef94444..dd8db7c 100644 --- a/session.py +++ b/session.py @@ -104,10 +104,10 @@ class Session(): def __del__(self): # handleLogoutRequest self.logout() - def call(self, method, args = ()): - args = [str(s) for s in args] - self.logger.debug("%s(%s)", method, ", ".join(args)) - self.stack.broadcastEvent(YowLayerEvent(method, **args)) + def call(self, method, **kwargs): + self.logger.debug("%s(%s)", method, + ", ".join(str(k) + ': ' + str(v) for k, v in kwargs.items())) + self.stack.broadcastEvent(YowLayerEvent(method, **kwargs)) def logout(self): self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT)) From f40f077270fd6ea9ed5816e6136743e53216cac2 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 12:44:48 +0200 Subject: [PATCH 18/51] Comment out import contacts from Bot.py This is interfering with testing. --- bot.py | 108 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/bot.py b/bot.py index 5ef61e5..e7ed9ce 100644 --- a/bot.py +++ b/bot.py @@ -31,7 +31,7 @@ import os import utils from constants import * -from googleclient import GoogleClient +#from googleclient import GoogleClient from Yowsup.Contacts.contacts import WAContactsSyncRequest @@ -43,7 +43,7 @@ class Bot(): self.google = GoogleClient() self.commands = { - "import": self._import, +# "import": self._import, "help": self._help, "prune": self._prune, "welcome": self._welcome, @@ -79,40 +79,40 @@ class Bot(): def send(self, message): self.session.backend.handleMessage(self.session.user, self.name, message) - def __do_import(self, token): - # Google - google = self.google.getContacts(token) - self.send("%d buddies imported from google" % len(google)) - - result = { } - for number, name in google.iteritems(): - number = re.sub("[^0-9]", "", number) - number = number if number[0] == "0" else "+" + number - - result[number] = { 'nick': name, 'state': 0 } - - # WhatsApp - user = self.session.legacyName - password = self.session.password - sync = WAContactsSyncRequest(user, password, result.keys()) - whatsapp = sync.send()['c'] - - for w in whatsapp: - result[w['p']]['state'] = w['w'] - result[w['p']]['number'] = w['n'] - - self.send("%d buddies are using whatsapp" % len(filter(lambda w: w['w'], whatsapp))) - - for r in result.values(): - if r['nick']: - self.session.buddies.add( - number = r['number'], - nick = r['nick'], - groups = [u'Google'], - state = r['state'] - ) - - self.send("%d buddies imported" % len(whatsapp)) +# def __do_import(self, token): +# # Google +# google = self.google.getContacts(token) +# self.send("%d buddies imported from google" % len(google)) +# +# result = { } +# for number, name in google.iteritems(): +# number = re.sub("[^0-9]", "", number) +# number = number if number[0] == "0" else "+" + number +# +# result[number] = { 'nick': name, 'state': 0 } +# +# # WhatsApp +# user = self.session.legacyName +# password = self.session.password +# sync = WAContactsSyncRequest(user, password, result.keys()) +# whatsapp = sync.send()['c'] +# +# for w in whatsapp: +# result[w['p']]['state'] = w['w'] +# result[w['p']]['number'] = w['n'] +# +# self.send("%d buddies are using whatsapp" % len(filter(lambda w: w['w'], whatsapp))) +# +# for r in result.values(): +# if r['nick']: +# self.session.buddies.add( +# number = r['number'], +# nick = r['nick'], +# groups = [u'Google'], +# state = r['state'] +# ) +# +# self.send("%d buddies imported" % len(whatsapp)) def __get_token(self, filename, timeout = 30): file = open(filename, 'r') @@ -135,24 +135,24 @@ class Bot(): file.close() # commands - def _import(self, token = None): - if not token: - token_url = self.google.getTokenUrl("http://whatsapp.0l.de/auth.py") - auth_url = "http://whatsapp.0l.de/auth.py?number=%s&auth_url=%s" % (self.session.legacyName, urllib.quote(token_url)) - short_url = utils.shorten(auth_url) - self.send("please visit this url to auth: %s" % short_url) - - self.send("waiting for authorization...") - token = self.__get_token(TOKEN_FILE) - if token: - self.send("got token: %s" % token) - self.__do_import(token) - self.session.updateRoster() - else: - self.send("timeout! please use \"\\import [token]\"") - else: - self.__do_import(token) - self.session.updateRoster() +# def _import(self, token = None): +# if not token: +# token_url = self.google.getTokenUrl("http://whatsapp.0l.de/auth.py") +# auth_url = "http://whatsapp.0l.de/auth.py?number=%s&auth_url=%s" % (self.session.legacyName, urllib.quote(token_url)) +# short_url = utils.shorten(auth_url) +# self.send("please visit this url to auth: %s" % short_url) +# +# self.send("waiting for authorization...") +# token = self.__get_token(TOKEN_FILE) +# if token: +# self.send("got token: %s" % token) +# self.__do_import(token) +# self.session.updateRoster() +# else: +# self.send("timeout! please use \"\\import [token]\"") +# else: +# self.__do_import(token) +# self.session.updateRoster() def _sync(self): user = self.session.legacyName From 8d8711b586a71b4b483e10bb48ad4785f49865ee Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 13:40:15 +0200 Subject: [PATCH 19/51] Comment out google client --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index e7ed9ce..8712d91 100644 --- a/bot.py +++ b/bot.py @@ -40,7 +40,7 @@ class Bot(): self.session = session self.name = name - self.google = GoogleClient() + # self.google = GoogleClient() self.commands = { # "import": self._import, From 83f499b81d15a32a873ad1b0012d9f322566d864 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 14:01:05 +0200 Subject: [PATCH 20/51] Set the yowsup PATH_STORAGE --- transwhat.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/transwhat.py b/transwhat.py index 9cda4d9..75557ac 100755 --- a/transwhat.py +++ b/transwhat.py @@ -38,6 +38,7 @@ from Spectrum2.iochannel import IOChannel from whatsappbackend import WhatsAppBackend from constants import * +from yowsup.common import YowConstants # Arguments parser = argparse.ArgumentParser() @@ -46,9 +47,11 @@ parser.add_argument('--host', type=str, required=True) parser.add_argument('--port', type=int, required=True) parser.add_argument('--service.backend_id', metavar="ID", type=int, required=True) parser.add_argument('config', type=str) +parser.add_argument('-j', type=str, required=True) args, unknown = parser.parse_known_args() +YowConstatns.PATH_STORAGE='/var/lib/spectrum/' + args.j # Logging logging.basicConfig( \ format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s", \ From 0ecc89d270fa691cddd355c9be37acfbaab0bf25 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 15:09:56 +0200 Subject: [PATCH 21/51] Catch TypeError thrown when password incorrectly formatted --- session.py | 8 ++++++-- transwhat.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/session.py b/session.py index dd8db7c..75fbfe2 100644 --- a/session.py +++ b/session.py @@ -121,8 +121,12 @@ class Session(): YowConstants.DOMAIN) self.stack.setProp(YowCoderLayer.PROP_RESOURCE, env.CURRENT_ENV.getResource()) - self.stack.broadcastEvent( - YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) + try: + self.stack.broadcastEvent( + YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) + except TypeError as e: # Occurs when password is not correctly formated + self.logger.debug("Auth error -> user: %s; details: %s;", + self.user, e) try: self.stack.loop() except AuthError as e: # For some reason Yowsup throws an exception diff --git a/transwhat.py b/transwhat.py index 75557ac..25eb8e5 100755 --- a/transwhat.py +++ b/transwhat.py @@ -51,7 +51,7 @@ parser.add_argument('-j', type=str, required=True) args, unknown = parser.parse_known_args() -YowConstatns.PATH_STORAGE='/var/lib/spectrum/' + args.j +YowConstants.PATH_STORAGE='/var/lib/spectrum2/' + args.j # Logging logging.basicConfig( \ format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s", \ From e94aa383bf58c945e0d88b0cab544111c00958ba Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 15:20:22 +0200 Subject: [PATCH 22/51] Remove redundent debug logs --- Spectrum2/iochannel.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Spectrum2/iochannel.py b/Spectrum2/iochannel.py index 05ab3c0..2d142c2 100644 --- a/Spectrum2/iochannel.py +++ b/Spectrum2/iochannel.py @@ -4,7 +4,6 @@ import logging class IOChannel(asyncore.dispatcher): def __init__(self, host, port, callback): asyncore.dispatcher.__init__(self) - self.logger = logging.getLogger(self.__class__.__name__) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((host, port)) @@ -14,7 +13,6 @@ class IOChannel(asyncore.dispatcher): def sendData(self, data): self.buffer += data - self.logger.debug("Added Data to buffer. len(buffer) == %d", len(self.buffer)) def handle_connect(self): pass @@ -29,7 +27,6 @@ class IOChannel(asyncore.dispatcher): def handle_write(self): sent = self.send(self.buffer) self.buffer = self.buffer[sent:] - self.logger.debug("Flush buffer. len(buffer) == %d", len(self.buffer)) def writable(self): return (len(self.buffer) > 0) From 0fd33d38bba0fb349ef080a244f8ed128814a608 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 15:32:58 +0200 Subject: [PATCH 23/51] Add ability to send a message --- session.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/session.py b/session.py index 75fbfe2..6ee5649 100644 --- a/session.py +++ b/session.py @@ -44,6 +44,7 @@ from yowsup.common import YowConstants from yowsup import env from yowsup.layers.protocol_presence import * from yowsup.layers.protocol_presence.protocolentities import * +from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity from Spectrum2 import protocol_pb2 @@ -173,7 +174,7 @@ class Session(): self.presenceRequested.append(buddy) self.call("presence_request", (buddy + "@s.whatsapp.net",)) else: - self.call("message_send", (buddy + "@s.whatsapp.net", message)) + self.call("message_send", to=buddy + "@s.whatsapp.net", message=message) def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): if timestamp: @@ -451,6 +452,11 @@ class SpectrumLayer(YowInterfaceLayer): entity = PresenceProtocolEntity(name = layerEvent.getArg('message')) self.toLower(entity) retval = True + elif layerEvent.getName() == 'message_send': + to = layerEvent.getArg('to') + message = layerEvent.getMessage('message') + messageEntity = TextMessageProtocolEntity(message, to = to) + self.toLower(messageEntity) self.logger.debug("EVENT %s", layerEvent.getName()) return retval From 68f9ca43ea56e3ac895859a647a97d7b79dd8573 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 15:56:09 +0200 Subject: [PATCH 24/51] Add ability to receive whatsapp messages --- session.py | 54 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/session.py b/session.py index 6ee5649..99717d6 100644 --- a/session.py +++ b/session.py @@ -176,17 +176,6 @@ class Session(): else: self.call("message_send", to=buddy + "@s.whatsapp.net", message=message) - def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): - if timestamp: - timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) - - if self.initialized == False: - self.logger.debug("Message queued from %s to %s: %s", buddy, self.legacyName, messageContent) - self.offlineQueue.append((buddy, messageContent, timestamp)) - else: - self.logger.debug("Message sent from %s to %s: %s", buddy, self.legacyName, messageContent) - self.backend.handleMessage(self.user, buddy, messageContent, "", "", timestamp) - def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""): if timestamp: timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) @@ -258,18 +247,6 @@ class Session(): self.logger.info("Disconnected from whatsapp: %s (%s)", self.legacyName, reason) self.backend.handleDisconnected(self.user, 0, reason) - def onMessageReceived(self, messageId, jid, messageContent, timestamp, receiptRequested, pushName, isBroadCast): - buddy = jid.split("@")[0] - messageContent = utils.softToUni(messageContent) - - if isBroadCast: - self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp) - messageContent = "[Broadcast] " + messageContent - else: - self.logger.info("Message received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp) - - self.sendMessageToXMPP(buddy, messageContent, timestamp) - if receiptRequested: self.call("message_ack", (jid, messageId)) def onMediaReceived(self, messageId, jid, preview, url, size, receiptRequested, isBroadcast): buddy = jid.split("@")[0] @@ -454,7 +431,7 @@ class SpectrumLayer(YowInterfaceLayer): retval = True elif layerEvent.getName() == 'message_send': to = layerEvent.getArg('to') - message = layerEvent.getMessage('message') + message = layerEvent.getArg('message') messageEntity = TextMessageProtocolEntity(message, to = to) self.toLower(messageEntity) self.logger.debug("EVENT %s", layerEvent.getName()) @@ -498,3 +475,32 @@ class SpectrumLayer(YowInterfaceLayer): buddy = self.buddies[number] entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") self.toLower(entity) + + @ProtocolEntityCallback("message") + def onMessageReceived(self, messageEntity): + buddy = messageEntity.getFrom() + messageContent = utils.softToUni(messageEntity.getBody()) + timestamp = messageEntity.getTimestamp() + + if messageEntity.isBroadCast(): + self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\ + buddy, self.legacyName, messageContent, timestamp) + messageContent = "[Broadcast] " + messageContent + else: + self.logger.info("Message received from %s to %s: %s (at ts=%s)", + buddy, self.legacyName, messageContent, timestamp) + + self.sendMessageToXMPP(buddy, messageContent, timestamp) + # if receiptRequested: self.call("message_ack", (jid, messageId)) + + def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): + if timestamp: + timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) + + if self.initialized == False: + self.logger.debug("Message queued from %s to %s: %s", buddy, self.legacyName, messageContent) + self.offlineQueue.append((buddy, messageContent, timestamp)) + else: + self.logger.debug("Message sent from %s to %s: %s", buddy, self.legacyName, messageContent) + self.backend.handleMessage(self.user, buddy, messageContent, "", "", timestamp) + From ce1bf5d35b84c60f5130a7b2fde3244ccbe98f9b Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 16:01:23 +0200 Subject: [PATCH 25/51] Fix typo --- session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session.py b/session.py index 99717d6..d7951b4 100644 --- a/session.py +++ b/session.py @@ -482,7 +482,7 @@ class SpectrumLayer(YowInterfaceLayer): messageContent = utils.softToUni(messageEntity.getBody()) timestamp = messageEntity.getTimestamp() - if messageEntity.isBroadCast(): + if messageEntity.isBroadcast(): self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\ buddy, self.legacyName, messageContent, timestamp) messageContent = "[Broadcast] " + messageContent From 0c17a497ee1b4a6afa297a06c6c80edfa0b9f0cf Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 16:11:43 +0200 Subject: [PATCH 26/51] Pass session into SpectrumLayer so that session specific things can be called --- session.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/session.py b/session.py index d7951b4..d2fcc56 100644 --- a/session.py +++ b/session.py @@ -98,7 +98,8 @@ class Session(): backend = self.backend, user = self.user, db = self.db, - legacyName = self.legacyName + legacyName = self.legacyName, + session = self ) ) @@ -176,6 +177,20 @@ class Session(): else: self.call("message_send", to=buddy + "@s.whatsapp.net", message=message) + def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): + if timestamp: + timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) + + if self.initialized == False: + self.logger.debug("Message queued from %s to %s: %s", + buddy, self.legacyName, messageContent) + self.offlineQueue.append((buddy, messageContent, timestamp)) + else: + self.logger.debug("Message sent from %s to %s: %s", buddy, + self.legacyName, messageContent) + self.backend.handleMessage(self.user, buddy, messageContent, "", + "", timestamp) + def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""): if timestamp: timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) @@ -409,6 +424,7 @@ class SpectrumLayer(YowInterfaceLayer): self.user = layerEvent.getArg("user") self.legacyName = layerEvent.getArg("legacyName") self.db = layerEvent.getArg("db") + self.session = layerEvent.getArg("session") self.buddies = BuddyList(self.legacyName, self.db) self.bot = Bot(self) @@ -490,17 +506,5 @@ class SpectrumLayer(YowInterfaceLayer): self.logger.info("Message received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp) - self.sendMessageToXMPP(buddy, messageContent, timestamp) + self.session.sendMessageToXMPP(buddy, messageContent, timestamp) # if receiptRequested: self.call("message_ack", (jid, messageId)) - - def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): - if timestamp: - timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) - - if self.initialized == False: - self.logger.debug("Message queued from %s to %s: %s", buddy, self.legacyName, messageContent) - self.offlineQueue.append((buddy, messageContent, timestamp)) - else: - self.logger.debug("Message sent from %s to %s: %s", buddy, self.legacyName, messageContent) - self.backend.handleMessage(self.user, buddy, messageContent, "", "", timestamp) - From 6b1b714d3c3ddedca36feb0aa157efcad4b9839f Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 16:16:26 +0200 Subject: [PATCH 27/51] Remove the domain from the buddy name before message is sent --- session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session.py b/session.py index d2fcc56..5214cf9 100644 --- a/session.py +++ b/session.py @@ -494,7 +494,7 @@ class SpectrumLayer(YowInterfaceLayer): @ProtocolEntityCallback("message") def onMessageReceived(self, messageEntity): - buddy = messageEntity.getFrom() + buddy = messageEntity.getFrom().split('@')[0] messageContent = utils.softToUni(messageEntity.getBody()) timestamp = messageEntity.getTimestamp() From 926aa9a6c9eeed6fc5f850cbfcea4bb5a4128711 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 16:31:01 +0200 Subject: [PATCH 28/51] Send typing state updates --- session.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/session.py b/session.py index 5214cf9..d06ca91 100644 --- a/session.py +++ b/session.py @@ -45,6 +45,7 @@ from yowsup import env from yowsup.layers.protocol_presence import * from yowsup.layers.protocol_presence.protocolentities import * from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity +from yowsup.layers.protocol_chatstate.protocolentities import * from Spectrum2 import protocol_pb2 @@ -146,12 +147,12 @@ class Session(): def sendTypingStarted(self, buddy): if buddy != "bot": self.logger.info("Started typing: %s to %s", self.legacyName, buddy) - self.call("typing_send", (buddy + "@s.whatsapp.net",)) + self.call("typing_send", buddy = (buddy + "@s.whatsapp.net",)) def sendTypingStopped(self, buddy): if buddy != "bot": self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy) - self.call("typing_paused", (buddy + "@s.whatsapp.net",)) + self.call("typing_paused", buddy = (buddy + "@s.whatsapp.net",)) def sendMessageToWA(self, sender, message): self.logger.info("Message sent from %s to %s: %s", self.legacyName, sender, message) @@ -450,6 +451,19 @@ class SpectrumLayer(YowInterfaceLayer): message = layerEvent.getArg('message') messageEntity = TextMessageProtocolEntity(message, to = to) self.toLower(messageEntity) + elif layerEvent.getName() == 'typing_send': + buddy = layerEvent.getArg('buddy') + state = OutgoingChatstateProtocolEntity( + ChatstateProtocolEntity.STATE_TYPING, buddy + ) + self.toLower(state) + elif layerEvent.getName() == 'typing_pause': + buddy = layerEvent.getArg('buddy') + state = OutgoingChatstateProtocolEntity( + ChatstateProtocolEntity.STATE_PAUSE, buddy + ) + self.toLower(state) + self.logger.debug("EVENT %s", layerEvent.getName()) return retval From ebed8039842a22850672fd3e9adfbf1b3cc1b802 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 16:44:20 +0200 Subject: [PATCH 29/51] Use YowStackBuilder and the default stack --- session.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/session.py b/session.py index d06ca91..c2bc548 100644 --- a/session.py +++ b/session.py @@ -27,7 +27,7 @@ import logging import urllib import time -from yowsup.stacks import YowStack +from yowsup.stacks import YowStack, YowStackBuilder from yowsup.layers import YowLayerEvent, YowParallelLayer from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, @@ -82,18 +82,10 @@ class Session(): self.bot = Bot(self) env.CURRENT_ENV = env.S40YowsupEnv() - layers = (SpectrumLayer, - YowParallelLayer((YowAuthenticationProtocolLayer, - YowMessagesProtocolLayer, - YowReceiptProtocolLayer, - YowAckProtocolLayer, - YowMediaProtocolLayer)), - YowCoderLayer, - YowCryptLayer, - YowStanzaRegulator, - YowNetworkLayer - ) - self.stack = YowStack(layers) + self.stack = YowStackBuilder()\ + .pushDefaultLayers(False)\ + .push(SpectrumLayer)\ + .build() self.stack.broadcastEvent( YowLayerEvent(SpectrumLayer.EVENT_START, backend = self.backend, @@ -163,13 +155,14 @@ class Session(): elif "-" in sender: # group msg if "/" in sender: room, buddy = sender.split("/") - self.call("message_send", (buddy + "@s.whatsapp.net", message)) + self.call("message_send", to = buddy + "@s.whatsapp.net", + message = message) else: room = sender group = self.groups[room] self.backend.handleMessage(self.user, room, message, group.nick) - self.call("message_send", (room + "@g.us", message)) + self.call("message_send", to = room + "@g.us", message = message) else: # private msg buddy = sender if message == "\\lastseen": @@ -451,18 +444,21 @@ class SpectrumLayer(YowInterfaceLayer): message = layerEvent.getArg('message') messageEntity = TextMessageProtocolEntity(message, to = to) self.toLower(messageEntity) + retval = True elif layerEvent.getName() == 'typing_send': buddy = layerEvent.getArg('buddy') state = OutgoingChatstateProtocolEntity( ChatstateProtocolEntity.STATE_TYPING, buddy ) self.toLower(state) - elif layerEvent.getName() == 'typing_pause': + retval = True + elif layerEvent.getName() == 'typing_paused': buddy = layerEvent.getArg('buddy') state = OutgoingChatstateProtocolEntity( ChatstateProtocolEntity.STATE_PAUSE, buddy ) self.toLower(state) + retval = True self.logger.debug("EVENT %s", layerEvent.getName()) return retval From 31bbab2ab16bc1efa6bc3e059c46ef308ec7bcfb Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 16:59:09 +0200 Subject: [PATCH 30/51] Remove default layers, The logging layer causes problems --- session.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/session.py b/session.py index c2bc548..97f7a8c 100644 --- a/session.py +++ b/session.py @@ -27,7 +27,7 @@ import logging import urllib import time -from yowsup.stacks import YowStack, YowStackBuilder +from yowsup.stacks import YowStack from yowsup.layers import YowLayerEvent, YowParallelLayer from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, @@ -82,10 +82,18 @@ class Session(): self.bot = Bot(self) env.CURRENT_ENV = env.S40YowsupEnv() - self.stack = YowStackBuilder()\ - .pushDefaultLayers(False)\ - .push(SpectrumLayer)\ - .build() + layers = (SpectrumLayer, + YowParallelLayer((YowAuthenticationProtocolLayer, + YowMessagesProtocolLayer, + YowReceiptProtocolLayer, + YowAckProtocolLayer, + YowMediaProtocolLayer)), + YowCoderLayer, + YowCryptLayer, + YowStanzaRegulator, + YowNetworkLayer + ) + self.stack = YowStack(layers) self.stack.broadcastEvent( YowLayerEvent(SpectrumLayer.EVENT_START, backend = self.backend, From 8b6a12c7985951d98ed40891190dcfe8d123d24f Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 17:02:58 +0200 Subject: [PATCH 31/51] Add YowIqProtocolLayer to prevent connection from timing out --- session.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/session.py b/session.py index 97f7a8c..f21c607 100644 --- a/session.py +++ b/session.py @@ -32,6 +32,7 @@ from yowsup.layers import YowLayerEvent, YowParallelLayer from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, AuthError) +from yowsup.layers.protocol_iq import YowIqProtocolLayer from yowsup.layers.coder import YowCoderLayer from yowsup.layers.network import YowNetworkLayer from yowsup.layers.protocol_messages import YowMessagesProtocolLayer @@ -46,7 +47,6 @@ from yowsup.layers.protocol_presence import * from yowsup.layers.protocol_presence.protocolentities import * from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity from yowsup.layers.protocol_chatstate.protocolentities import * - from Spectrum2 import protocol_pb2 from buddy import BuddyList @@ -87,7 +87,7 @@ class Session(): YowMessagesProtocolLayer, YowReceiptProtocolLayer, YowAckProtocolLayer, - YowMediaProtocolLayer)), + YowMediaProtocolLayer, YowIqProtocolLayer)), YowCoderLayer, YowCryptLayer, YowStanzaRegulator, @@ -131,7 +131,7 @@ class Session(): self.logger.debug("Auth error -> user: %s; details: %s;", self.user, e) try: - self.stack.loop() + self.stack.loop(timeout=0.5, discrete=0.5) except AuthError as e: # For some reason Yowsup throws an exception self.logger.debug("Auth error -> user: %s; details: %s;", self.user, e) From 85c320e24c040f9aca9a18c9ab895210b4edda0d Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 17:16:04 +0200 Subject: [PATCH 32/51] Send presence requests --- session.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/session.py b/session.py index f21c607..3e5f122 100644 --- a/session.py +++ b/session.py @@ -175,7 +175,7 @@ class Session(): buddy = sender if message == "\\lastseen": self.presenceRequested.append(buddy) - self.call("presence_request", (buddy + "@s.whatsapp.net",)) + self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",)) else: self.call("message_send", to=buddy + "@s.whatsapp.net", message=message) @@ -260,11 +260,6 @@ class Session(): else: self.logger.warn("Room doesn't exist: %s", room) - def onDisconnected(self, reason): - self.logger.info("Disconnected from whatsapp: %s (%s)", self.legacyName, reason) - self.backend.handleDisconnected(self.user, 0, reason) - - def onMediaReceived(self, messageId, jid, preview, url, size, receiptRequested, isBroadcast): buddy = jid.split("@")[0] @@ -434,7 +429,7 @@ class SpectrumLayer(YowInterfaceLayer): elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: reason = layerEvent.getArg("reason") self.logger.info("Disconnected: %s (%s)", self.user, reason) - self.backend.handleDisconnected(sefl.user, 0, reason) + self.backend.handleDisconnected(self.user, 0, reason) elif layerEvent.getName() == 'presence_sendAvailable': entity = AvailablePresenceProtocolEntity() self.toLower(entity) @@ -467,10 +462,15 @@ class SpectrumLayer(YowInterfaceLayer): ) self.toLower(state) retval = True + elif layerEvent.getName() == 'presence_request': + buddy = layerEvent.getArg('buddy') + sub = SubscribePresenceProtocolEntity(buddy) + self.toLower(sub) self.logger.debug("EVENT %s", layerEvent.getName()) return retval + @ProtocolEntityCallback("success") def onAuthSuccess(self, entity): self.logger.info("Auth success: %s", self.user) From af0150866db34f71c5ed004de9678f539a321440 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 18:20:09 +0200 Subject: [PATCH 33/51] Add groups protocol layer --- session.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/session.py b/session.py index 3e5f122..774b1b7 100644 --- a/session.py +++ b/session.py @@ -32,7 +32,8 @@ from yowsup.layers import YowLayerEvent, YowParallelLayer from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, AuthError) -from yowsup.layers.protocol_iq import YowIqProtocolLayer +from yowsup.layers.protocol_iq import YowIqProtocolLayer +from yowsup.layers.protocol_groups import YowGroupsProtocolLayer from yowsup.layers.coder import YowCoderLayer from yowsup.layers.network import YowNetworkLayer from yowsup.layers.protocol_messages import YowMessagesProtocolLayer @@ -87,7 +88,7 @@ class Session(): YowMessagesProtocolLayer, YowReceiptProtocolLayer, YowAckProtocolLayer, - YowMediaProtocolLayer, YowIqProtocolLayer)), + YowMediaProtocolLayer, YowIqProtocolLayer, YowGroupsProtocolLayer)), YowCoderLayer, YowCryptLayer, YowStanzaRegulator, @@ -124,6 +125,7 @@ class Session(): YowConstants.DOMAIN) self.stack.setProp(YowCoderLayer.PROP_RESOURCE, env.CURRENT_ENV.getResource()) + self.stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 5) try: self.stack.broadcastEvent( YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) From f0a19d1b6cdc615082981d55b5177117e8843d8c Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 19:02:39 +0200 Subject: [PATCH 34/51] Handle presence sent from whatsapp --- session.py | 74 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/session.py b/session.py index 774b1b7..dd465bc 100644 --- a/session.py +++ b/session.py @@ -297,41 +297,6 @@ class Session(): self.backend.handleBuddyTyped(self.user, jid.split("@")[0]) self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, (self.user, buddy)).start() - def onPrecenceUpdated(self, jid, lastseen): - buddy = jid.split("@")[0] - self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen)) - - if buddy in self.presenceRequested: - timestamp = time.localtime(time.time() - lastseen) - timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp) - self.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen))) - self.presenceRequested.remove(buddy) - - if lastseen < 60: - self.onPrecenceAvailable(jid) - else: - self.onPrecenceUnavailable(jid) - - def onPrecenceAvailable(self, jid): - buddy = jid.split("@")[0] - - try: - buddy = self.buddies[buddy] - self.logger.info("Is available: %s", buddy) - self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE) - except KeyError: - self.logger.error("Buddy not found: %s", buddy) - - def onPrecenceUnavailable(self, jid): - buddy = jid.split("@")[0] - - try: - buddy = self.buddies[buddy] - self.logger.info("Is unavailable: %s", buddy) - self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA) - except KeyError: - self.logger.error("Buddy not found: %s", buddy) - def onGroupGotInfo(self, gjid, owner, subject, subjectOwner, subjectTimestamp, creationTimestamp): room = gjid.split("@")[0] owner = owner.split("@")[0] @@ -528,3 +493,42 @@ class SpectrumLayer(YowInterfaceLayer): self.session.sendMessageToXMPP(buddy, messageContent, timestamp) # if receiptRequested: self.call("message_ack", (jid, messageId)) + + @ProtocolEntityCallback("presence") + def onPrecenceUpdated(self, presence): + jid = presence.getFrom() + lastseen = presence.getLast() + buddy = jid.split("@")[0] + self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen)) + + if buddy in self.session.presenceRequested: + timestamp = time.localtime(time.time() - lastseen) + timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp) + self.session.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen))) + self.session.presenceRequested.remove(buddy) + + if lastseen < 60: + self.onPrecenceAvailable(jid) + else: + self.onPrecenceUnavailable(jid) + + def onPrecenceAvailable(self, jid): + buddy = jid.split("@")[0] + + try: + buddy = self.buddies[buddy] + self.logger.info("Is available: %s", buddy) + self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE) + except KeyError: + self.logger.error("Buddy not found: %s", buddy) + + def onPrecenceUnavailable(self, jid): + buddy = jid.split("@")[0] + + try: + buddy = self.buddies[buddy] + self.logger.info("Is unavailable: %s", buddy) + self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA) + except KeyError: + self.logger.error("Buddy not found: %s", buddy) + From f871ed664b64dc6a03d82ff73c448a1eeea8760e Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 19:05:28 +0200 Subject: [PATCH 35/51] Add PresenceProtocolEntity to YowStack --- session.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/session.py b/session.py index dd465bc..f78cc4c 100644 --- a/session.py +++ b/session.py @@ -88,7 +88,10 @@ class Session(): YowMessagesProtocolLayer, YowReceiptProtocolLayer, YowAckProtocolLayer, - YowMediaProtocolLayer, YowIqProtocolLayer, YowGroupsProtocolLayer)), + YowMediaProtocolLayer, + YowIqProtocolLayer, + YowGroupsProtocolLayer, + YowPresenceProtocolLayer)), YowCoderLayer, YowCryptLayer, YowStanzaRegulator, From ba54217377a60c778ee30b7c1259fad192437f34 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 21:11:23 +0200 Subject: [PATCH 36/51] Send logs to logfile --- transwhat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/transwhat.py b/transwhat.py index 25eb8e5..90703a4 100755 --- a/transwhat.py +++ b/transwhat.py @@ -54,6 +54,7 @@ args, unknown = parser.parse_known_args() YowConstants.PATH_STORAGE='/var/lib/spectrum2/' + args.j # Logging logging.basicConfig( \ + filename='/var/log/spectrum2/' + args.j + '/backends/backend.log',\ format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s", \ level = logging.DEBUG if args.debug else logging.INFO \ ) From f2cadb7492fd8a9fd06771641804f4b2274cf784 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 21 Jun 2015 21:24:36 +0200 Subject: [PATCH 37/51] Reconnect when disconnected from whatsapp --- session.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/session.py b/session.py index f78cc4c..8e30faa 100644 --- a/session.py +++ b/session.py @@ -78,7 +78,7 @@ class Session(): self.timer = None self.password = None self.initialized = False - + self.loggedin = False self.bot = Bot(self) @@ -117,6 +117,7 @@ class Session(): self.stack.broadcastEvent(YowLayerEvent(method, **kwargs)) def logout(self): + self.loggedin = False self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT)) def login(self, password): @@ -129,6 +130,8 @@ class Session(): self.stack.setProp(YowCoderLayer.PROP_RESOURCE, env.CURRENT_ENV.getResource()) self.stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 5) + self.loggedin = True + self.password = password try: self.stack.broadcastEvent( YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) @@ -399,7 +402,10 @@ class SpectrumLayer(YowInterfaceLayer): elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: reason = layerEvent.getArg("reason") self.logger.info("Disconnected: %s (%s)", self.user, reason) - self.backend.handleDisconnected(self.user, 0, reason) + if not self.session.loggedin or self.session.password is None: + self.backend.handleDisconnected(self.user, 0, reason) + else: + self.session.login(self.session.password) elif layerEvent.getName() == 'presence_sendAvailable': entity = AvailablePresenceProtocolEntity() self.toLower(entity) @@ -454,7 +460,7 @@ class SpectrumLayer(YowInterfaceLayer): def onAuthFailed(self, entity): self.logger.info("Auth failed: %s (%s)", self.user, entity.getReason()) self.backend.handleDisconnected(self.user, 0, entity.getReason()) - self.password = None + self.session.password = None def updateRoster(self): self.logger.debug("Update roster") From 2f73ed1a1098d0d22f44b68df8a9abd859a4714d Mon Sep 17 00:00:00 2001 From: moyamo Date: Sat, 29 Aug 2015 20:50:07 +0200 Subject: [PATCH 38/51] Send Acks and Receipts on receiving a message --- session.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/session.py b/session.py index 8e30faa..0b445c9 100644 --- a/session.py +++ b/session.py @@ -43,11 +43,13 @@ from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer from yowsup.layers.protocol_acks import YowAckProtocolLayer from yowsup.layers.logger import YowLoggerLayer from yowsup.common import YowConstants +from yowsup.layers.protocol_receipts.protocolentities import * from yowsup import env from yowsup.layers.protocol_presence import * from yowsup.layers.protocol_presence.protocolentities import * from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity from yowsup.layers.protocol_chatstate.protocolentities import * +from yowsup.layers.protocol_acks.protocolentities import * from Spectrum2 import protocol_pb2 from buddy import BuddyList @@ -415,7 +417,8 @@ class SpectrumLayer(YowInterfaceLayer): self.toLower(entity) retval = True elif layerEvent.getName() == 'profile_setStatus': - entity = PresenceProtocolEntity(name = layerEvent.getArg('message')) + # entity = PresenceProtocolEntity(name = layerEvent.getArg('message')) + entity = PresenceProtocolEntity(name = 'This status is non-empty') self.toLower(entity) retval = True elif layerEvent.getName() == 'message_send': @@ -453,6 +456,7 @@ class SpectrumLayer(YowInterfaceLayer): self.backend.handleConnected(self.user) self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE) + self.session.initialized = True self.updateRoster() @@ -462,6 +466,20 @@ class SpectrumLayer(YowInterfaceLayer): self.backend.handleDisconnected(self.user, 0, entity.getReason()) self.session.password = None + @ProtocolEntityCallback("receipt") + def onReceipt(self, entity): + self.logger.debug("received receipt, sending ack: " + str(entity.ack())) + ack = OutgoingAckProtocolEntity(entity.getId(), "receipt", entity.getType(), entity.getFrom()) + self.toLower(ack) + + @ProtocolEntityCallback("ack") + def onAck(self, entity): + self.logger.debug('received ack ' + str(entity)) + + @ProtocolEntityCallback("notification") + def onNotification(self, notification): + self.toLower(notification.ack()) + def updateRoster(self): self.logger.debug("Update roster") @@ -492,6 +510,9 @@ class SpectrumLayer(YowInterfaceLayer): messageContent = utils.softToUni(messageEntity.getBody()) timestamp = messageEntity.getTimestamp() + receipt = OutgoingReceiptProtocolEntity(messageEntity.getId(),messageEntity.getFrom(), 'read', messageEntity.getParticipant()) + self.toLower(receipt) + if messageEntity.isBroadcast(): self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\ buddy, self.legacyName, messageContent, timestamp) @@ -499,8 +520,9 @@ class SpectrumLayer(YowInterfaceLayer): else: self.logger.info("Message received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp) + self.session.sendMessageToXMPP(buddy, messageContent, timestamp) + - self.session.sendMessageToXMPP(buddy, messageContent, timestamp) # if receiptRequested: self.call("message_ack", (jid, messageId)) @ProtocolEntityCallback("presence") From 59754848a7a52099ce1f7ec27b0e66a8d34d5129 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sat, 29 Aug 2015 22:01:44 +0200 Subject: [PATCH 39/51] Enable Axolotl encryption as it is mandatory. --- session.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/session.py b/session.py index 0b445c9..6758f67 100644 --- a/session.py +++ b/session.py @@ -30,6 +30,7 @@ import time from yowsup.stacks import YowStack from yowsup.layers import YowLayerEvent, YowParallelLayer from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback +from yowsup.layers.axolotl import YowAxolotlLayer from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, AuthError) from yowsup.layers.protocol_iq import YowIqProtocolLayer @@ -94,6 +95,7 @@ class Session(): YowIqProtocolLayer, YowGroupsProtocolLayer, YowPresenceProtocolLayer)), + YowAxolotlLayer, YowCoderLayer, YowCryptLayer, YowStanzaRegulator, @@ -530,7 +532,8 @@ class SpectrumLayer(YowInterfaceLayer): jid = presence.getFrom() lastseen = presence.getLast() buddy = jid.split("@")[0] - self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen)) +# seems to be causing an error +# self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen)) if buddy in self.session.presenceRequested: timestamp = time.localtime(time.time() - lastseen) From c7afff2cf995f3e10eafbf20545c543c680cb132 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 30 Aug 2015 19:16:53 +0200 Subject: [PATCH 40/51] Add more protocol layers to the yowstack --- session.py | 49 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/session.py b/session.py index 6758f67..ccbc2ac 100644 --- a/session.py +++ b/session.py @@ -51,6 +51,26 @@ from yowsup.layers.protocol_presence.protocolentities import * from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity from yowsup.layers.protocol_chatstate.protocolentities import * from yowsup.layers.protocol_acks.protocolentities import * +from yowsup.layers import YowLayer +from yowsup.layers.auth import YowCryptLayer, YowAuthenticationProtocolLayer +from yowsup.layers.coder import YowCoderLayer +from yowsup.layers.logger import YowLoggerLayer +from yowsup.layers.network import YowNetworkLayer +from yowsup.layers.protocol_messages import YowMessagesProtocolLayer +from yowsup.layers.stanzaregulator import YowStanzaRegulator +from yowsup.layers.protocol_media import YowMediaProtocolLayer +from yowsup.layers.protocol_acks import YowAckProtocolLayer +from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer +from yowsup.layers.protocol_groups import YowGroupsProtocolLayer +from yowsup.layers.protocol_presence import YowPresenceProtocolLayer +from yowsup.layers.protocol_ib import YowIbProtocolLayer +from yowsup.layers.protocol_notifications import YowNotificationsProtocolLayer +from yowsup.layers.protocol_iq import YowIqProtocolLayer +from yowsup.layers.protocol_contacts import YowContactsIqProtocolLayer +from yowsup.layers.protocol_chatstate import YowChatstateProtocolLayer +from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer +from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer +from yowsup.layers.protocol_calls import YowCallsProtocolLayer from Spectrum2 import protocol_pb2 from buddy import BuddyList @@ -92,7 +112,15 @@ class Session(): YowReceiptProtocolLayer, YowAckProtocolLayer, YowMediaProtocolLayer, + YowIbProtocolLayer, YowIqProtocolLayer, + YowNotificationsProtocolLayer, + YowContactsIqProtocolLayer, +# YowChatstateProtocolLayer, + YowCallsProtocolLayer, + YowMediaProtocolLayer, + YowPrivacyProtocolLayer, + YowProfilesProtocolLayer, YowGroupsProtocolLayer, YowPresenceProtocolLayer)), YowAxolotlLayer, @@ -400,16 +428,13 @@ class SpectrumLayer(YowInterfaceLayer): self.db = layerEvent.getArg("db") self.session = layerEvent.getArg("session") - self.buddies = BuddyList(self.legacyName, self.db) + self.session.buddies = BuddyList(self.legacyName, self.db) self.bot = Bot(self) retval = True elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: reason = layerEvent.getArg("reason") self.logger.info("Disconnected: %s (%s)", self.user, reason) - if not self.session.loggedin or self.session.password is None: - self.backend.handleDisconnected(self.user, 0, reason) - else: - self.session.login(self.session.password) + self.backend.handleDisconnected(self.user, 0, reason) elif layerEvent.getName() == 'presence_sendAvailable': entity = AvailablePresenceProtocolEntity() self.toLower(entity) @@ -439,7 +464,7 @@ class SpectrumLayer(YowInterfaceLayer): elif layerEvent.getName() == 'typing_paused': buddy = layerEvent.getArg('buddy') state = OutgoingChatstateProtocolEntity( - ChatstateProtocolEntity.STATE_PAUSE, buddy + ChatstateProtocolEntity.STATE_PAUSED, buddy ) self.toLower(state) retval = True @@ -485,9 +510,9 @@ class SpectrumLayer(YowInterfaceLayer): def updateRoster(self): self.logger.debug("Update roster") - old = self.buddies.keys() - self.buddies.load() - new = self.buddies.keys() + old = self.session.buddies.keys() + self.session.buddies.load() + new = self.session.buddies.keys() add = set(new) - set(old) remove = set(old) - set(new) @@ -502,7 +527,7 @@ class SpectrumLayer(YowInterfaceLayer): self.toLower(entity) for number in add: - buddy = self.buddies[number] + buddy = self.session.buddies[number] entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") self.toLower(entity) @@ -550,7 +575,7 @@ class SpectrumLayer(YowInterfaceLayer): buddy = jid.split("@")[0] try: - buddy = self.buddies[buddy] + buddy = self.session.buddies[buddy] self.logger.info("Is available: %s", buddy) self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE) except KeyError: @@ -560,7 +585,7 @@ class SpectrumLayer(YowInterfaceLayer): buddy = jid.split("@")[0] try: - buddy = self.buddies[buddy] + buddy = self.session.buddies[buddy] self.logger.info("Is unavailable: %s", buddy) self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA) except KeyError: From 8cc3c8514534bd38866f9eca62f8d5e7667fcf7f Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 30 Aug 2015 20:08:41 +0200 Subject: [PATCH 41/51] Add a yowsup wrapper to simplify the session.py code --- yowsup.py | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 yowsup.py diff --git a/yowsup.py b/yowsup.py new file mode 100644 index 0000000..4022417 --- /dev/null +++ b/yowsup.py @@ -0,0 +1,178 @@ + +from yowsup import env +from yowsup.stacks import YowStack + +# Layers +from yowsup.layers.auth import YowCryptLayer, YowAuthenticationProtocolLayer +from yowsup.layers.coder import YowCoderLayer +from yowsup.layers.logger import YowLoggerLayer +from yowsup.layers.network import YowNetworkLayer +from yowsup.layers.protocol_messages import YowMessagesProtocolLayer +from yowsup.layers.stanzaregulator import YowStanzaRegulator +from yowsup.layers.protocol_media import YowMediaProtocolLayer +from yowsup.layers.protocol_acks import YowAckProtocolLayer +from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer +from yowsup.layers.protocol_groups import YowGroupsProtocolLayer +from yowsup.layers.protocol_presence import YowPresenceProtocolLayer +from yowsup.layers.protocol_ib import YowIbProtocolLayer +from yowsup.layers.protocol_notifications import YowNotificationsProtocolLayer +from yowsup.layers.protocol_iq import YowIqProtocolLayer +from yowsup.layers.protocol_contacts import YowContactsIqProtocolLayer +from yowsup.layers.protocol_chatstate import YowChatstateProtocolLayer +from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer +from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer +from yowsup.layers.protocol_calls import YowCallsProtocolLayer + +class YowsupApp: + def __init__(self): + self.logged_in = False + + env.CURRENT_ENV = env.S40YowsupEnv() + + layers = (SpectrumLayer, + YowParallelLayer((YowAuthenticationProtocolLayer, + YowMessagesProtocolLayer, + YowReceiptProtocolLayer, + YowAckProtocolLayer, + YowMediaProtocolLayer, + YowIbProtocolLayer, + YowIqProtocolLayer, + YowNotificationsProtocolLayer, + YowContactsIqProtocolLayer, +# YowChatstateProtocolLayer, + YowCallsProtocolLayer, + YowMediaProtocolLayer, + YowPrivacyProtocolLayer, + YowProfilesProtocolLayer, + YowGroupsProtocolLayer, + YowPresenceProtocolLayer)), + YowAxolotlLayer, + YowCoderLayer, + YowCryptLayer, + YowStanzaRegulator, + YowNetworkLayer + ) + self.stack = YowStack(layers) + self.stack.broadcastEvent( + YowLayerEvent(YowsupAppLayer.EVENT_START, caller = self) + ) + + def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t): + """ + Called when login is successful. + + Args: + - status + - kind + - creation + - expiration + - props + - nonce + - t + """ + pass + + def onAuthFailure(self, reason): + """ + Called when login is a failure + + Args: + - reason: (str) Reason for the login failure + """ + pass + + def onReceipt(self, _id, _from, timestamp, type, participant, offline, items): + """ + Called when a receipt is received (double tick or blue tick) + + Args + - _id + - _from + - timestamp + - type: Is 'read' for blue ticks and None for double-ticks + - participant: (dxxxxxxxxxx@s.whatsapp.net) delivered to or + read by this participant in group + - offline: (True, False or None) + - items + """ + pass + + def onAck(self, _id,_class, _from, timestamp): + """ + Called when Ack is received + + Args: + - _id + - _class: ('message', 'receipt' or something else?) + - _from + - timestamp + """ + pass + +from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback + +class YowsupAppLayer(YowInterfaceLayer): + EVENT_START = 'transwhat.event.SpectrumLayer.start' + + def onEvent(self, layerEvent): + # We cannot pass instance varaibles in through init, so we use an event + # instead + # Return False if you want the event to propogate down the stack + # return True otherwise + if layerEvent.getName() == YowsupAppLayer.EVENT_START: + self.caller = layerEvent.getArg('caller') + + @ProtocolEntityCallback('success') + def onAuthSuccess(self, entity): + # entity is SuccessProtocolEntity + status = entity.status + kind = entity.kind + creation = entity.creation + expiration = entity.expiration + props = entity.props + nonce = entity.nonce + t = entity.t # I don't know what this is + self.caller.onAuthSuccess(status, kind, creation, expiration, props, nonce, t) + + @ProtocolEntityCallback('failure') + def onAuthFailure(self, entity): + # entity is FailureProtocolEntity + reason = entity.reason + self.caller.onAuthFailure(reason) + + @ProtocolEntityCallback('receipt') + def onReceipt(self, entity): + """Sends ack automatically""" + # entity is IncomingReceiptProtocolEntity + ack = OutgoingAckProtocolEntity(entity.getId(), + 'receipt', entity.getType(), entity.getFrom()) + self.toLower(ack) + _id = entity._id + _from = entity._from + timestamp = entity.timestamp + type = entity.type + participant = entity.participant + offline = entity.offline + items = entity.items + self.caller.onReceipt(_id, _from, timestamp, type, participant, offline, items) + + @ProtocolEntityCallback('ack') + def onAck(self, entity) + # entity is IncomingAckProtocolEntity + self.caller.onAck( + entity._id, + entity._class, + entity._from, + entity.timestamp + ) + + @ProtocolEntityCallback('notification') + def onNotification(self, entity): + """ + Sends ack automatically + """ + self.toLower(notification.ack()) + + @ProtocolEntityCallback("message") + def onMessageReceived(self, entity): + self.caller.onMessage(entity) From 254465301b366c0210b7a93c36aa58d33bdb5907 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 30 Aug 2015 20:51:44 +0200 Subject: [PATCH 42/51] Make Session use YowsupApp instead of SpectrumLayer --- session.py | 279 ++++++++++++++-------------------- yowsup.py => yowsupwrapper.py | 99 ++++++++++-- 2 files changed, 203 insertions(+), 175 deletions(-) rename yowsup.py => yowsupwrapper.py (64%) diff --git a/session.py b/session.py index ccbc2ac..d877078 100644 --- a/session.py +++ b/session.py @@ -44,32 +44,32 @@ from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer from yowsup.layers.protocol_acks import YowAckProtocolLayer from yowsup.layers.logger import YowLoggerLayer from yowsup.common import YowConstants -from yowsup.layers.protocol_receipts.protocolentities import * +from yowsup.layers.protocol_receipts.protocolentities import * from yowsup import env from yowsup.layers.protocol_presence import * from yowsup.layers.protocol_presence.protocolentities import * from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity from yowsup.layers.protocol_chatstate.protocolentities import * -from yowsup.layers.protocol_acks.protocolentities import * +from yowsup.layers.protocol_acks.protocolentities import * from yowsup.layers import YowLayer -from yowsup.layers.auth import YowCryptLayer, YowAuthenticationProtocolLayer -from yowsup.layers.coder import YowCoderLayer -from yowsup.layers.logger import YowLoggerLayer -from yowsup.layers.network import YowNetworkLayer -from yowsup.layers.protocol_messages import YowMessagesProtocolLayer -from yowsup.layers.stanzaregulator import YowStanzaRegulator -from yowsup.layers.protocol_media import YowMediaProtocolLayer -from yowsup.layers.protocol_acks import YowAckProtocolLayer -from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer -from yowsup.layers.protocol_groups import YowGroupsProtocolLayer -from yowsup.layers.protocol_presence import YowPresenceProtocolLayer -from yowsup.layers.protocol_ib import YowIbProtocolLayer -from yowsup.layers.protocol_notifications import YowNotificationsProtocolLayer -from yowsup.layers.protocol_iq import YowIqProtocolLayer -from yowsup.layers.protocol_contacts import YowContactsIqProtocolLayer -from yowsup.layers.protocol_chatstate import YowChatstateProtocolLayer -from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer -from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer +from yowsup.layers.auth import YowCryptLayer, YowAuthenticationProtocolLayer +from yowsup.layers.coder import YowCoderLayer +from yowsup.layers.logger import YowLoggerLayer +from yowsup.layers.network import YowNetworkLayer +from yowsup.layers.protocol_messages import YowMessagesProtocolLayer +from yowsup.layers.stanzaregulator import YowStanzaRegulator +from yowsup.layers.protocol_media import YowMediaProtocolLayer +from yowsup.layers.protocol_acks import YowAckProtocolLayer +from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer +from yowsup.layers.protocol_groups import YowGroupsProtocolLayer +from yowsup.layers.protocol_presence import YowPresenceProtocolLayer +from yowsup.layers.protocol_ib import YowIbProtocolLayer +from yowsup.layers.protocol_notifications import YowNotificationsProtocolLayer +from yowsup.layers.protocol_iq import YowIqProtocolLayer +from yowsup.layers.protocol_contacts import YowContactsIqProtocolLayer +from yowsup.layers.protocol_chatstate import YowChatstateProtocolLayer +from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer +from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer from yowsup.layers.protocol_calls import YowCallsProtocolLayer from Spectrum2 import protocol_pb2 @@ -78,8 +78,9 @@ from threading import Timer from group import Group from bot import Bot from constants import * +from yowsupwrapper import YowsupApp -class Session(): +class Session(YowsupApp): def __init__(self, backend, user, legacyName, extra, db): self.logger = logging.getLogger(self.__class__.__name__) @@ -105,76 +106,22 @@ class Session(): self.bot = Bot(self) - env.CURRENT_ENV = env.S40YowsupEnv() - layers = (SpectrumLayer, - YowParallelLayer((YowAuthenticationProtocolLayer, - YowMessagesProtocolLayer, - YowReceiptProtocolLayer, - YowAckProtocolLayer, - YowMediaProtocolLayer, - YowIbProtocolLayer, - YowIqProtocolLayer, - YowNotificationsProtocolLayer, - YowContactsIqProtocolLayer, -# YowChatstateProtocolLayer, - YowCallsProtocolLayer, - YowMediaProtocolLayer, - YowPrivacyProtocolLayer, - YowProfilesProtocolLayer, - YowGroupsProtocolLayer, - YowPresenceProtocolLayer)), - YowAxolotlLayer, - YowCoderLayer, - YowCryptLayer, - YowStanzaRegulator, - YowNetworkLayer - ) - self.stack = YowStack(layers) - self.stack.broadcastEvent( - YowLayerEvent(SpectrumLayer.EVENT_START, - backend = self.backend, - user = self.user, - db = self.db, - legacyName = self.legacyName, - session = self - ) - ) - def __del__(self): # handleLogoutRequest self.logout() def call(self, method, **kwargs): self.logger.debug("%s(%s)", method, ", ".join(str(k) + ': ' + str(v) for k, v in kwargs.items())) - self.stack.broadcastEvent(YowLayerEvent(method, **kwargs)) + ##self.stack.broadcastEvent(YowLayerEvent(method, **kwargs)) def logout(self): self.loggedin = False - self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT)) + super(Session, self).logout() def login(self, password): - self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, - (self.legacyName, password)) - self.stack.setProp(YowNetworkLayer.PROP_ENDPOINT, - YowConstants.ENDPOINTS[0]) - self.stack.setProp(YowCoderLayer.PROP_DOMAIN, - YowConstants.DOMAIN) - self.stack.setProp(YowCoderLayer.PROP_RESOURCE, - env.CURRENT_ENV.getResource()) - self.stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 5) self.loggedin = True self.password = password - try: - self.stack.broadcastEvent( - YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) - except TypeError as e: # Occurs when password is not correctly formated - self.logger.debug("Auth error -> user: %s; details: %s;", - self.user, e) - try: - self.stack.loop(timeout=0.5, discrete=0.5) - except AuthError as e: # For some reason Yowsup throws an exception - self.logger.debug("Auth error -> user: %s; details: %s;", - self.user, e) + super(Session, self).login(self.legacyName, self.password) def updateRoomList(self): rooms = [] @@ -183,6 +130,86 @@ class Session(): self.backend.handleRoomList(rooms) + def updateRoster(self): + self.logger.debug("Update roster") + + old = self.buddies.keys() + self.buddies.load() + new = self.buddies.keys() + + add = set(new) - set(old) + remove = set(old) - set(new) + + self.logger.debug("Roster remove: %s", str(list(remove))) + self.logger.debug("Roster add: %s", str(list(add))) + + for number in remove: + self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE) + self.backend.handleBuddyRemoved(self.user, number) + entity = UnsubscribePresenceProtocolEntity(number + "@s.whatsapp.net") + self.toLower(entity) + + for number in add: + buddy = self.buddies[number] + entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") + self.toLower(entity) + + # Called by superclass + def onAuthSuccess(self, status, kind, creation, + expiration, props, nonce, t): + self.logger.info("Auth success: %s", self.user) + + self.backend.handleConnected(self.user) + self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE) + self.initialized = True + + self.updateRoster() + + # Called by superclass + def onAuthFailed(self, reason): + self.logger.info("Auth failed: %s (%s)", self.user, reason) + self.backend.handleDisconnected(self.user, 0, reason) + self.password = None + + # Called by superclass + def onDisconnect(self): + self.backend.handleDisconnected(self.user, 0, 'Disconnected for unknown reasons') + self.loggedin = False + + # Called by superclass + def onReceipt(self, _id, _from, timestamp, type, participant, offline, items): + self.logger.debug("received receipt, sending ack: " + + ' '.join(map(str, [_id, _from, timestamp, + type, participant, offline, items])) + ) + + # Called by superclass + def onAck(self, _id,_class, _from, timestamp): + self.logger.debug('received ack ' + + ' '.join(map(str, [_id, _class, _from,timestamp,])) + ) + + # Called by superclass + def onMessageReceived(self, messageEntity): + self.logger.debug(str(messageEntity)) + buddy = messageEntity.getFrom().split('@')[0] + messageContent = utils.softToUni(messageEntity.getBody()) + timestamp = messageEntity.getTimestamp() + + self.sendReceipt(messageEntity.getId(), messageEntity.getFrom(), 'read', + messageEntity.getParticipant()) + + if messageEntity.isBroadcast(): + self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\ + buddy, self.legacyName, messageContent, timestamp) + messageContent = "[Broadcast] " + messageContent + else: + self.logger.info("Message received from %s to %s: %s (at ts=%s)", + buddy, self.legacyName, messageContent, timestamp) + self.session.sendMessageToXMPP(buddy, messageContent, timestamp) + + # if receiptRequested: self.call("message_ack", (jid, messageId)) + # spectrum RequestMethods def sendTypingStarted(self, buddy): if buddy != "bot": @@ -203,21 +230,21 @@ class Session(): elif "-" in sender: # group msg if "/" in sender: room, buddy = sender.split("/") - self.call("message_send", to = buddy + "@s.whatsapp.net", - message = message) + self.sendTextMessage(buddy + '@s.whatsapp.net', message) else: room = sender group = self.groups[room] self.backend.handleMessage(self.user, room, message, group.nick) - self.call("message_send", to = room + "@g.us", message = message) + self.sendTextMessage(room + '@g.us', message) + else: # private msg buddy = sender if message == "\\lastseen": self.presenceRequested.append(buddy) self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",)) else: - self.call("message_send", to=buddy + "@s.whatsapp.net", message=message) + self.sendTextMessage(buddy + '@s.whatsapp.net', message) def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): if timestamp: @@ -448,12 +475,12 @@ class SpectrumLayer(YowInterfaceLayer): entity = PresenceProtocolEntity(name = 'This status is non-empty') self.toLower(entity) retval = True - elif layerEvent.getName() == 'message_send': - to = layerEvent.getArg('to') - message = layerEvent.getArg('message') - messageEntity = TextMessageProtocolEntity(message, to = to) - self.toLower(messageEntity) - retval = True +# elif layerEvent.getName() == 'message_send': +# to = layerEvent.getArg('to') +# message = layerEvent.getArg('message') +# messageEntity = TextMessageProtocolEntity(message, to = to) +# self.toLower(messageEntity) +# retval = True elif layerEvent.getName() == 'typing_send': buddy = layerEvent.getArg('buddy') state = OutgoingChatstateProtocolEntity( @@ -476,82 +503,6 @@ class SpectrumLayer(YowInterfaceLayer): self.logger.debug("EVENT %s", layerEvent.getName()) return retval - - @ProtocolEntityCallback("success") - def onAuthSuccess(self, entity): - self.logger.info("Auth success: %s", self.user) - - self.backend.handleConnected(self.user) - self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE) - self.session.initialized = True - - self.updateRoster() - - @ProtocolEntityCallback("failure") - def onAuthFailed(self, entity): - self.logger.info("Auth failed: %s (%s)", self.user, entity.getReason()) - self.backend.handleDisconnected(self.user, 0, entity.getReason()) - self.session.password = None - - @ProtocolEntityCallback("receipt") - def onReceipt(self, entity): - self.logger.debug("received receipt, sending ack: " + str(entity.ack())) - ack = OutgoingAckProtocolEntity(entity.getId(), "receipt", entity.getType(), entity.getFrom()) - self.toLower(ack) - - @ProtocolEntityCallback("ack") - def onAck(self, entity): - self.logger.debug('received ack ' + str(entity)) - - @ProtocolEntityCallback("notification") - def onNotification(self, notification): - self.toLower(notification.ack()) - - def updateRoster(self): - self.logger.debug("Update roster") - - old = self.session.buddies.keys() - self.session.buddies.load() - new = self.session.buddies.keys() - - add = set(new) - set(old) - remove = set(old) - set(new) - - self.logger.debug("Roster remove: %s", str(list(remove))) - self.logger.debug("Roster add: %s", str(list(add))) - - for number in remove: - self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE) - self.backend.handleBuddyRemoved(self.user, number) - entity = UnsubscribePresenceProtocolEntity(number + "@s.whatsapp.net") - self.toLower(entity) - - for number in add: - buddy = self.session.buddies[number] - entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") - self.toLower(entity) - - @ProtocolEntityCallback("message") - def onMessageReceived(self, messageEntity): - buddy = messageEntity.getFrom().split('@')[0] - messageContent = utils.softToUni(messageEntity.getBody()) - timestamp = messageEntity.getTimestamp() - - receipt = OutgoingReceiptProtocolEntity(messageEntity.getId(),messageEntity.getFrom(), 'read', messageEntity.getParticipant()) - self.toLower(receipt) - - if messageEntity.isBroadcast(): - self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\ - buddy, self.legacyName, messageContent, timestamp) - messageContent = "[Broadcast] " + messageContent - else: - self.logger.info("Message received from %s to %s: %s (at ts=%s)", - buddy, self.legacyName, messageContent, timestamp) - self.session.sendMessageToXMPP(buddy, messageContent, timestamp) - - - # if receiptRequested: self.call("message_ack", (jid, messageId)) - @ProtocolEntityCallback("presence") def onPrecenceUpdated(self, presence): jid = presence.getFrom() diff --git a/yowsup.py b/yowsupwrapper.py similarity index 64% rename from yowsup.py rename to yowsupwrapper.py index 4022417..70aa049 100644 --- a/yowsup.py +++ b/yowsupwrapper.py @@ -1,6 +1,8 @@ from yowsup import env from yowsup.stacks import YowStack +from yowsup.common import YowConstants +from yowsup.layers import YowLayerEvent, YowParallelLayer # Layers from yowsup.layers.auth import YowCryptLayer, YowAuthenticationProtocolLayer @@ -23,10 +25,15 @@ from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer from yowsup.layers.protocol_calls import YowCallsProtocolLayer +# ProtocolEntities + +from yowsup.layers.protocol_presence.protocolentities import * +from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity +from yowsup.layers.protocol_chatstate.protocolentities import * +from yowsup.layers.protocol_acks.protocolentities import * + class YowsupApp: def __init__(self): - self.logged_in = False - env.CURRENT_ENV = env.S40YowsupEnv() layers = (SpectrumLayer, @@ -57,6 +64,67 @@ class YowsupApp: YowLayerEvent(YowsupAppLayer.EVENT_START, caller = self) ) + def login(self, username, password): + """Login to yowsup + + Should result in onAuthSuccess or onAuthFailure to be called. + + Args: + - username: (str) username in the form of 1239482382 (country code + and cellphone number) + + - password: (str) base64 encoded password + """ + self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, + (username, password)) + self.stack.setProp(YowNetworkLayer.PROP_ENDPOINT, + YowConstants.ENDPOINTS[0]) + self.stack.setProp(YowCoderLayer.PROP_DOMAIN, + YowConstants.DOMAIN) + self.stack.setProp(YowCoderLayer.PROP_RESOURCE, + env.CURRENT_ENV.getResource()) +# self.stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 5) + + try: + self.stack.broadcastEvent( + YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) + except TypeError as e: # Occurs when password is not correctly formated + self.onAuthFailure('password not base64 encoded') + try: + self.stack.loop(timeout=0.5, discrete=0.5) + except AuthError as e: # For some reason Yowsup throws an exception + self.onAuthFailure("%s" % e) + + def logout(self): + """ + Logout from whatsapp + """ + self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT)) + + def sendReceipt(self, _id, _from, read, participant): + """ + Send a receipt (delivered: double-tick, read: blue-ticks) + + Args: + - _id: id of message received + - _from + - read: ('read' or something else) + - participant + """ + receipt = OutgoingReceiptProtocolEntity(_id, _from, read, participant) + self.stack.toLower(receipt) + + def sendTextMessage(self, to, message): + """ + Sends a text message + + Args: + - to: (xxxxxxxxxx@s.whatsapp.net) who to send the message to + - message: (str) the body of the message + """ + messageEntity = TextMessageProtocolEntity(message, to = to) + self.stack.toLower(messageEntity) + def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t): """ Called when login is successful. @@ -109,6 +177,11 @@ class YowsupApp: """ pass + def onDisconnect(self): + """ + Called when disconnected from whatsapp + """ + from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback class YowsupAppLayer(YowInterfaceLayer): @@ -121,17 +194,21 @@ class YowsupAppLayer(YowInterfaceLayer): # return True otherwise if layerEvent.getName() == YowsupAppLayer.EVENT_START: self.caller = layerEvent.getArg('caller') + return True + elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: + self.caller.onDisconnect() + return True @ProtocolEntityCallback('success') def onAuthSuccess(self, entity): # entity is SuccessProtocolEntity - status = entity.status - kind = entity.kind - creation = entity.creation - expiration = entity.expiration - props = entity.props - nonce = entity.nonce - t = entity.t # I don't know what this is + status = entity.status + kind = entity.kind + creation = entity.creation + expiration = entity.expiration + props = entity.props + nonce = entity.nonce + t = entity.t # I don't know what this is self.caller.onAuthSuccess(status, kind, creation, expiration, props, nonce, t) @ProtocolEntityCallback('failure') @@ -150,8 +227,8 @@ class YowsupAppLayer(YowInterfaceLayer): _id = entity._id _from = entity._from timestamp = entity.timestamp - type = entity.type - participant = entity.participant + type = entity.type + participant = entity.participant offline = entity.offline items = entity.items self.caller.onReceipt(_id, _from, timestamp, type, participant, offline, items) From 47d52ae22cec253e3cba20d3fcb0720f94aa455e Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 30 Aug 2015 21:03:11 +0200 Subject: [PATCH 43/51] Use new-style classes and Session should call super.__init__ --- session.py | 1 + yowsupwrapper.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/session.py b/session.py index d877078..e7cd9b6 100644 --- a/session.py +++ b/session.py @@ -83,6 +83,7 @@ from yowsupwrapper import YowsupApp class Session(YowsupApp): def __init__(self, backend, user, legacyName, extra, db): + super(Session, self).__init__() self.logger = logging.getLogger(self.__class__.__name__) self.logger.info("Created: %s", legacyName) diff --git a/yowsupwrapper.py b/yowsupwrapper.py index 70aa049..bc7e592 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -32,7 +32,7 @@ from yowsup.layers.protocol_messages.protocolentities import TextMessageProtoco from yowsup.layers.protocol_chatstate.protocolentities import * from yowsup.layers.protocol_acks.protocolentities import * -class YowsupApp: +class YowsupApp(object): def __init__(self): env.CURRENT_ENV = env.S40YowsupEnv() @@ -234,7 +234,7 @@ class YowsupAppLayer(YowInterfaceLayer): self.caller.onReceipt(_id, _from, timestamp, type, participant, offline, items) @ProtocolEntityCallback('ack') - def onAck(self, entity) + def onAck(self, entity): # entity is IncomingAckProtocolEntity self.caller.onAck( entity._id, From 48313c54755a147e58f014fcb1bf69305166d5f0 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 30 Aug 2015 22:18:03 +0200 Subject: [PATCH 44/51] Minor fixes --- session.py | 44 +++++++++++++++++++++++--------------------- yowsupwrapper.py | 18 +++++++++++++++--- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/session.py b/session.py index e7cd9b6..8eec4a9 100644 --- a/session.py +++ b/session.py @@ -91,6 +91,8 @@ class Session(YowsupApp): self.backend = backend self.user = user self.legacyName = legacyName + self.buddies = BuddyList(self.legacyName, self.db) + self.bot = Bot(self) self.status = protocol_pb2.STATUS_NONE self.statusMessage = '' @@ -191,7 +193,7 @@ class Session(YowsupApp): ) # Called by superclass - def onMessageReceived(self, messageEntity): + def onMessage(self, messageEntity): self.logger.debug(str(messageEntity)) buddy = messageEntity.getFrom().split('@')[0] messageContent = utils.softToUni(messageEntity.getBody()) @@ -226,26 +228,26 @@ class Session(YowsupApp): self.logger.info("Message sent from %s to %s: %s", self.legacyName, sender, message) message = message.encode("utf-8") - if sender == "bot": - self.bot.parse(message) - elif "-" in sender: # group msg - if "/" in sender: - room, buddy = sender.split("/") - self.sendTextMessage(buddy + '@s.whatsapp.net', message) - else: - room = sender - group = self.groups[room] - - self.backend.handleMessage(self.user, room, message, group.nick) - self.sendTextMessage(room + '@g.us', message) - - else: # private msg - buddy = sender - if message == "\\lastseen": - self.presenceRequested.append(buddy) - self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",)) - else: - self.sendTextMessage(buddy + '@s.whatsapp.net', message) +# if sender == "bot": +# self.bot.parse(message) +# elif "-" in sender: # group msg +# if "/" in sender: +# room, buddy = sender.split("/") +# self.sendTextMessage(buddy + '@s.whatsapp.net', message) +# else: +# room = sender +# group = self.groups[room] +# +# self.backend.handleMessage(self.user, room, message, group.nick) +# self.sendTextMessage(room + '@g.us', message) +# +# else: # private msg +# buddy = sender +# if message == "\\lastseen": +# self.presenceRequested.append(buddy) +# self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",)) +# else: + self.sendTextMessage(sender + '@s.whatsapp.net', message) def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): if timestamp: diff --git a/yowsupwrapper.py b/yowsupwrapper.py index bc7e592..576fa31 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -3,8 +3,10 @@ from yowsup import env from yowsup.stacks import YowStack from yowsup.common import YowConstants from yowsup.layers import YowLayerEvent, YowParallelLayer +from yowsup.layers.auth import AuthError # Layers +from yowsup.layers.axolotl import YowAxolotlLayer from yowsup.layers.auth import YowCryptLayer, YowAuthenticationProtocolLayer from yowsup.layers.coder import YowCoderLayer from yowsup.layers.logger import YowLoggerLayer @@ -36,7 +38,7 @@ class YowsupApp(object): def __init__(self): env.CURRENT_ENV = env.S40YowsupEnv() - layers = (SpectrumLayer, + layers = (YowsupAppLayer, YowParallelLayer((YowAuthenticationProtocolLayer, YowMessagesProtocolLayer, YowReceiptProtocolLayer, @@ -123,7 +125,7 @@ class YowsupApp(object): - message: (str) the body of the message """ messageEntity = TextMessageProtocolEntity(message, to = to) - self.stack.toLower(messageEntity) + self.sendEntity(messageEntity) def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t): """ @@ -181,11 +183,18 @@ class YowsupApp(object): """ Called when disconnected from whatsapp """ + + def sendEntity(self, entity): + """Sends an entity down the stack (as if YowsupAppLayer called toLower)""" + self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT, + entity = entity + )) from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback class YowsupAppLayer(YowInterfaceLayer): - EVENT_START = 'transwhat.event.SpectrumLayer.start' + EVENT_START = 'transwhat.event.YowsupAppLayer.start' + TO_LOWER_EVENT = 'transwhat.event.YowsupAppLayer.to_lower' def onEvent(self, layerEvent): # We cannot pass instance varaibles in through init, so we use an event @@ -198,6 +207,9 @@ class YowsupAppLayer(YowInterfaceLayer): elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: self.caller.onDisconnect() return True + elif layerEvent.getName() == YowsupAppLayer.TO_LOWER_EVENT: + self.toLower(layerEvent.getArg('entity')) + return True @ProtocolEntityCallback('success') def onAuthSuccess(self, entity): From 67c5a7c9515e2c1a463be710ca4ddafc48a99c22 Mon Sep 17 00:00:00 2001 From: moyamo Date: Wed, 2 Sep 2015 17:04:49 +0200 Subject: [PATCH 45/51] Add presence updates and looping asyncore.loop() Add functions to update presence and status Make asyncore.loop() timeout after some time and call the functions in the YowStack.__detachedQueue. --- session.py | 45 +++++++++++++++++++++++---------------------- transwhat.py | 10 +++++++++- yowsupwrapper.py | 35 +++++++++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/session.py b/session.py index 8eec4a9..fda0edb 100644 --- a/session.py +++ b/session.py @@ -149,13 +149,13 @@ class Session(YowsupApp): for number in remove: self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE) self.backend.handleBuddyRemoved(self.user, number) - entity = UnsubscribePresenceProtocolEntity(number + "@s.whatsapp.net") - self.toLower(entity) +# entity = UnsubscribePresenceProtocolEntity(number + "@s.whatsapp.net") +# self.toLower(entity) for number in add: buddy = self.buddies[number] - entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") - self.toLower(entity) +# entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") +# self.toLower(entity) # Called by superclass def onAuthSuccess(self, status, kind, creation, @@ -176,6 +176,7 @@ class Session(YowsupApp): # Called by superclass def onDisconnect(self): + self.logger.debug('Disconnected') self.backend.handleDisconnected(self.user, 0, 'Disconnected for unknown reasons') self.loggedin = False @@ -199,7 +200,7 @@ class Session(YowsupApp): messageContent = utils.softToUni(messageEntity.getBody()) timestamp = messageEntity.getTimestamp() - self.sendReceipt(messageEntity.getId(), messageEntity.getFrom(), 'read', + self.sendReceipt(messageEntity.getId(), messageEntity.getFrom(), None, messageEntity.getParticipant()) if messageEntity.isBroadcast(): @@ -209,7 +210,7 @@ class Session(YowsupApp): else: self.logger.info("Message received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp) - self.session.sendMessageToXMPP(buddy, messageContent, timestamp) + self.sendMessageToXMPP(buddy, messageContent, timestamp) # if receiptRequested: self.call("message_ack", (jid, messageId)) @@ -284,14 +285,14 @@ class Session(YowsupApp): self.status = status if status == protocol_pb2.STATUS_ONLINE or status == protocol_pb2.STATUS_FFC: - self.call("presence_sendAvailable") + self.sendPresence(True) else: - self.call("presence_sendUnavailable") + self.sendPresence(False) def changeStatusMessage(self, statusMessage): if (statusMessage != self.statusMessage) or (self.initialized == False): self.statusMessage = statusMessage - self.call("profile_setStatus", message = statusMessage.encode("utf-8")) + self.setStatus(statusMessage.encode('utf-8')) self.logger.info("Status message changed: %s", statusMessage) if self.initialized == False: @@ -465,19 +466,19 @@ class SpectrumLayer(YowInterfaceLayer): reason = layerEvent.getArg("reason") self.logger.info("Disconnected: %s (%s)", self.user, reason) self.backend.handleDisconnected(self.user, 0, reason) - elif layerEvent.getName() == 'presence_sendAvailable': - entity = AvailablePresenceProtocolEntity() - self.toLower(entity) - retval = True - elif layerEvent.getName() == 'presence_sendUnavailable': - entity = UnavailablePresenceProtocolEntity() - self.toLower(entity) - retval = True - elif layerEvent.getName() == 'profile_setStatus': - # entity = PresenceProtocolEntity(name = layerEvent.getArg('message')) - entity = PresenceProtocolEntity(name = 'This status is non-empty') - self.toLower(entity) - retval = True +# elif layerEvent.getName() == 'presence_sendAvailable': +# entity = AvailablePresenceProtocolEntity() +# self.toLower(entity) +# retval = True +# elif layerEvent.getName() == 'presence_sendUnavailable': +# entity = UnavailablePresenceProtocolEntity() +# self.toLower(entity) +# retval = True +# elif layerEvent.getName() == 'profile_setStatus': +# # entity = PresenceProtocolEntity(name = layerEvent.getArg('message')) +# entity = PresenceProtocolEntity(name = 'This status is non-empty') +# self.toLower(entity) +# retval = True # elif layerEvent.getName() == 'message_send': # to = layerEvent.getArg('to') # message = layerEvent.getArg('message') diff --git a/transwhat.py b/transwhat.py index 90703a4..d378b9b 100755 --- a/transwhat.py +++ b/transwhat.py @@ -31,6 +31,7 @@ import sys, os import MySQLdb import e4u import threading +import Queue sys.path.insert(0, os.getcwd()) @@ -39,6 +40,7 @@ from Spectrum2.iochannel import IOChannel from whatsappbackend import WhatsAppBackend from constants import * from yowsup.common import YowConstants +from yowsup.stacks import YowStack # Arguments parser = argparse.ArgumentParser() @@ -71,4 +73,10 @@ io = IOChannel(args.host, args.port, handleTransportData) plugin = WhatsAppBackend(io, db) -asyncore.loop(1) +while True: + asyncore.loop(timeout=1.0, count=10, use_poll = True) + try: + callback = YowStack._YowStack__detachedQueue.get(False) #doesn't block + callback() + except Queue.Empty: + pass diff --git a/yowsupwrapper.py b/yowsupwrapper.py index 576fa31..2dbc7b4 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -33,6 +33,7 @@ from yowsup.layers.protocol_presence.protocolentities import * from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity from yowsup.layers.protocol_chatstate.protocolentities import * from yowsup.layers.protocol_acks.protocolentities import * +from yowsup.layers.protocol_receipts.protocolentities import * class YowsupApp(object): def __init__(self): @@ -92,10 +93,10 @@ class YowsupApp(object): YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT)) except TypeError as e: # Occurs when password is not correctly formated self.onAuthFailure('password not base64 encoded') - try: - self.stack.loop(timeout=0.5, discrete=0.5) - except AuthError as e: # For some reason Yowsup throws an exception - self.onAuthFailure("%s" % e) +# try: +# self.stack.loop(timeout=0.5, discrete=0.5) +# except AuthError as e: # For some reason Yowsup throws an exception +# self.onAuthFailure("%s" % e) def logout(self): """ @@ -114,7 +115,7 @@ class YowsupApp(object): - participant """ receipt = OutgoingReceiptProtocolEntity(_id, _from, read, participant) - self.stack.toLower(receipt) + self.sendEntity(receipt) def sendTextMessage(self, to, message): """ @@ -127,6 +128,28 @@ class YowsupApp(object): messageEntity = TextMessageProtocolEntity(message, to = to) self.sendEntity(messageEntity) + def sendPresence(self, available): + """ + Send presence to whatsapp + + Args: + - available: (boolean) True if available false otherwise + """ + if available: + self.sendEntity(AvailablePresenceProtocolEntity()) + else: + self.sendEntity(UnavailablePresenceProtocolEntity()) + + def setStatus(self, statusText): + """ + Send status to whatsapp + + Args: + - statusTest: (str) Your whatsapp status + """ + entity = PresenceProtocolEntity(name = statusText if len(statusText) == 0 else 'this') + self.sendEntity(entity) + def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t): """ Called when login is successful. @@ -165,7 +188,7 @@ class YowsupApp(object): - offline: (True, False or None) - items """ - pass + pass def onAck(self, _id,_class, _from, timestamp): """ From e43aeedd9d204279997a6335e343f684c3d6b103 Mon Sep 17 00:00:00 2001 From: moyamo Date: Thu, 3 Sep 2015 20:04:29 +0200 Subject: [PATCH 46/51] Add presence and chatstate * Chatstate typing and typing stop is sent and received * Presence is received and presence is sent when user types --- Spectrum2/iochannel.py | 10 ++- buddy.py | 10 ++- session.py | 194 +++++++++++++++-------------------------- transwhat.py | 33 +++++-- yowsupwrapper.py | 129 ++++++++++++++++++++++++++- 5 files changed, 235 insertions(+), 141 deletions(-) diff --git a/Spectrum2/iochannel.py b/Spectrum2/iochannel.py index 2d142c2..858fc2a 100644 --- a/Spectrum2/iochannel.py +++ b/Spectrum2/iochannel.py @@ -1,14 +1,17 @@ import asyncore, socket import logging +import sys class IOChannel(asyncore.dispatcher): - def __init__(self, host, port, callback): + def __init__(self, host, port, callback, closeCallback): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((host, port)) + self.logger = logging.getLogger(self.__class__.__name__) self.callback = callback + self.closeCallback = closeCallback self.buffer = "" def sendData(self, data): @@ -28,6 +31,11 @@ class IOChannel(asyncore.dispatcher): sent = self.send(self.buffer) self.buffer = self.buffer[sent:] + def handle_close(self): + self.logger.info('Connection to backend closed, terminating.') + self.close() + self.closeCallback() + def writable(self): return (len(self.buffer) > 0) diff --git a/buddy.py b/buddy.py index 8338c11..923327f 100644 --- a/buddy.py +++ b/buddy.py @@ -125,10 +125,12 @@ class BuddyList(dict): return Buddy.create(self.owner, Number(number, state, self.db), nick, groups, self.db) def remove(self, number): - buddy = self[number] - buddy.delete() - - return buddy + try: + buddy = self[number] + buddy.delete() + return buddy + except KeyError: + return None def prune(self): cur = self.db.cursor() diff --git a/session.py b/session.py index fda0edb..b480806 100644 --- a/session.py +++ b/session.py @@ -149,13 +149,19 @@ class Session(YowsupApp): for number in remove: self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE) self.backend.handleBuddyRemoved(self.user, number) -# entity = UnsubscribePresenceProtocolEntity(number + "@s.whatsapp.net") -# self.toLower(entity) + self.unsubscribePresence(number) for number in add: buddy = self.buddies[number] -# entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net") -# self.toLower(entity) + self.subscribePresence(number) + self.requestLastSeen(number, self._lastSeen) + + def _lastSeen(self, number, seconds): + self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds))) + if seconds < 60: + self.onPresenceAvailable(number) + else: + self.onPresenceUnavailable(number) # Called by superclass def onAuthSuccess(self, status, kind, creation, @@ -165,6 +171,7 @@ class Session(YowsupApp): self.backend.handleConnected(self.user) self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE) self.initialized = True + self.sendPresence(True) self.updateRoster() @@ -186,9 +193,12 @@ class Session(YowsupApp): ' '.join(map(str, [_id, _from, timestamp, type, participant, offline, items])) ) + buddy = self.buddies[_from.split('@')[0]] + self.backend.handleBuddyChanged(self.user, buddy.number.number, + buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE) # Called by superclass - def onAck(self, _id,_class, _from, timestamp): + def onAck(self, _id, _class, _from, timestamp): self.logger.debug('received ack ' + ' '.join(map(str, [_id, _class, _from,timestamp,])) ) @@ -214,16 +224,68 @@ class Session(YowsupApp): # if receiptRequested: self.call("message_ack", (jid, messageId)) + + # Called by superclass + def onContactTyping(self, buddy): + self.logger.info("Started typing: %s", buddy) + self.sendPresence(True) + self.backend.handleBuddyTyping(self.user, buddy) + + if self.timer != None: + self.timer.cancel() + + # Called by superclass + def onContactPaused(self, buddy): + self.logger.info("Paused typing: %s", buddy) + self.backend.handleBuddyTyped(self.user, buddy) + self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, (self.user, buddy)).start() + def onPresenceReceived(self, _type, name, jid, lastseen): + self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen) + buddy = jid.split("@")[0] +# seems to be causing an error +# self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen)) + + if buddy in self.presenceRequested: + timestamp = time.localtime(time.time() - lastseen) + timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp) + self.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen))) + self.presenceRequested.remove(buddy) + + if lastseen < 60: + self.onPresenceAvailable(buddy) + else: + self.onPresenceUnavailable(buddy) + + def onPresenceAvailable(self, buddy): + try: + buddy = self.buddies[buddy] + self.logger.info("Is available: %s", buddy) + self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE) + except KeyError: + self.logger.error("Buddy not found: %s", buddy) + + def onPresenceUnavailable(self, buddy): + try: + buddy = self.buddies[buddy] + self.logger.info("Is unavailable: %s", buddy) + self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA) + except KeyError: + self.logger.error("Buddy not found: %s", buddy) + # spectrum RequestMethods def sendTypingStarted(self, buddy): if buddy != "bot": self.logger.info("Started typing: %s to %s", self.legacyName, buddy) - self.call("typing_send", buddy = (buddy + "@s.whatsapp.net",)) + self.sendTyping(buddy, True) + # If he is typing he is present + # I really don't know where else to put this. + # Ideally, this should be sent if the user is looking at his client + self.sendPresence(True) def sendTypingStopped(self, buddy): if buddy != "bot": self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy) - self.call("typing_paused", buddy = (buddy + "@s.whatsapp.net",)) + self.sendTyping(buddy, False) def sendMessageToWA(self, sender, message): self.logger.info("Message sent from %s to %s: %s", self.legacyName, sender, message) @@ -352,20 +414,6 @@ class Session(YowsupApp): self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)") if receiptRequested: self.call("message_ack", (jid, messageId)) - def onContactTyping(self, jid): - buddy = jid.split("@")[0] - self.logger.info("Started typing: %s", buddy) - self.backend.handleBuddyTyping(self.user, buddy) - - if self.timer != None: - self.timer.cancel() - - def onContactPaused(self, jid): - buddy = jid.split("@")[0] - self.logger.info("Paused typing: %s", buddy) - self.backend.handleBuddyTyped(self.user, jid.split("@")[0]) - self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, (self.user, buddy)).start() - def onGroupGotInfo(self, gjid, owner, subject, subjectOwner, subjectTimestamp, creationTimestamp): room = gjid.split("@")[0] owner = owner.split("@")[0] @@ -423,7 +471,7 @@ class Session(YowsupApp): room = gjid.split("@")[0] buddy = jid.split("@")[0] - loggin.info("Added % to room %s", buddy, room) + logger.info("Added % to room %s", buddy, room) self.backend.handleParticipantChanged(self.user, buddy, room, protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_ONLINE) if receiptRequested: self.call("notification_ack", (gjid, messageId)) @@ -444,105 +492,3 @@ class Session(YowsupApp): def onGroupPictureUpdated(self, jid, author, timestamp, messageId, pictureId, receiptRequested): # TODO if receiptRequested: self.call("notification_ack", (jid, messageId)) - -class SpectrumLayer(YowInterfaceLayer): - EVENT_START = "transwhat.event.SpectrumLayer.start" - - def onEvent(self, layerEvent): - # We cannot use __init__, since it can take no arguments - retval = False - if layerEvent.getName() == SpectrumLayer.EVENT_START: - self.logger = logging.getLogger(self.__class__.__name__) - self.backend = layerEvent.getArg("backend") - self.user = layerEvent.getArg("user") - self.legacyName = layerEvent.getArg("legacyName") - self.db = layerEvent.getArg("db") - self.session = layerEvent.getArg("session") - - self.session.buddies = BuddyList(self.legacyName, self.db) - self.bot = Bot(self) - retval = True - elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: - reason = layerEvent.getArg("reason") - self.logger.info("Disconnected: %s (%s)", self.user, reason) - self.backend.handleDisconnected(self.user, 0, reason) -# elif layerEvent.getName() == 'presence_sendAvailable': -# entity = AvailablePresenceProtocolEntity() -# self.toLower(entity) -# retval = True -# elif layerEvent.getName() == 'presence_sendUnavailable': -# entity = UnavailablePresenceProtocolEntity() -# self.toLower(entity) -# retval = True -# elif layerEvent.getName() == 'profile_setStatus': -# # entity = PresenceProtocolEntity(name = layerEvent.getArg('message')) -# entity = PresenceProtocolEntity(name = 'This status is non-empty') -# self.toLower(entity) -# retval = True -# elif layerEvent.getName() == 'message_send': -# to = layerEvent.getArg('to') -# message = layerEvent.getArg('message') -# messageEntity = TextMessageProtocolEntity(message, to = to) -# self.toLower(messageEntity) -# retval = True - elif layerEvent.getName() == 'typing_send': - buddy = layerEvent.getArg('buddy') - state = OutgoingChatstateProtocolEntity( - ChatstateProtocolEntity.STATE_TYPING, buddy - ) - self.toLower(state) - retval = True - elif layerEvent.getName() == 'typing_paused': - buddy = layerEvent.getArg('buddy') - state = OutgoingChatstateProtocolEntity( - ChatstateProtocolEntity.STATE_PAUSED, buddy - ) - self.toLower(state) - retval = True - elif layerEvent.getName() == 'presence_request': - buddy = layerEvent.getArg('buddy') - sub = SubscribePresenceProtocolEntity(buddy) - self.toLower(sub) - - self.logger.debug("EVENT %s", layerEvent.getName()) - return retval - - @ProtocolEntityCallback("presence") - def onPrecenceUpdated(self, presence): - jid = presence.getFrom() - lastseen = presence.getLast() - buddy = jid.split("@")[0] -# seems to be causing an error -# self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen)) - - if buddy in self.session.presenceRequested: - timestamp = time.localtime(time.time() - lastseen) - timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp) - self.session.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen))) - self.session.presenceRequested.remove(buddy) - - if lastseen < 60: - self.onPrecenceAvailable(jid) - else: - self.onPrecenceUnavailable(jid) - - def onPrecenceAvailable(self, jid): - buddy = jid.split("@")[0] - - try: - buddy = self.session.buddies[buddy] - self.logger.info("Is available: %s", buddy) - self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE) - except KeyError: - self.logger.error("Buddy not found: %s", buddy) - - def onPrecenceUnavailable(self, jid): - buddy = jid.split("@")[0] - - try: - buddy = self.session.buddies[buddy] - self.logger.info("Is unavailable: %s", buddy) - self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA) - except KeyError: - self.logger.error("Buddy not found: %s", buddy) - diff --git a/transwhat.py b/transwhat.py index d378b9b..84abc31 100755 --- a/transwhat.py +++ b/transwhat.py @@ -25,6 +25,7 @@ __status__ = "Prototype" """ import argparse +import traceback import logging import asyncore import sys, os @@ -54,9 +55,10 @@ parser.add_argument('-j', type=str, required=True) args, unknown = parser.parse_known_args() YowConstants.PATH_STORAGE='/var/lib/spectrum2/' + args.j +loggingfile = '/var/log/spectrum2/' + args.j + '/backends/backend.log' # Logging logging.basicConfig( \ - filename='/var/log/spectrum2/' + args.j + '/backends/backend.log',\ + filename=loggingfile,\ format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s", \ level = logging.DEBUG if args.debug else logging.INFO \ ) @@ -67,16 +69,31 @@ def handleTransportData(data): e4u.load() +closed = False +def connectionClosed(): + global closed + closed = True + # Main db = MySQLdb.connect(DB_HOST, DB_USER, DB_PASS, DB_TABLE) -io = IOChannel(args.host, args.port, handleTransportData) +io = IOChannel(args.host, args.port, handleTransportData, connectionClosed) plugin = WhatsAppBackend(io, db) while True: - asyncore.loop(timeout=1.0, count=10, use_poll = True) - try: - callback = YowStack._YowStack__detachedQueue.get(False) #doesn't block - callback() - except Queue.Empty: - pass + try: + asyncore.loop(timeout=1.0, count=10, use_poll = True) + try: + callback = YowStack._YowStack__detachedQueue.get(False) #doesn't block + callback() + except Queue.Empty: + pass + else: + break + if closed: + break + except SystemExit: + break + except: + logger = logging.getLogger('transwhat') + logger.error(traceback.format_exc()) diff --git a/yowsupwrapper.py b/yowsupwrapper.py index 2dbc7b4..c4e497b 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -49,7 +49,7 @@ class YowsupApp(object): YowIqProtocolLayer, YowNotificationsProtocolLayer, YowContactsIqProtocolLayer, -# YowChatstateProtocolLayer, + YowChatstateProtocolLayer, YowCallsProtocolLayer, YowMediaProtocolLayer, YowPrivacyProtocolLayer, @@ -139,6 +139,30 @@ class YowsupApp(object): self.sendEntity(AvailablePresenceProtocolEntity()) else: self.sendEntity(UnavailablePresenceProtocolEntity()) + + def subscribePresence(self, phone_number): + """ + Subscribe to presence updates from phone_number + + Args: + - phone_number: (str) The cellphone number of the person to + subscribe to + """ + jid = phone_number + '@s.whatsapp.net' + entity = SubscribePresenceProtocolEntity(jid) + self.sendEntity(entity) + + def unsubscribePresence(self, phone_number): + """ + Unsubscribe to presence updates from phone_number + + Args: + - phone_number: (str) The cellphone number of the person to + unsubscribe from + """ + jid = phone_number + '@s.whatsapp.net' + entity = UnsubscribePresenceProtocolEntity(jid) + self.sendEntity(entity) def setStatus(self, statusText): """ @@ -149,6 +173,48 @@ class YowsupApp(object): """ entity = PresenceProtocolEntity(name = statusText if len(statusText) == 0 else 'this') self.sendEntity(entity) + + def sendTyping(self, phoneNumber, typing): + """ + Notify buddy using phoneNumber that you are typing to him + + Args: + - phoneNumber: (str) cellphone number of the buddy you are typing to. + - typing: (bool) True if you are typing, False if you are not + """ + jid = phoneNumber + '@s.whatsapp.net' + if typing: + state = OutgoingChatstateProtocolEntity( + ChatstateProtocolEntity.STATE_TYPING, jid + ) + else: + state = OutgoingChatstateProtocolEntity( + ChatstateProtocolEntity.STATE_PAUSED, jid + ) + self.sendEntity(state) + + def requestLastSeen(self, phoneNumber, success = None, failure = None): + """ + Requests when user was last seen. + Args: + - phone_number: (str) the phone number of the user + - success: (func) called when request is successfully processed. + The first argument is the number, second argument is the seconds + since last seen. + - failure: (func) called when request has failed + """ + iq = LastseenIqProtocolEntity(phoneNumber + '@s.whatsapp.net') + self.stack.broadcastEvent( + YowLayerEvent(YowsupAppLayer.SEND_IQ, + iq = iq, + success = self._lastSeenSuccess(success), + failure = failure, + ) + ) + def _lastSeenSuccess(self, success): + def func(response, request): + success(response._from.split('@')[0], response.seconds) + return func def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t): """ @@ -201,12 +267,43 @@ class YowsupApp(object): - timestamp """ pass + + def onPresenceReceived(self, _type, name, _from, last): + """ + Called when presence (e.g. available, unavailable) is received + from whatsapp + + Args: + - _type: (str) 'available' or 'unavailable' + - _name + - _from + - _last + """ + pass def onDisconnect(self): """ Called when disconnected from whatsapp """ + def onContactTyping(self, number): + """ + Called when contact starts to type + + Args: + - number: (str) cellphone number of contact + """ + pass + + def onContactPaused(self, number): + """ + Called when contact stops typing + + Args: + - number: (str) cellphone number of contact + """ + pass + def sendEntity(self, entity): """Sends an entity down the stack (as if YowsupAppLayer called toLower)""" self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT, @@ -217,7 +314,8 @@ from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback class YowsupAppLayer(YowInterfaceLayer): EVENT_START = 'transwhat.event.YowsupAppLayer.start' - TO_LOWER_EVENT = 'transwhat.event.YowsupAppLayer.to_lower' + TO_LOWER_EVENT = 'transwhat.event.YowsupAppLayer.toLower' + SEND_IQ = 'transwhat.event.YowsupAppLayer.sendIq' def onEvent(self, layerEvent): # We cannot pass instance varaibles in through init, so we use an event @@ -233,6 +331,13 @@ class YowsupAppLayer(YowInterfaceLayer): elif layerEvent.getName() == YowsupAppLayer.TO_LOWER_EVENT: self.toLower(layerEvent.getArg('entity')) return True + elif layerEvent.getName() == YowsupAppLayer.SEND_IQ: + iq = layerEvent.getArg('iq') + success = layerEvent.getArg('success') + failure = layerEvent.getArg('failure') + self._sendIq(iq, success, failure) + return True + return False @ProtocolEntityCallback('success') def onAuthSuccess(self, entity): @@ -283,8 +388,24 @@ class YowsupAppLayer(YowInterfaceLayer): """ Sends ack automatically """ - self.toLower(notification.ack()) + self.toLower(entity.ack()) - @ProtocolEntityCallback("message") + @ProtocolEntityCallback('message') def onMessageReceived(self, entity): self.caller.onMessage(entity) + + @ProtocolEntityCallback('presence') + def onPresenceReceived(self, presence): + _type = presence.getType() + name = presence.getName() + _from = presence.getFrom() + last = presence.getLast() + self.caller.onPresenceReceived(_type, name, _from, last) + + @ProtocolEntityCallback('chatstate') + def onChatstate(self, chatstate): + number = chatstate._from.split('@')[0] + if chatstate.getState() == ChatstateProtocolEntity.STATE_TYPING: + self.caller.onContactTyping(number) + else: + self.caller.onContactPaused(number) From a6971ad8895472c62c66ec98c3fccc45b26bc904 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sat, 5 Sep 2015 11:39:34 +0200 Subject: [PATCH 47/51] Add workaround to prevent double messages For some reason spectrum occasionally sends to identical messages to a buddy, one to the bare jid and one to /bot. This causes duplicate messages. Since it is unlikely a user wants to send the same message twice, we should just ignore the second message. This is only a work around, a proper fix should be implemented. --- whatsappbackend.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/whatsappbackend.py b/whatsappbackend.py index 4aa0b17..4c07fb7 100644 --- a/whatsappbackend.py +++ b/whatsappbackend.py @@ -36,6 +36,8 @@ class WhatsAppBackend(SpectrumBackend): self.io = io self.db = db self.sessions = { } + # Used to prevent duplicate messages + self.lastMessage = {} self.logger.debug("Backend started") @@ -45,6 +47,9 @@ class WhatsAppBackend(SpectrumBackend): if user not in self.sessions: self.sessions[user] = Session(self, user, legacyName, extra, self.db) + if user not in self.lastMessage: + self.lastMessage[user] = {} + self.sessions[user].login(password) def handleLogoutRequest(self, user, legacyName): @@ -54,8 +59,15 @@ class WhatsAppBackend(SpectrumBackend): del self.sessions[user] def handleMessageSendRequest(self, user, buddy, message, xhtml = ""): - self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s)", user, buddy, message) - self.sessions[user].sendMessageToWA(buddy, message) + self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s, xhtml = %s)", user, buddy, message, xhtml) + # For some reason spectrum occasionally sends to identical messages to + # a buddy, one to the bare jid and one to /bot. This causes duplicate + # messages. Since it is unlikely a user wants to send the same message + # twice, we should just ignore the second message + usersMessage = self.lastMessage[user] + if buddy not in usersMessage or usersMessage[buddy] != message: + self.sessions[user].sendMessageToWA(buddy, message) + usersMessage[buddy] = message def handleJoinRoomRequest(self, user, room, nickname, pasword): self.logger.debug("handleJoinRoomRequest(user=%s, room=%s, nickname=%s)", user, room, nickname) From 4c365e3f810fe61d79035aaaceeba312c1d7818d Mon Sep 17 00:00:00 2001 From: moyamo Date: Sat, 5 Sep 2015 22:05:34 +0200 Subject: [PATCH 48/51] Add receiveing of images, videos and sound files When a whatsapp buddy sends an image, video or sound to the spectrum user, the spectrum user will receive a URL that links to the media. FileTranfer in spectrum is not working, apparently. Thus media cannot be sent yet. --- Spectrum2/backend.py | 2 +- session.py | 82 +++++++++++++++++++++++++---------- whatsappbackend.py | 6 ++- yowsupwrapper.py | 101 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 163 insertions(+), 28 deletions(-) diff --git a/Spectrum2/backend.py b/Spectrum2/backend.py index 3bef24f..a027adc 100644 --- a/Spectrum2/backend.py +++ b/Spectrum2/backend.py @@ -204,7 +204,7 @@ class SpectrumBackend: def handleFTData(self, ftID, data): d = protocol_pb2.FileTransferData() - d.ftid = ftID + d.ftID = ftID d.data = data message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA); diff --git a/session.py b/session.py index b480806..ddd14f8 100644 --- a/session.py +++ b/session.py @@ -204,26 +204,69 @@ class Session(YowsupApp): ) # Called by superclass - def onMessage(self, messageEntity): - self.logger.debug(str(messageEntity)) - buddy = messageEntity.getFrom().split('@')[0] - messageContent = utils.softToUni(messageEntity.getBody()) - timestamp = messageEntity.getTimestamp() + def onTextMessage(self, _id, _from, to, notify, timestamp, participant, offline, retry, body): + self.logger.debug('received TextMessage' + + ' '.join(map(str, [ + _id, _from, to, notify, timestamp, + participant, offline, retry, body + ])) + ) + buddy = _from.split('@')[0] + messageContent = utils.softToUni(body) + self.sendReceipt(_id, _from, None, participant) + self.logger.info("Message received from %s to %s: %s (at ts=%s)", + buddy, self.legacyName, messageContent, timestamp) + self.sendMessageToXMPP(buddy, messageContent, timestamp) + # isBroadcast always returns false, I'm not sure how to get a broadcast + # message. + #if messageEntity.isBroadcast(): + # self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\ + # buddy, self.legacyName, messageContent, timestamp) + # messageContent = "[Broadcast] " + messageContent - self.sendReceipt(messageEntity.getId(), messageEntity.getFrom(), None, - messageEntity.getParticipant()) + # Called by superclass + def onImage(self, image): + self.logger.debug('Received image message %s', str(image)) + buddy = image._from.split('@')[0] + message = image.url + ' ' + image.caption + self.sendMessageToXMPP(buddy, message, image.timestamp) + self.sendReceipt(image._id, image._from, None, image.participant) - if messageEntity.isBroadcast(): - self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\ - buddy, self.legacyName, messageContent, timestamp) - messageContent = "[Broadcast] " + messageContent - else: - self.logger.info("Message received from %s to %s: %s (at ts=%s)", - buddy, self.legacyName, messageContent, timestamp) - self.sendMessageToXMPP(buddy, messageContent, timestamp) + # Called by superclass + def onAudio(self, audio): + self.logger.debug('Received audio message %s', str(audio)) + buddy = audio._from.split('@')[0] + message = audio.url + self.sendMessageToXMPP(buddy, message, audio.timestamp) + self.sendReceipt(audio._id, audio._from, None, audio.participant) - # if receiptRequested: self.call("message_ack", (jid, messageId)) + # Called by superclass + def onVideo(self, video): + self.logger.debug('Received video message %s', str(video)) + buddy = video._from.split('@')[0] + message = video.url + self.sendMessageToXMPP(buddy, message, video.timestamp) + self.sendReceipt(video._id, video._from, None, video.participant) + # Called by superclass + def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant): + self.logger.debug('received VCard' + + ' '.join(map(str, [ + _id, _from, name, card_data, to, notify, timestamp, participant + ])) + ) + buddy = _from.split("@")[0] + self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)") + self.sendMessageToXMPP(buddy, card_data) + self.transferFile(buddy, str(name), card_data) + self.sendReceipt(_id, _from, None, participant) + + def transferFile(self, buddy, name, data): + # Not working + self.logger.debug('transfering file %s', name) + self.backend.handleFTStart(self.user, buddy, name, len(data)) + self.backend.handleFTData(0, data) + self.backend.handleFTFinish(self.user, buddy, name, len(data), 0) # Called by superclass def onContactTyping(self, buddy): @@ -408,11 +451,6 @@ class Session(YowsupApp): self.sendMessageToXMPP(buddy, utils.shorten(url)) if receiptRequested: self.call("message_ack", (jid, messageId)) - def onVcardReceived(self, messageId, jid, name, data, receiptRequested, isBroadcast): # TODO - buddy = jid.split("@")[0] - self.logger.info("VCard received from %s", buddy) - self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)") - if receiptRequested: self.call("message_ack", (jid, messageId)) def onGroupGotInfo(self, gjid, owner, subject, subjectOwner, subjectTimestamp, creationTimestamp): room = gjid.split("@")[0] @@ -471,7 +509,7 @@ class Session(YowsupApp): room = gjid.split("@")[0] buddy = jid.split("@")[0] - logger.info("Added % to room %s", buddy, room) + self.logger.info("Added %s to room %s", buddy, room) self.backend.handleParticipantChanged(self.user, buddy, room, protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_ONLINE) if receiptRequested: self.call("notification_ack", (gjid, messageId)) diff --git a/whatsappbackend.py b/whatsappbackend.py index 4c07fb7..95a0ae1 100644 --- a/whatsappbackend.py +++ b/whatsappbackend.py @@ -64,6 +64,9 @@ class WhatsAppBackend(SpectrumBackend): # a buddy, one to the bare jid and one to /bot. This causes duplicate # messages. Since it is unlikely a user wants to send the same message # twice, we should just ignore the second message + # + # TODO Proper fix, this work around drops all duplicate messages even + # intentional ones. usersMessage = self.lastMessage[user] if buddy not in usersMessage or usersMessage[buddy] != message: self.sessions[user].sendMessageToWA(buddy, message) @@ -115,7 +118,8 @@ class WhatsAppBackend(SpectrumBackend): pass def handleFTStartRequest(self, user, buddy, fileName, size, ftID): - pass + self.logger.debug('File send request %s, for user %s, from %s, size: %s', + fileName, user, buddy, size) def handleFTFinishRequest(self, user, buddy, fileName, size, ftID): pass diff --git a/yowsupwrapper.py b/yowsupwrapper.py index c4e497b..78b0a69 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -30,7 +30,8 @@ from yowsup.layers.protocol_calls import YowCallsProtocolLayer # ProtocolEntities from yowsup.layers.protocol_presence.protocolentities import * -from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity +from yowsup.layers.protocol_messages.protocolentities import * +from yowsup.layers.protocol_media.protocolentities import * from yowsup.layers.protocol_chatstate.protocolentities import * from yowsup.layers.protocol_acks.protocolentities import * from yowsup.layers.protocol_receipts.protocolentities import * @@ -110,8 +111,8 @@ class YowsupApp(object): Args: - _id: id of message received - - _from - - read: ('read' or something else) + - _from: jid of person who sent the message + - read: ('read' or None) None is just delivered, 'read' is read - participant """ receipt = OutgoingReceiptProtocolEntity(_id, _from, read, participant) @@ -304,6 +305,67 @@ class YowsupApp(object): """ pass + def onTextMessage(self, _id, _from, to, notify, timestamp, participant, offline, retry, body): + """ + Called when text message is received + + Args: + - _id: + - _from: (str) jid of of sender + - to: + - notify: (str) human readable name of _from (e.g. John Smith) + - timestamp: + - participant: + - offline: + - retry: + - body: The content of the message + """ + pass + + def onImage(self, entity): + """ + Called when image message is received + + Args: + - entity: ImageDownloadableMediaMessageProtocolEntity + """ + pass + + def onAudio(self, entity): + """ + Called when audio message is received + + Args: + - entity: AudioDownloadableMediaMessageProtocolEntity + """ + pass + + + def onVideo(self, entity): + """ + Called when video message is received + + Args: + - entity: VideoDownloadableMediaMessageProtocolEntity + """ + pass + + def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant): + """ + Called when VCard message is received + + Args: + - _id: (str) id of entity + - _from: + - name: + - card_data: + - to: + - notify: + - timestamp: + - participant: + """ + pass + def sendEntity(self, entity): """Sends an entity down the stack (as if YowsupAppLayer called toLower)""" self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT, @@ -392,7 +454,38 @@ class YowsupAppLayer(YowInterfaceLayer): @ProtocolEntityCallback('message') def onMessageReceived(self, entity): - self.caller.onMessage(entity) + if entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_TEXT: + self.caller.onTextMessage( + entity._id, + entity._from, + entity.to, + entity.notify, + entity.timestamp, + entity.participant, + entity.offline, + entity.retry, + entity.body + ) + elif entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_MEDIA: + if isinstance(entity, ImageDownloadableMediaMessageProtocolEntity): + # There is just way too many fields to pass them into the + # function + self.caller.onImage(entity) + elif isinstance(entity, AudioDownloadableMediaMessageProtocolEntity): + self.caller.onAudio(entity) + elif isinstance(entity, VideoDownloadableMediaMessageProtocolEntity): + self.caller.onVideo(entity) + elif isinstance(entity, VCardMediaMessageProtocolEntity): + self.caller.onVCard( + entity._id, + entity._from, + entity.name, + entity.card_data, + entity.to, + entity.notify, + entity.timestamp, + entity.participant + ) @ProtocolEntityCallback('presence') def onPresenceReceived(self, presence): From d2efa0dd7bc7858b62176f8f835e08a9f47afe17 Mon Sep 17 00:00:00 2001 From: moyamo Date: Sat, 5 Sep 2015 22:22:30 +0200 Subject: [PATCH 49/51] Fix status message --- session.py | 52 +----------------------------------------------- yowsupwrapper.py | 24 +++++++++++++--------- 2 files changed, 16 insertions(+), 60 deletions(-) diff --git a/session.py b/session.py index ddd14f8..63baa5b 100644 --- a/session.py +++ b/session.py @@ -27,50 +27,6 @@ import logging import urllib import time -from yowsup.stacks import YowStack -from yowsup.layers import YowLayerEvent, YowParallelLayer -from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback -from yowsup.layers.axolotl import YowAxolotlLayer -from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer, - AuthError) -from yowsup.layers.protocol_iq import YowIqProtocolLayer -from yowsup.layers.protocol_groups import YowGroupsProtocolLayer -from yowsup.layers.coder import YowCoderLayer -from yowsup.layers.network import YowNetworkLayer -from yowsup.layers.protocol_messages import YowMessagesProtocolLayer -from yowsup.layers.protocol_media import YowMediaProtocolLayer -from yowsup.layers.stanzaregulator import YowStanzaRegulator -from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer -from yowsup.layers.protocol_acks import YowAckProtocolLayer -from yowsup.layers.logger import YowLoggerLayer -from yowsup.common import YowConstants -from yowsup.layers.protocol_receipts.protocolentities import * -from yowsup import env -from yowsup.layers.protocol_presence import * -from yowsup.layers.protocol_presence.protocolentities import * -from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity -from yowsup.layers.protocol_chatstate.protocolentities import * -from yowsup.layers.protocol_acks.protocolentities import * -from yowsup.layers import YowLayer -from yowsup.layers.auth import YowCryptLayer, YowAuthenticationProtocolLayer -from yowsup.layers.coder import YowCoderLayer -from yowsup.layers.logger import YowLoggerLayer -from yowsup.layers.network import YowNetworkLayer -from yowsup.layers.protocol_messages import YowMessagesProtocolLayer -from yowsup.layers.stanzaregulator import YowStanzaRegulator -from yowsup.layers.protocol_media import YowMediaProtocolLayer -from yowsup.layers.protocol_acks import YowAckProtocolLayer -from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer -from yowsup.layers.protocol_groups import YowGroupsProtocolLayer -from yowsup.layers.protocol_presence import YowPresenceProtocolLayer -from yowsup.layers.protocol_ib import YowIbProtocolLayer -from yowsup.layers.protocol_notifications import YowNotificationsProtocolLayer -from yowsup.layers.protocol_iq import YowIqProtocolLayer -from yowsup.layers.protocol_contacts import YowContactsIqProtocolLayer -from yowsup.layers.protocol_chatstate import YowChatstateProtocolLayer -from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer -from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer -from yowsup.layers.protocol_calls import YowCallsProtocolLayer from Spectrum2 import protocol_pb2 from buddy import BuddyList @@ -436,13 +392,7 @@ class Session(YowsupApp): else: self.logger.warn("Room doesn't exist: %s", room) - def onMediaReceived(self, messageId, jid, preview, url, size, receiptRequested, isBroadcast): - buddy = jid.split("@")[0] - - self.logger.info("Media received from %s: %s", buddy, url) - self.sendMessageToXMPP(buddy, utils.shorten(url)) - if receiptRequested: self.call("message_ack", (jid, messageId)) - + # Not used def onLocationReceived(self, messageId, jid, name, preview, latitude, longitude, receiptRequested, isBroadcast): buddy = jid.split("@")[0] self.logger.info("Location received from %s: %s, %s", buddy, latitude, longitude) diff --git a/yowsupwrapper.py b/yowsupwrapper.py index 78b0a69..0e62125 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -35,6 +35,7 @@ from yowsup.layers.protocol_media.protocolentities import * from yowsup.layers.protocol_chatstate.protocolentities import * from yowsup.layers.protocol_acks.protocolentities import * from yowsup.layers.protocol_receipts.protocolentities import * +from yowsup.layers.protocol_profiles.protocolentities import * class YowsupApp(object): def __init__(self): @@ -172,8 +173,8 @@ class YowsupApp(object): Args: - statusTest: (str) Your whatsapp status """ - entity = PresenceProtocolEntity(name = statusText if len(statusText) == 0 else 'this') - self.sendEntity(entity) + iq = SetStatusIqProtocolEntity(statusText) + self.sendIq(iq) def sendTyping(self, phoneNumber, typing): """ @@ -205,13 +206,8 @@ class YowsupApp(object): - failure: (func) called when request has failed """ iq = LastseenIqProtocolEntity(phoneNumber + '@s.whatsapp.net') - self.stack.broadcastEvent( - YowLayerEvent(YowsupAppLayer.SEND_IQ, - iq = iq, - success = self._lastSeenSuccess(success), - failure = failure, - ) - ) + self.sendIq(iq, self._lastSeenSuccess(success), failure) + def _lastSeenSuccess(self, success): def func(response, request): success(response._from.split('@')[0], response.seconds) @@ -371,6 +367,16 @@ class YowsupApp(object): self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT, entity = entity )) + + def sendIq(self, iq, onSuccess = None, onError = None): + self.stack.broadcastEvent( + YowLayerEvent( + YowsupAppLayer.SEND_IQ, + iq = iq, + success = onSuccess, + failure = onError, + ) + ) from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback From 4fd313e516c31bb66116a3ca88ad0643eef4617c Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 6 Sep 2015 00:14:12 +0200 Subject: [PATCH 50/51] Download whatsapp profile pictures Everytime the user requests a VCard the profile pictures are updated. The sha1hash hash of the picture is calculated and stored in buddy. --- buddy.py | 31 +++++++++++++++++-------------- conf/schema.sql | 1 + session.py | 19 +++++++++++++++++-- utils.py | 4 ++++ whatsappbackend.py | 7 ++++--- yowsupwrapper.py | 13 ++++++++++++- 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/buddy.py b/buddy.py index 923327f..1ab9944 100644 --- a/buddy.py +++ b/buddy.py @@ -48,7 +48,7 @@ class Number(): class Buddy(): - def __init__(self, owner, number, nick, groups, id, db): + def __init__(self, owner, number, nick, groups, image_hash, id, db): self.id = id self.db = db @@ -56,14 +56,16 @@ class Buddy(): self.owner = owner self.number = number self.groups = groups + self.image_hash = image_hash - def update(self, nick, groups): + def update(self, nick, groups, image_hash): self.nick = nick self.groups = groups + self.image_hash = image_hash groups = u",".join(groups).encode("latin-1") cur = self.db.cursor() - cur.execute("UPDATE buddies SET nick = %s, groups = %s WHERE owner_id = %s AND buddy_id = %s", (self.nick, groups, self.owner.id, self.number.id)) + cur.execute("UPDATE buddies SET nick = %s, groups = %s, image_hash = %s WHERE owner_id = %s AND buddy_id = %s", (self.nick, groups, image_hash, self.owner.id, self.number.id)) self.db.commit() def delete(self): @@ -73,13 +75,13 @@ class Buddy(): self.id = None @staticmethod - def create(owner, number, nick, groups, db): + def create(owner, number, nick, groups, image_hash, db): groups = u",".join(groups).encode("latin-1") cur = db.cursor() - cur.execute("REPLACE buddies (owner_id, buddy_id, nick, groups) VALUES (%s, %s, %s, %s)", (owner.id, number.id, nick, groups)) + cur.execute("REPLACE buddies (owner_id, buddy_id, nick, groups, image_hash) VALUES (%s, %s, %s, %s, %s)", (owner.id, number.id, nick, groups, image_hash)) db.commit() - return Buddy(owner, number, nick, groups, cur.lastrowid, db) + return Buddy(owner, number, nick, groups, image_hash, cur.lastrowid, db) def __str__(self): return "%s (nick=%s, id=%s)" % (self.number, self.nick, self.id) @@ -99,7 +101,8 @@ class BuddyList(dict): n.number AS number, b.nick AS nick, b.groups AS groups, - n.state AS state + n.state AS state, + b.image_hash AS image_hash FROM buddies AS b LEFT JOIN numbers AS n ON b.buddy_id = n.id @@ -109,20 +112,20 @@ class BuddyList(dict): ORDER BY b.owner_id DESC""", self.owner.id) for i in range(cur.rowcount): - id, number, nick, groups, state = cur.fetchone() - self[number] = Buddy(self.owner, Number(number, state, self.db), nick.decode('latin1'), groups.split(","), id, self.db) + id, number, nick, groups, state, image_hash = cur.fetchone() + self[number] = Buddy(self.owner, Number(number, state, self.db), nick.decode('latin1'), groups.split(","), image_hash, id, self.db) - def update(self, number, nick, groups): + def update(self, number, nick, groups, image_hash): if number in self: buddy = self[number] - buddy.update(nick, groups) + buddy.update(nick, groups, image_hash) else: - buddy = self.add(number, nick, groups, 1) + buddy = self.add(number, nick, groups, 1, image_hash) return buddy - def add(self, number, nick, groups = [], state = 0): - return Buddy.create(self.owner, Number(number, state, self.db), nick, groups, self.db) + def add(self, number, nick, groups = [], state = 0, image_hash = ""): + return Buddy.create(self.owner, Number(number, state, self.db), nick, groups, image_hash, self.db) def remove(self, number): try: diff --git a/conf/schema.sql b/conf/schema.sql index 32441ca..f6fb0e7 100644 --- a/conf/schema.sql +++ b/conf/schema.sql @@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS `buddies` ( `buddy_id` int(11) NOT NULL, `nick` varchar(255) NOT NULL, `groups` varchar(255) NOT NULL, + `image_hash` varchar(40), PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; diff --git a/session.py b/session.py index 63baa5b..6281a08 100644 --- a/session.py +++ b/session.py @@ -110,6 +110,7 @@ class Session(YowsupApp): for number in add: buddy = self.buddies[number] self.subscribePresence(number) + self.backend.handleBuddyChanged(self.user, number, buddy.nick, buddy.groups, protocol_pb2.STATUS_NONE, iconHash = buddy.image_hash if buddy.image_hash is not None else "") self.requestLastSeen(number, self._lastSeen) def _lastSeen(self, number, seconds): @@ -368,9 +369,9 @@ class Session(YowsupApp): self.backend.handleMessage(self.user, msg[0], msg[1], "", "", msg[2]) # also for adding a new buddy - def updateBuddy(self, buddy, nick, groups): + def updateBuddy(self, buddy, nick, groups, image_hash =""): if buddy != "bot": - self.buddies.update(buddy, nick, groups) + self.buddies.update(buddy, nick, groups, image_hash) self.updateRoster() def removeBuddy(self, buddy): @@ -391,6 +392,20 @@ class Session(YowsupApp): self.backend.handleSubject(self.user, room, group.subject, group.subjectOwner) else: self.logger.warn("Room doesn't exist: %s", room) + + def requestVCard(self, buddy, ID): + + def onSuccess(response, request): + self.logger.debug('Sending VCard (%s) with image id %s', + ID, response.pictureId) + image_hash = utils.sha1hash(response.pictureData) + self.logger.debug('Image hash is %s', image_hash) + self.backend.handleVCard(self.user, ID, buddy, "", "", response.pictureData) + obuddy = self.buddies[buddy] + self.updateBuddy(buddy, obuddy.nick, obuddy.groups, image_hash) + + self.logger.debug('Requesting profile picture of %s', buddy) + self.requestProfilePicture(buddy, onSuccess = onSuccess) # Not used def onLocationReceived(self, messageId, jid, name, preview, latitude, longitude, receiptRequested, isBroadcast): diff --git a/utils.py b/utils.py index 71d6f38..c2e1ba6 100644 --- a/utils.py +++ b/utils.py @@ -26,6 +26,7 @@ import urllib import json import e4u import base64 +import hashlib def shorten(url): url = urllib.urlopen("http://d.0l.de/add.json?type=URL&rdata=%s" % urllib.quote(url)) @@ -61,3 +62,6 @@ def softToUni(message): def decodePassword(password): return base64.b64decode(bytes(password.encode("utf-8"))) + +def sha1hash(data): + return hashlib.sha1(data).hexdigest() diff --git a/whatsappbackend.py b/whatsappbackend.py index 95a0ae1..b7b9827 100644 --- a/whatsappbackend.py +++ b/whatsappbackend.py @@ -101,6 +101,10 @@ class WhatsAppBackend(SpectrumBackend): self.logger.debug("handleStoppedTypingRequest(user=%s, buddy=%s)", user, buddy) self.sessions[user].sendTypingStopped(buddy) + def handleVCardRequest(self, user, buddy, ID): + self.logger.debug("handleVCardRequest(user=%s, buddy=%s, ID=%s)", user, buddy, ID) + self.sessions[user].requestVCard(buddy, ID) + # TODO def handleBuddyBlockToggled(self, user, buddy, blocked): pass @@ -108,9 +112,6 @@ class WhatsAppBackend(SpectrumBackend): def handleLeaveRoomRequest(self, user, room): pass - def handleVCardRequest(self, user, buddy, ID): - pass - def handleVCardUpdatedRequest(self, user, photo, nickname): pass diff --git a/yowsupwrapper.py b/yowsupwrapper.py index 0e62125..ad374a5 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -206,13 +206,24 @@ class YowsupApp(object): - failure: (func) called when request has failed """ iq = LastseenIqProtocolEntity(phoneNumber + '@s.whatsapp.net') - self.sendIq(iq, self._lastSeenSuccess(success), failure) + self.sendIq(iq, onSuccess = self._lastSeenSuccess(success), onError = failure) def _lastSeenSuccess(self, success): def func(response, request): success(response._from.split('@')[0], response.seconds) return func + def requestProfilePicture(self, phoneNumber, onSuccess = None, onFailure = None): + """ + Requests profile picture of whatsapp user + Args: + - phoneNumber: (str) the phone number of the user + - success: (func) called when request is successfully processed. + - failure: (func) called when request has failed + """ + iq = GetPictureIqProtocolEntity(phoneNumber + '@s.whatsapp.net') + self.sendIq(iq, onSuccess = onSuccess, onError = onFailure) + def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t): """ Called when login is successful. From 646f45434b07081eed722de0182c91ac01c4f9dd Mon Sep 17 00:00:00 2001 From: moyamo Date: Sun, 6 Sep 2015 14:06:16 +0200 Subject: [PATCH 51/51] Crude implementation of groupchat --- session.py | 103 +++++++++++++++++++++++++++-------------------- yowsupwrapper.py | 29 +++++++------ 2 files changed, 77 insertions(+), 55 deletions(-) diff --git a/session.py b/session.py index 6281a08..b86fa97 100644 --- a/session.py +++ b/session.py @@ -86,7 +86,7 @@ class Session(YowsupApp): rooms = [] for room, group in self.groups.iteritems(): rooms.append([room, group.subject]) - + self.logger.debug("Got rooms: %s", rooms) self.backend.handleRoomList(rooms) def updateRoster(self): @@ -111,7 +111,44 @@ class Session(YowsupApp): buddy = self.buddies[number] self.subscribePresence(number) self.backend.handleBuddyChanged(self.user, number, buddy.nick, buddy.groups, protocol_pb2.STATUS_NONE, iconHash = buddy.image_hash if buddy.image_hash is not None else "") - self.requestLastSeen(number, self._lastSeen) + #self.requestLastSeen(number, self._lastSeen) + self.logger.debug('Requesting groups list') + self.requestGroupsList(self._updateGroups) + + def _updateGroups(self, response, request): + self.logger.debug('Received groups list %s', response) + # This XMPP client is not receiving this for some reason. + groups = response.getGroups() + for group in groups: + room = group.getId() + owner = group.getOwner() + subjectOwner = group.getSubjectOwner() + subject = group.getSubject() + + if room in self.groups: + oroom = self.groups[room] + oroom.owner = owner + oroom.subjectOwner = subjectOwner + oroom.subject = subject + else: + self.groups[room] = Group(room, owner, subject, subjectOwner) + # A crude implemtation of groups that act like buddies + + self.backend.handleBuddyChanged(self.user, room, subject, [], protocol_pb2.STATUS_NONE) + # This XMPP client is not receiving this for some reason. +# self.updateRoomList() +# for group in groups: +# room = group.getId() +# subjectOwner = group.getSubjectOwner() +# subject = group.getSubject() +# self.backend.handleSubject(self.user, room, subject, subjectOwner) +# for participant in group.getParticipants(): +# buddy = participant.split('@')[0] +# self.logger.debug("Added %s to room %s", buddy, room) +# self.backend.handleParticipantChanged(self.user, buddy, room, +# protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_ONLINE) + + def _lastSeen(self, number, seconds): self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds))) @@ -173,7 +210,12 @@ class Session(YowsupApp): self.sendReceipt(_id, _from, None, participant) self.logger.info("Message received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp) - self.sendMessageToXMPP(buddy, messageContent, timestamp) + if participant is not None: + partname = participant.split('@')[0] + message = partname + ': ' + messageContent + self.sendMessageToXMPP(buddy, message, timestamp) + else: + self.sendMessageToXMPP(buddy, messageContent, timestamp) # isBroadcast always returns false, I'm not sure how to get a broadcast # message. #if messageEntity.isBroadcast(): @@ -291,28 +333,27 @@ class Session(YowsupApp): self.logger.info("Message sent from %s to %s: %s", self.legacyName, sender, message) message = message.encode("utf-8") -# if sender == "bot": -# self.bot.parse(message) -# elif "-" in sender: # group msg -# if "/" in sender: -# room, buddy = sender.split("/") -# self.sendTextMessage(buddy + '@s.whatsapp.net', message) -# else: -# room = sender + if sender == "bot": + self.bot.parse(message) + elif "-" in sender: # group msg + if "/" in sender: + room, buddy = sender.split("/") + self.sendTextMessage(buddy + '@s.whatsapp.net', message) + else: + room = sender # group = self.groups[room] -# + # self.backend.handleMessage(self.user, room, message, group.nick) -# self.sendTextMessage(room + '@g.us', message) -# -# else: # private msg -# buddy = sender + self.sendTextMessage(room + '@g.us', message) + + else: # private msg + buddy = sender # if message == "\\lastseen": -# self.presenceRequested.append(buddy) # self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",)) # else: - self.sendTextMessage(sender + '@s.whatsapp.net', message) + self.sendTextMessage(sender + '@s.whatsapp.net', message) - def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): + def sendMessageToXMPP(self, buddy, messageContent, timestamp = "", nickname = ""): if timestamp: timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) @@ -417,21 +458,6 @@ class Session(YowsupApp): if receiptRequested: self.call("message_ack", (jid, messageId)) - def onGroupGotInfo(self, gjid, owner, subject, subjectOwner, subjectTimestamp, creationTimestamp): - room = gjid.split("@")[0] - owner = owner.split("@")[0] - subjectOwner = subjectOwner.split("@")[0] - - if room in self.groups: - room = self.groups[room] - room.owner = owner - room.subjectOwner = subjectOwner - room.subject = subject - else: - self.groups[room] = Group(room, owner, subject, subjectOwner) - - self.updateRoomList() - def onGroupGotParticipants(self, gjid, jids): room = gjid.split("@")[0] group = self.groups[room] @@ -470,15 +496,6 @@ class Session(YowsupApp): if receiptRequested: self.call("subject_ack", (gjid, messageId)) # Yowsup Notifications - def onGroupParticipantAdded(self, gjid, jid, author, timestamp, messageId, receiptRequested): - room = gjid.split("@")[0] - buddy = jid.split("@")[0] - - self.logger.info("Added %s to room %s", buddy, room) - - self.backend.handleParticipantChanged(self.user, buddy, room, protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_ONLINE) - if receiptRequested: self.call("notification_ack", (gjid, messageId)) - def onGroupParticipantRemoved(self, gjid, jid, author, timestamp, messageId, receiptRequested): room = gjid.split("@")[0] buddy = jid.split("@")[0] diff --git a/yowsupwrapper.py b/yowsupwrapper.py index ad374a5..8058b10 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -1,4 +1,3 @@ - from yowsup import env from yowsup.stacks import YowStack from yowsup.common import YowConstants @@ -29,13 +28,16 @@ from yowsup.layers.protocol_calls import YowCallsProtocolLayer # ProtocolEntities -from yowsup.layers.protocol_presence.protocolentities import * -from yowsup.layers.protocol_messages.protocolentities import * -from yowsup.layers.protocol_media.protocolentities import * +from yowsup.layers.protocol_acks.protocolentities import * from yowsup.layers.protocol_chatstate.protocolentities import * -from yowsup.layers.protocol_acks.protocolentities import * -from yowsup.layers.protocol_receipts.protocolentities import * +from yowsup.layers.protocol_groups.protocolentities import * +from yowsup.layers.protocol_media.protocolentities import * +from yowsup.layers.protocol_messages.protocolentities import * +from yowsup.layers.protocol_presence.protocolentities import * from yowsup.layers.protocol_profiles.protocolentities import * +from yowsup.layers.protocol_receipts.protocolentities import * + +from functools import partial class YowsupApp(object): def __init__(self): @@ -206,12 +208,11 @@ class YowsupApp(object): - failure: (func) called when request has failed """ iq = LastseenIqProtocolEntity(phoneNumber + '@s.whatsapp.net') - self.sendIq(iq, onSuccess = self._lastSeenSuccess(success), onError = failure) + self.sendIq(iq, onSuccess = partial(self._lastSeenSuccess, success), + onError = failure) - def _lastSeenSuccess(self, success): - def func(response, request): - success(response._from.split('@')[0], response.seconds) - return func + def _lastSeenSuccess(self, success, response, request): + success(response._from.split('@')[0], response.seconds) def requestProfilePicture(self, phoneNumber, onSuccess = None, onFailure = None): """ @@ -223,6 +224,10 @@ class YowsupApp(object): """ iq = GetPictureIqProtocolEntity(phoneNumber + '@s.whatsapp.net') self.sendIq(iq, onSuccess = onSuccess, onError = onFailure) + + def requestGroupsList(self, onSuccess = None, onFailure = None): + iq = ListGroupsIqProtocolEntity() + self.sendIq(iq, onSuccess = onSuccess, onError = onFailure) def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t): """ @@ -322,7 +327,7 @@ class YowsupApp(object): - to: - notify: (str) human readable name of _from (e.g. John Smith) - timestamp: - - participant: + - participant: (str) jid of user who sent the message in a groupchat - offline: - retry: - body: The content of the message