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. 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/Spectrum2/iochannel.py b/Spectrum2/iochannel.py index 0b5ffad..858fc2a 100644 --- a/Spectrum2/iochannel.py +++ b/Spectrum2/iochannel.py @@ -1,13 +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): @@ -27,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/bot.py b/bot.py index 5ef61e5..8712d91 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 @@ -40,10 +40,10 @@ class Bot(): self.session = session self.name = name - self.google = GoogleClient() + # 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 diff --git a/buddy.py b/buddy.py index 8338c11..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,26 +112,28 @@ 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): - 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/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/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) diff --git a/session.py b/session.py index e1d8e61..b86fa97 100644 --- a/session.py +++ b/session.py @@ -27,7 +27,6 @@ import logging import urllib import time -from Yowsup.connectionmanager import YowsupConnectionManager from Spectrum2 import protocol_pb2 from buddy import BuddyList @@ -35,10 +34,12 @@ 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): + super(Session, self).__init__() self.logger = logging.getLogger(self.__class__.__name__) self.logger.info("Created: %s", legacyName) @@ -46,6 +47,8 @@ class Session: 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 = '' @@ -58,76 +61,273 @@ class Session: self.timer = None self.password = None self.initialized = False - - self.buddies = BuddyList(legacyName, db) - self.frontend = YowsupConnectionManager() + self.loggedin = False self.bot = Bot(self) - # Events - self.listen("auth_success", self.onAuthSuccess) - self.listen("auth_fail", self.onAuthFailed) - self.listen("disconnected", self.onDisconnected) - - 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() - 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) + 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.call("disconnect", ("logout",)) + self.loggedin = False + super(Session, self).logout() def login(self, password): - self.password = utils.decodePassword(password) - self.call("auth_login", (self.legacyName, self.password)) + self.loggedin = True + self.password = password + super(Session, self).login(self.legacyName, self.password) def updateRoomList(self): 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): + 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.unsubscribePresence(number) + + 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) + 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))) + if seconds < 60: + self.onPresenceAvailable(number) + else: + self.onPresenceUnavailable(number) + + # 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.sendPresence(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.logger.debug('Disconnected') + 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])) + ) + 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): + self.logger.debug('received ack ' + + ' '.join(map(str, [_id, _class, _from,timestamp,])) + ) + + # Called by superclass + 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) + 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(): + # self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\ + # buddy, self.legacyName, messageContent, timestamp) + # messageContent = "[Broadcast] " + messageContent + + # 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) + + # 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) + + # 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): + 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 + "@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 + "@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) @@ -138,31 +338,34 @@ class Session: elif "-" in sender: # group msg if "/" in sender: room, buddy = sender.split("/") - self.call("message_send", (buddy + "@s.whatsapp.net", message)) + self.sendTextMessage(buddy + '@s.whatsapp.net', message) else: room = sender - group = self.groups[room] +# group = self.groups[room] + +# self.backend.handleMessage(self.user, room, message, group.nick) + self.sendTextMessage(room + '@g.us', message) - self.backend.handleMessage(self.user, room, message, group.nick) - self.call("message_send", (room + "@g.us", message)) else: # private msg buddy = sender - if message == "\\lastseen": - self.presenceRequested.append(buddy) - self.call("presence_request", (buddy + "@s.whatsapp.net",)) - else: - self.call("message_send", (buddy + "@s.whatsapp.net", message)) +# if message == "\\lastseen": +# self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",)) +# else: + 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)) if self.initialized == False: - self.logger.debug("Message queued from %s to %s: %s", buddy, self.legacyName, messageContent) + 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) + 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: @@ -185,14 +388,14 @@ class Session: 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", (statusMessage.encode("utf-8"),)) + self.setStatus(statusMessage.encode('utf-8')) self.logger.info("Status message changed: %s", statusMessage) if self.initialized == False: @@ -207,9 +410,9 @@ class Session: 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): @@ -230,72 +433,22 @@ class Session: 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 updateRoster(self): - self.logger.debug("Update roster") + 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) - 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) - - 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] - - self.logger.info("Media received from %s: %s", buddy, url) - self.sendMessageToXMPP(buddy, utils.shorten(url)) - if receiptRequested: self.call("message_ack", (jid, messageId)) + 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): buddy = jid.split("@")[0] self.logger.info("Location received from %s: %s, %s", buddy, latitude, longitude) @@ -304,75 +457,6 @@ class Session: 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 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 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] - 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] @@ -412,15 +496,6 @@ class Session: 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] - - loggin.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)) - def onGroupParticipantRemoved(self, gjid, jid, author, timestamp, messageId, receiptRequested): room = gjid.split("@")[0] buddy = jid.split("@")[0] diff --git a/transwhat.py b/transwhat.py index 9cda4d9..84abc31 100755 --- a/transwhat.py +++ b/transwhat.py @@ -25,12 +25,14 @@ __status__ = "Prototype" """ import argparse +import traceback import logging import asyncore import sys, os import MySQLdb import e4u import threading +import Queue sys.path.insert(0, os.getcwd()) @@ -38,6 +40,8 @@ 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() @@ -46,11 +50,15 @@ 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() +YowConstants.PATH_STORAGE='/var/lib/spectrum2/' + args.j +loggingfile = '/var/log/spectrum2/' + args.j + '/backends/backend.log' # Logging logging.basicConfig( \ + filename=loggingfile,\ format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s", \ level = logging.DEBUG if args.debug else logging.INFO \ ) @@ -61,10 +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) -asyncore.loop(1) +while True: + 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/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 4aa0b17..b7b9827 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,18 @@ 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 + # + # 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) + usersMessage[buddy] = message def handleJoinRoomRequest(self, user, room, nickname, pasword): self.logger.debug("handleJoinRoomRequest(user=%s, room=%s, nickname=%s)", user, room, nickname) @@ -86,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 @@ -93,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 @@ -103,7 +119,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 new file mode 100644 index 0000000..8058b10 --- /dev/null +++ b/yowsupwrapper.py @@ -0,0 +1,526 @@ +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 +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 + +# ProtocolEntities + +from yowsup.layers.protocol_acks.protocolentities import * +from yowsup.layers.protocol_chatstate.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): + env.CURRENT_ENV = env.S40YowsupEnv() + + layers = (YowsupAppLayer, + 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 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: 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) + self.sendEntity(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.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 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): + """ + Send status to whatsapp + + Args: + - statusTest: (str) Your whatsapp status + """ + iq = SetStatusIqProtocolEntity(statusText) + self.sendIq(iq) + + 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.sendIq(iq, onSuccess = partial(self._lastSeenSuccess, success), + onError = failure) + + def _lastSeenSuccess(self, success, response, request): + success(response._from.split('@')[0], response.seconds) + + 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 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): + """ + 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 + + 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 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: (str) jid of user who sent the message in a groupchat + - 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, + 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 + +class YowsupAppLayer(YowInterfaceLayer): + EVENT_START = 'transwhat.event.YowsupAppLayer.start' + 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 + # 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') + return True + 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 + 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): + # 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(entity.ack()) + + @ProtocolEntityCallback('message') + def onMessageReceived(self, 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): + _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)