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)