Merge branch 'groupchat-fix' of https://github.com/moyamo/transwhat into moyamo-groupchat-fix

Conflicts:
	Spectrum2/backend.py
	session.py
	yowsupwrapper.py
This commit is contained in:
root 2015-11-10 13:24:40 +01:00
commit c5b42044b2
6 changed files with 113 additions and 75 deletions

View file

@ -9,7 +9,7 @@ transWhat is a WhatsApp XMPP Gateway based on [Spectrum 2](http://www.spectrum.i
pip install e4u protobuf mysql dateutil pip install e4u protobuf mysql dateutil
- **e4u**: is a simple emoji4unicode python bindings - **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 - **mysqldb**: MySQL client python bindings
#### Spectrum 2 #### Spectrum 2
@ -28,6 +28,6 @@ The following persons have contributed major parts of this code:
## Documentation ## 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/). An *outdated* writeup of this project is also availabe at my [blog](http://www.steffenvogel.de/2013/06/29/transwhat/).

View file

@ -7,7 +7,7 @@ import logging
import google.protobuf import google.protobuf
def WRAP(MESSAGE, TYPE): def WRAP(MESSAGE, TYPE):
wrap = protocol_pb2.WrapperMessage() wrap = protocol_pb2.WrapperMessage()
wrap.type = TYPE wrap.type = TYPE
wrap.payload = MESSAGE wrap.payload = MESSAGE
return wrap.SerializeToString() return wrap.SerializeToString()
@ -25,7 +25,6 @@ class SpectrumBackend:
self.m_init_res = 0 self.m_init_res = 0
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
def handleMessage(self, user, legacyName, msg, nickname = "", xhtml = "", timestamp = ""): def handleMessage(self, user, legacyName, msg, nickname = "", xhtml = "", timestamp = ""):
m = protocol_pb2.ConversationMessage() m = protocol_pb2.ConversationMessage()
m.userName = user m.userName = user
@ -371,20 +370,19 @@ class SpectrumBackend:
self.logger.error("Data too small") self.logger.error("Data too small")
return return
packet = self.m_data[4:4+expected_size]
wrapper = protocol_pb2.WrapperMessage() wrapper = protocol_pb2.WrapperMessage()
try: try:
parseFromString = wrapper.ParseFromString(self.m_data[4:]) parseFromString = wrapper.ParseFromString(packet)
except: except:
parseFromString = True
self.logger.error("Parse from String exception")
if (parseFromString == False):
self.m_data = self.m_data[expected_size+4:] 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 return
self.m_data = self.m_data[4+expected_size:] self.m_data = self.m_data[4+expected_size:]
#self.logger.error("Data Type: %s",wrapper.type) #self.logger.error("Data Type: %s",wrapper.type)

View file

@ -73,6 +73,8 @@ class Session(YowsupApp):
self.statusMessage = '' self.statusMessage = ''
self.groups = {} self.groups = {}
self.gotGroupList = False
self.joinRoomQueue = []
self.presenceRequested = [] self.presenceRequested = []
self.offlineQueue = [] self.offlineQueue = []
self.msgIDs = { } self.msgIDs = { }
@ -108,22 +110,27 @@ class Session(YowsupApp):
super(Session, self).login(self.legacyName, self.password) super(Session, self).login(self.legacyName, self.password)
def _shortenGroupId(self, gid): def _shortenGroupId(self, gid):
# FIXME: will have problems if number begins with 0 # FIXME: might have problems if number begins with 0
#return '-'.join(hex(int(s))[2:] for s in gid.split('-')) return gid
return gid # return '-'.join(hex(int(s))[2:] for s in gid.split('-'))
def _lengthenGroupId(self, gid): 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 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): def updateRoomList(self):
rooms = [] rooms = []
text = []
for room, group in self.groups.iteritems(): for room, group in self.groups.iteritems():
rooms.append([self._shortenGroupId(room), group.subject]) 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.logger.debug("Got rooms: %s", rooms)
self.backend.handleRoomList(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): def updateRoster(self):
self.logger.debug("Update roster") self.logger.debug("Update roster")
@ -174,9 +181,10 @@ class Session(YowsupApp):
oroom.subject = subject oroom.subject = subject
else: else:
self.groups[room] = Group(room, owner, subject, subjectOwner) 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: if room in self.groupOfflineQueue:
while self.groupOfflineQueue[room]: while self.groupOfflineQueue[room]:
@ -185,14 +193,17 @@ class Session(YowsupApp):
msg[0], "", msg[2]) msg[0], "", msg[2])
self.logger.debug("Send queued group message to: %s %s %s", self.logger.debug("Send queued group message to: %s %s %s",
msg[0],msg[1], msg[2]) msg[0],msg[1], msg[2])
self.gotGroupList = True
for room, nick in self.joinRoomQueue:
self.joinRoom(room, nick)
self.joinRoomQueue = []
self.updateRoomList() self.updateRoomList()
def joinRoom(self, room, nick): def joinRoom(self, room, nick):
if not self.gotGroupList:
self.joinRoomQueue.append((room, nick))
return
room = self._lengthenGroupId(room) room = self._lengthenGroupId(room)
if room not in self.groups:
time.sleep(5)
if room in self.groups: if room in self.groups:
self.logger.info("Joining room: %s room=%s, nick=%s", self.logger.info("Joining room: %s room=%s, nick=%s",
self.legacyName, room, nick) self.legacyName, room, nick)
@ -204,11 +215,14 @@ class Session(YowsupApp):
except KeyError: except KeyError:
ownerNick = group.subjectOwner ownerNick = group.subjectOwner
self.backend.handleSubject(self.user, room, group.subject,
ownerNick)
self.backend.handleRoomNicknameChanged(self.user, room,
group.subject)
self._refreshParticipants(room) 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: else:
self.logger.warn("Room doesn't exist: %s", room) self.logger.warn("Room doesn't exist: %s", room)
@ -224,7 +238,6 @@ class Session(YowsupApp):
if nick == "": if nick == "":
nick = buddy nick = buddy
buddyFull = buddy
if buddy == group.owner: if buddy == group.owner:
flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
else: else:
@ -232,37 +245,9 @@ class Session(YowsupApp):
if buddy == self.legacyName: if buddy == self.legacyName:
nick = group.nick nick = group.nick
flags = flags | protocol_pb2.PARTICIPANT_FLAG_ME flags = flags | protocol_pb2.PARTICIPANT_FLAG_ME
buddyFull = self.user
self.backend.handleParticipantChanged( self.backend.handleParticipantChanged(
self.user, buddyFull, self._shortenGroupId(room), flags, self.user, nick, self._shortenGroupId(room), flags,
protocol_pb2.STATUS_ONLINE, buddy, nick) protocol_pb2.STATUS_ONLINE, buddy)
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)
def _lastSeen(self, number, seconds): def _lastSeen(self, number, seconds):
self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds))) self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds)))
@ -282,6 +267,7 @@ class Session(YowsupApp):
if self.initialized == False: if self.initialized == False:
self.sendOfflineMessages() self.sendOfflineMessages()
#self.bot.call("welcome") #self.bot.call("welcome")
self.bot.call("welcome")
self.initialized = True self.initialized = True
self.sendPresence(True) self.sendPresence(True)
self.updateRoster() self.updateRoster()
@ -336,11 +322,22 @@ class Session(YowsupApp):
) )
buddy = _from.split('@')[0] buddy = _from.split('@')[0]
messageContent = utils.softToUni(body) 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)", self.logger.info("Message received from %s to %s: %s (at ts=%s)",
buddy, self.legacyName, messageContent, timestamp) buddy, self.legacyName, messageContent, timestamp)
if participant is not None: # Group message if participant is not None: # Group message
partname = participant.split('@')[0] 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, self.sendGroupMessageToXMPP(buddy, partname, messageContent,
timestamp) timestamp)
else: else:
@ -357,6 +354,8 @@ class Session(YowsupApp):
self.logger.debug('Received image message %s', str(image)) self.logger.debug('Received image message %s', str(image))
buddy = image._from.split('@')[0] buddy = image._from.split('@')[0]
participant = image.participant participant = image.participant
if image.caption is None:
image.caption = ''
message = image.url + ' ' + image.caption message = image.url + ' ' + image.caption
if participant is not None: # Group message if participant is not None: # Group message
partname = participant.split('@')[0] partname = participant.split('@')[0]
@ -416,6 +415,7 @@ class Session(YowsupApp):
self.sendMessageToXMPP(buddy, 'geo:' + latitude + ',' + longitude, self.sendMessageToXMPP(buddy, 'geo:' + latitude + ',' + longitude,
location.timestamp) location.timestamp)
self.sendReceipt(location._id, location._from, None, location.participant) self.sendReceipt(location._id, location._from, None, location.participant)
location.timestamp)
# Called by superclass # Called by superclass
@ -461,6 +461,28 @@ class Session(YowsupApp):
self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, self.timer = Timer(3, self.backend.handleBuddyStoppedTyping,
(self.user, buddy)).start() (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): def onPresenceReceived(self, _type, name, jid, lastseen):
self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen) self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen)
buddy = jid.split("@")[0] buddy = jid.split("@")[0]
@ -642,7 +664,7 @@ class Session(YowsupApp):
"", timestamp) "", timestamp)
def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""): def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""):
self._refreshParticipants(room) # self._refreshParticipants(room)
try: try:
nick = self.buddies[buddy].nick nick = self.buddies[buddy].nick
except KeyError: except KeyError:

View file

@ -78,7 +78,7 @@ def connectionClosed():
db = MySQLdb.connect(DB_HOST, DB_USER, DB_PASS, DB_TABLE) db = MySQLdb.connect(DB_HOST, DB_USER, DB_PASS, DB_TABLE)
io = IOChannel(args.host, args.port, handleTransportData, connectionClosed) io = IOChannel(args.host, args.port, handleTransportData, connectionClosed)
plugin = WhatsAppBackend(io, db) plugin = WhatsAppBackend(io, db, args.j)
while True: while True:
try: try:

View file

@ -30,12 +30,13 @@ from session import Session
import logging import logging
class WhatsAppBackend(SpectrumBackend): class WhatsAppBackend(SpectrumBackend):
def __init__(self, io, db): def __init__(self, io, db, spectrum_jid):
SpectrumBackend.__init__(self) SpectrumBackend.__init__(self)
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
self.io = io self.io = io
self.db = db self.db = db
self.sessions = { } self.sessions = { }
self.spectrum_jid = spectrum_jid
# Used to prevent duplicate messages # Used to prevent duplicate messages
self.lastMessage = {} self.lastMessage = {}

View file

@ -1,3 +1,5 @@
import logging
from yowsup import env from yowsup import env
from yowsup.stacks import YowStack from yowsup.stacks import YowStack
from yowsup.common import YowConstants from yowsup.common import YowConstants
@ -70,6 +72,7 @@ class YowsupApp(object):
YowStanzaRegulator, YowStanzaRegulator,
YowNetworkLayer YowNetworkLayer
) )
self.logger = logging.getLogger(self.__class__.__name__)
self.stack = YowStack(layers) self.stack = YowStack(layers)
self.stack.broadcastEvent( self.stack.broadcastEvent(
YowLayerEvent(YowsupAppLayer.EVENT_START, caller = self) YowLayerEvent(YowsupAppLayer.EVENT_START, caller = self)
@ -111,7 +114,7 @@ class YowsupApp(object):
Logout from whatsapp Logout from whatsapp
""" """
self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT)) self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT))
def sendReceipt(self, _id, _from, read, participant): def sendReceipt(self, _id, _from, read, participant):
""" """
Send a receipt (delivered: double-tick, read: blue-ticks) Send a receipt (delivered: double-tick, read: blue-ticks)
@ -229,7 +232,7 @@ class YowsupApp(object):
jid = phone_number + '@s.whatsapp.net' jid = phone_number + '@s.whatsapp.net'
entity = UnsubscribePresenceProtocolEntity(jid) entity = UnsubscribePresenceProtocolEntity(jid)
self.sendEntity(entity) self.sendEntity(entity)
def setStatus(self, statusText): def setStatus(self, statusText):
""" """
Send status to whatsapp Send status to whatsapp
@ -374,7 +377,7 @@ class YowsupApp(object):
- timestamp - timestamp
""" """
pass pass
def onPresenceReceived(self, _type, name, _from, last): def onPresenceReceived(self, _type, name, _from, last):
""" """
Called when presence (e.g. available, unavailable) is received Called when presence (e.g. available, unavailable) is received
@ -392,7 +395,7 @@ class YowsupApp(object):
""" """
Called when disconnected from whatsapp Called when disconnected from whatsapp
""" """
def onContactTyping(self, number): def onContactTyping(self, number):
""" """
Called when contact starts to type Called when contact starts to type
@ -427,7 +430,7 @@ class YowsupApp(object):
- body: The content of the message - body: The content of the message
""" """
pass pass
def onImage(self, entity): def onImage(self, entity):
""" """
Called when image message is received Called when image message is received
@ -445,7 +448,7 @@ class YowsupApp(object):
- entity: AudioDownloadableMediaMessageProtocolEntity - entity: AudioDownloadableMediaMessageProtocolEntity
""" """
pass pass
def onVideo(self, entity): def onVideo(self, entity):
""" """
@ -464,7 +467,7 @@ class YowsupApp(object):
- entity: LocationMediaMessageProtocolEntity - entity: LocationMediaMessageProtocolEntity
""" """
pass pass
def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant): def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant):
""" """
Called when VCard message is received Called when VCard message is received
@ -481,12 +484,20 @@ class YowsupApp(object):
""" """
pass 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): def sendEntity(self, entity):
"""Sends an entity down the stack (as if YowsupAppLayer called toLower)""" """Sends an entity down the stack (as if YowsupAppLayer called toLower)"""
self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT, self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT,
entity = entity entity = entity
)) ))
def sendIq(self, iq, onSuccess = None, onError = None): def sendIq(self, iq, onSuccess = None, onError = None):
self.stack.broadcastEvent( self.stack.broadcastEvent(
YowLayerEvent( YowLayerEvent(
@ -511,6 +522,7 @@ class YowsupAppLayer(YowInterfaceLayer):
# return True otherwise # return True otherwise
if layerEvent.getName() == YowsupAppLayer.EVENT_START: if layerEvent.getName() == YowsupAppLayer.EVENT_START:
self.caller = layerEvent.getArg('caller') self.caller = layerEvent.getArg('caller')
self.logger = logging.getLogger(self.__class__.__name__)
return True return True
elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED: elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED:
self.caller.onDisconnect() self.caller.onDisconnect()
@ -575,8 +587,13 @@ class YowsupAppLayer(YowInterfaceLayer):
""" """
Sends ack automatically Sends ack automatically
""" """
self.logger.debug("Received notification: %s", entity)
self.toLower(entity.ack()) self.toLower(entity.ack())
if isinstance(entity, CreateGroupsNotificationProtocolEntity):
self.caller.onAddedToGroup(entity)
elif isinstance(entity, AddGroupsNotificationProtocolEntity):
self.caller.onParticipantsAddedToGroup(entity)
@ProtocolEntityCallback('message') @ProtocolEntityCallback('message')
def onMessageReceived(self, entity): def onMessageReceived(self, entity):
if entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_TEXT: if entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_TEXT:
@ -621,7 +638,7 @@ class YowsupAppLayer(YowInterfaceLayer):
_from = presence.getFrom() _from = presence.getFrom()
last = presence.getLast() last = presence.getLast()
self.caller.onPresenceReceived(_type, name, _from, last) self.caller.onPresenceReceived(_type, name, _from, last)
@ProtocolEntityCallback('chatstate') @ProtocolEntityCallback('chatstate')
def onChatstate(self, chatstate): def onChatstate(self, chatstate):
number = chatstate._from.split('@')[0] number = chatstate._from.split('@')[0]