diff --git a/README.md b/README.md index 5b204eb..ac537d9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ transWhat is a WhatsApp XMPP Gateway based on [Spectrum 2](http://www.spectrum.i pip install e4u protobuf mysql dateutil - **e4u**: is a simple emoji4unicode python bindings - - **yowsup**: + - [**yowsup**](https://github.com/tgalal/yowsup): is a python library that enables you build application which use WhatsApp service. - **mysqldb**: MySQL client python bindings #### Spectrum 2 @@ -28,6 +28,6 @@ The following persons have contributed major parts of this code: ## Documentation -A project wiki is available [here](http://dev.0l.de/projects/transwhat/start). +A project wiki is available [here](https://dev.0l.de/wiki/projects/transwhat/). An *outdated* writeup of this project is also availabe at my [blog](http://www.steffenvogel.de/2013/06/29/transwhat/). diff --git a/Spectrum2/backend.py b/Spectrum2/backend.py index ec04767..05b498f 100644 --- a/Spectrum2/backend.py +++ b/Spectrum2/backend.py @@ -7,7 +7,7 @@ import logging import google.protobuf def WRAP(MESSAGE, TYPE): - wrap = protocol_pb2.WrapperMessage() + wrap = protocol_pb2.WrapperMessage() wrap.type = TYPE wrap.payload = MESSAGE return wrap.SerializeToString() @@ -25,7 +25,6 @@ class SpectrumBackend: self.m_init_res = 0 self.logger = logging.getLogger(self.__class__.__name__) - def handleMessage(self, user, legacyName, msg, nickname = "", xhtml = "", timestamp = ""): m = protocol_pb2.ConversationMessage() m.userName = user @@ -371,20 +370,19 @@ class SpectrumBackend: self.logger.error("Data too small") return - + packet = self.m_data[4:4+expected_size] wrapper = protocol_pb2.WrapperMessage() - try: - parseFromString = wrapper.ParseFromString(self.m_data[4:]) - except: - parseFromString = True - self.logger.error("Parse from String exception") - - - if (parseFromString == False): + try: + parseFromString = wrapper.ParseFromString(packet) + except: self.m_data = self.m_data[expected_size+4:] - self.logger.error("Parse from String error") + self.logger.error("Parse from String exception") + return + + if parseFromString == False: + self.m_data = self.m_data[expected_size+4:] + self.logger.error("Parse from String failed") return - self.m_data = self.m_data[4+expected_size:] #self.logger.error("Data Type: %s",wrapper.type) diff --git a/session.py b/session.py index d5022ae..c80072f 100644 --- a/session.py +++ b/session.py @@ -73,6 +73,8 @@ class Session(YowsupApp): self.statusMessage = '' self.groups = {} + self.gotGroupList = False + self.joinRoomQueue = [] self.presenceRequested = [] self.offlineQueue = [] self.msgIDs = { } @@ -108,22 +110,27 @@ class Session(YowsupApp): super(Session, self).login(self.legacyName, self.password) def _shortenGroupId(self, gid): - # FIXME: will have problems if number begins with 0 - #return '-'.join(hex(int(s))[2:] for s in gid.split('-')) - return gid + # FIXME: might have problems if number begins with 0 + return gid +# return '-'.join(hex(int(s))[2:] for s in gid.split('-')) def _lengthenGroupId(self, gid): - # FIXME: will have problems if number begins with 0 - #return '-'.join(str(int(s, 16)) for s in gid.split('-')) return gid + # FIXME: might have problems if number begins with 0 +# return '-'.join(str(int(s, 16)) for s in gid.split('-')) def updateRoomList(self): rooms = [] + text = [] for room, group in self.groups.iteritems(): rooms.append([self._shortenGroupId(room), group.subject]) + text.append(self._shortenGroupId(room) + '@' + self.backend.spectrum_jid + ' :' + group.subject) self.logger.debug("Got rooms: %s", rooms) self.backend.handleRoomList(rooms) + message = "Note, you are a participant of the following groups:\n" +\ + '\n'.join(text) + '\nIf you do not join them you will lose messages' + self.bot.send(message) def updateRoster(self): self.logger.debug("Update roster") @@ -174,9 +181,10 @@ class Session(YowsupApp): oroom.subject = subject else: self.groups[room] = Group(room, owner, subject, subjectOwner) - #self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0]) +# self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0]) + self.groups[room].participants = group.getParticipants().keys() - self._addParticipantsToRoom(room, group.getParticipants()) + #self._addParticipantsToRoom(room, group.getParticipants()) if room in self.groupOfflineQueue: while self.groupOfflineQueue[room]: @@ -185,14 +193,17 @@ class Session(YowsupApp): msg[0], "", msg[2]) self.logger.debug("Send queued group message to: %s %s %s", msg[0],msg[1], msg[2]) + self.gotGroupList = True + for room, nick in self.joinRoomQueue: + self.joinRoom(room, nick) + self.joinRoomQueue = [] self.updateRoomList() def joinRoom(self, room, nick): + if not self.gotGroupList: + self.joinRoomQueue.append((room, nick)) + return room = self._lengthenGroupId(room) - if room not in self.groups: - time.sleep(5) - - if room in self.groups: self.logger.info("Joining room: %s room=%s, nick=%s", self.legacyName, room, nick) @@ -204,11 +215,14 @@ class Session(YowsupApp): except KeyError: ownerNick = group.subjectOwner - self.backend.handleSubject(self.user, room, group.subject, - ownerNick) - self.backend.handleRoomNicknameChanged(self.user, room, - group.subject) self._refreshParticipants(room) + self.backend.handleSubject(self.user, self._shortenGroupId(room), + group.subject, ownerNick) + self.logger.debug("Room subject: room=%s, subject=%s", + room, group.subject) + self.backend.handleRoomNicknameChanged( + self.user, self._shortenGroupId(room), group.subject + ) else: self.logger.warn("Room doesn't exist: %s", room) @@ -224,7 +238,6 @@ class Session(YowsupApp): if nick == "": nick = buddy - buddyFull = buddy if buddy == group.owner: flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR else: @@ -232,37 +245,9 @@ class Session(YowsupApp): if buddy == self.legacyName: nick = group.nick flags = flags | protocol_pb2.PARTICIPANT_FLAG_ME - buddyFull = self.user self.backend.handleParticipantChanged( - self.user, buddyFull, self._shortenGroupId(room), flags, - protocol_pb2.STATUS_ONLINE, buddy, nick) - - def _addParticipantsToRoom(self, room, participants): - group = self.groups[room] - group.participants = participants - group.nick = self.user.split("@")[0] - - for jid, _type in participants.iteritems(): - buddy = jid.split("@")[0] - buddyFull = buddy - self.logger.info("Added %s to room %s", buddy, room) - try: - nick = self.buddies[buddy].nick - except KeyError: - nick = buddy - buddyFull = buddy - if _type == 'admin': - flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR - else: - flags = protocol_pb2.PARTICIPANT_FLAG_NONE - if buddy == self.legacyName: - nick = group.nick - flags = flags | protocol_pb2.PARTICIPANT_FLAG_ME - buddyFull = self.user - - self.backend.handleParticipantChanged(self.user, buddyFull, - self._shortenGroupId(room), flags, protocol_pb2.STATUS_ONLINE, buddy, nick) - + self.user, nick, self._shortenGroupId(room), flags, + protocol_pb2.STATUS_ONLINE, buddy) def _lastSeen(self, number, seconds): self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds))) @@ -282,6 +267,7 @@ class Session(YowsupApp): if self.initialized == False: self.sendOfflineMessages() #self.bot.call("welcome") + self.bot.call("welcome") self.initialized = True self.sendPresence(True) self.updateRoster() @@ -336,11 +322,22 @@ class Session(YowsupApp): ) buddy = _from.split('@')[0] messageContent = utils.softToUni(body) - self.sendReceipt(_id, _from, None, participant) + 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: # Group message partname = participant.split('@')[0] + try: + part = self.buddies[partname] + if part.nick == "": + part.nick = notify + self.backend.handleParticipantChanged( + self.user, partname, self._shortenGroupId(buddy), + protocol_pb2.PARTICIPANT_FLAG_NONE, + protocol_pb2.STATUS_ONLINE, "", part.nick + ) # TODO + except KeyError: + self.updateBuddy(partname, notify, []) self.sendGroupMessageToXMPP(buddy, partname, messageContent, timestamp) else: @@ -357,6 +354,8 @@ class Session(YowsupApp): self.logger.debug('Received image message %s', str(image)) buddy = image._from.split('@')[0] participant = image.participant + if image.caption is None: + image.caption = '' message = image.url + ' ' + image.caption if participant is not None: # Group message partname = participant.split('@')[0] @@ -416,6 +415,7 @@ class Session(YowsupApp): self.sendMessageToXMPP(buddy, 'geo:' + latitude + ',' + longitude, location.timestamp) self.sendReceipt(location._id, location._from, None, location.participant) + location.timestamp) # Called by superclass @@ -461,6 +461,28 @@ class Session(YowsupApp): self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, (self.user, buddy)).start() + # Called by superclass + def onAddedToGroup(self, group): + self.logger.debug("Added to group: %s", group) + room = group.getGroupId() + owner = group.getCreatorJid(full = False) + subjectOwner = group.getSubjectOwnerJid(full = False) + subject = utils.softToUni(group.getSubject()) + + self.groups[room] = Group(room, owner, subject, subjectOwner) + self.groups[room].participants = group.getParticipants().keys() +# self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0]) + + #self._addParticipantsToRoom(room, group.getParticipants()) + self.bot.send("You have been added to group: %s@%s (%s)" + % (self._shortenGroupId(room), subject, self.backend.spectrum_jid)) + + def onParticipantsAddedToGroup(self, group): + self.logger.debug("Participants added to group: %s", group) + room = group.getGroupId().split('@')[0] + self.groups[room].participants.extend(group.getParticipants()) + self._refreshParticipants(room) + def onPresenceReceived(self, _type, name, jid, lastseen): self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen) buddy = jid.split("@")[0] @@ -642,7 +664,7 @@ class Session(YowsupApp): "", timestamp) def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""): - self._refreshParticipants(room) + # self._refreshParticipants(room) try: nick = self.buddies[buddy].nick except KeyError: diff --git a/transwhat.py b/transwhat.py index 84abc31..5875dc4 100755 --- a/transwhat.py +++ b/transwhat.py @@ -78,7 +78,7 @@ def connectionClosed(): db = MySQLdb.connect(DB_HOST, DB_USER, DB_PASS, DB_TABLE) io = IOChannel(args.host, args.port, handleTransportData, connectionClosed) -plugin = WhatsAppBackend(io, db) +plugin = WhatsAppBackend(io, db, args.j) while True: try: diff --git a/whatsappbackend.py b/whatsappbackend.py index ae3a6b9..0de395d 100644 --- a/whatsappbackend.py +++ b/whatsappbackend.py @@ -30,12 +30,13 @@ from session import Session import logging class WhatsAppBackend(SpectrumBackend): - def __init__(self, io, db): + def __init__(self, io, db, spectrum_jid): SpectrumBackend.__init__(self) self.logger = logging.getLogger(self.__class__.__name__) self.io = io self.db = db self.sessions = { } + self.spectrum_jid = spectrum_jid # Used to prevent duplicate messages self.lastMessage = {} diff --git a/yowsupwrapper.py b/yowsupwrapper.py index 6369279..978da3f 100644 --- a/yowsupwrapper.py +++ b/yowsupwrapper.py @@ -1,3 +1,5 @@ +import logging + from yowsup import env from yowsup.stacks import YowStack from yowsup.common import YowConstants @@ -70,6 +72,7 @@ class YowsupApp(object): YowStanzaRegulator, YowNetworkLayer ) + self.logger = logging.getLogger(self.__class__.__name__) self.stack = YowStack(layers) self.stack.broadcastEvent( YowLayerEvent(YowsupAppLayer.EVENT_START, caller = self) @@ -111,7 +114,7 @@ class YowsupApp(object): 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) @@ -229,7 +232,7 @@ class YowsupApp(object): jid = phone_number + '@s.whatsapp.net' entity = UnsubscribePresenceProtocolEntity(jid) self.sendEntity(entity) - + def setStatus(self, statusText): """ Send status to whatsapp @@ -374,7 +377,7 @@ class YowsupApp(object): - timestamp """ pass - + def onPresenceReceived(self, _type, name, _from, last): """ Called when presence (e.g. available, unavailable) is received @@ -392,7 +395,7 @@ class YowsupApp(object): """ Called when disconnected from whatsapp """ - + def onContactTyping(self, number): """ Called when contact starts to type @@ -427,7 +430,7 @@ class YowsupApp(object): - body: The content of the message """ pass - + def onImage(self, entity): """ Called when image message is received @@ -445,7 +448,7 @@ class YowsupApp(object): - entity: AudioDownloadableMediaMessageProtocolEntity """ pass - + def onVideo(self, entity): """ @@ -464,7 +467,7 @@ class YowsupApp(object): - entity: LocationMediaMessageProtocolEntity """ pass - + def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant): """ Called when VCard message is received @@ -481,12 +484,20 @@ class YowsupApp(object): """ pass + def onAddedToGroup(self, entity): + """Called when the user has been added to a new group""" + pass + + def onParticipantsAddedToGroup(self, entity): + """Called when participants have been added to a group""" + 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( @@ -511,6 +522,7 @@ class YowsupAppLayer(YowInterfaceLayer): # return True otherwise if layerEvent.getName() == YowsupAppLayer.EVENT_START: self.caller = layerEvent.getArg('caller') + self.logger = logging.getLogger(self.__class__.__name__) return True elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: self.caller.onDisconnect() @@ -575,8 +587,13 @@ class YowsupAppLayer(YowInterfaceLayer): """ Sends ack automatically """ + self.logger.debug("Received notification: %s", entity) self.toLower(entity.ack()) - + if isinstance(entity, CreateGroupsNotificationProtocolEntity): + self.caller.onAddedToGroup(entity) + elif isinstance(entity, AddGroupsNotificationProtocolEntity): + self.caller.onParticipantsAddedToGroup(entity) + @ProtocolEntityCallback('message') def onMessageReceived(self, entity): if entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_TEXT: @@ -621,7 +638,7 @@ class YowsupAppLayer(YowInterfaceLayer): _from = presence.getFrom() last = presence.getLast() self.caller.onPresenceReceived(_type, name, _from, last) - + @ProtocolEntityCallback('chatstate') def onChatstate(self, chatstate): number = chatstate._from.split('@')[0]