Compare commits

..

No commits in common. "yowsup-3" and "yowsup-2" have entirely different histories.

23 changed files with 5234 additions and 2395 deletions

View file

@ -31,7 +31,7 @@ Configuration
The only important thing for us is the configuration of a XMPP component
(Spectrum 2 in our case). See http://prosody.im/doc/components.
Append the following at the end of ``/etc/prosody/conf.d/transwhat.cfg.lua``
Append the following at the end of ``/etc/prosody/prosody.cfg.lua``
::
@ -53,7 +53,7 @@ http://spectrum.im/documentation/installation/from\_source\_code.html.
Configuration
~~~~~~~~~~~~~
Create a new file ``/etc/spectrum2/transports/transwhat.cfg`` with the
Create a new file ``/etc/spectrum2/transports/whatsapp.cfg`` with the
following content:
::
@ -104,12 +104,14 @@ Install required dependencies:
::
$ pip install --pre protobuf python-dateutil yowsup2
$ pip install --pre e4u protobuf python-dateutil yowsup2
- e4u_: is a simple emoji4unicode python bindings
- yowsup_: is a python library that enables you build application
which use WhatsApp service.
.. _Spectrum 2: http://www.spectrum.im
.. _Yowsup 3: https://github.com/tgalal/yowsup
.. _Yowsup 2: https://github.com/tgalal/yowsup
.. _Github: https://github.com/hanzz/libtransport
.. _yowsup: https://github.com/tgalal/yowsup
.. _e4u: https://pypi.python.org/pypi/e4u

1
MANIFEST.in Normal file
View file

@ -0,0 +1 @@
include *.md

View file

@ -1,6 +1,7 @@
transpub
transWhat
=========
transWhat is a WhatsApp XMPP Gateway based on `Spectrum 2`_ and `Yowsup 2`_.
Support
-------
@ -10,11 +11,11 @@ For support and discussions please join the XMPP MUC: **transwhat@conference.0l.
Features
--------
- notifications
- Receive data on pubsub extension xep
- Typing notifications
- Receive images, audio & video
- Set/get online status
- Set status message
- Groupchats
Installation
------------
@ -27,9 +28,31 @@ Users find details on the `Usage`_ page.
Branches
--------
- `yowsup-3`_ Update to @tgalals new Yowsup 3
- `yowsup-1`_ My original version which is based on @tgalal first
Yowsup version (**deprecated** and broken).
- `yowsup-2`_ Major rewrite from @moyamo for @tgalals new Yowsup 2
(**recommended**).
For production, please use the ``yowsup-2`` branch.
Contributors
------------
Pull requests, bug reports etc. are welcome. Help us to provide a open
implementation of the WhatsApp protocol.
The following persons have contributed major parts of this code:
- @stv0g (Steffen Vogel): Idea and initial implementation based on
Yowsup 1
- @moyamo (Mohammed Yaseen Mowzer): Port to Yowsup 2
- @DaZZZl: Improvements to group chats, media & message receipts
License
-------
transWhat is licensed under the GPLv3_ license.
Links
-----
@ -37,6 +60,11 @@ Links
- An *outdated* writeup of this project is also availabe at my `blog`_.
.. _Spectrum 2: http://www.spectrum.im
.. _Yowsup 2: https://github.com/tgalal/yowsup
.. _yowsup-1: http://github.com/stv0g/transwhat/tree/yowsup-1
.. _yowsup-2: http://github.com/stv0g/transwhat/tree/yowsup-2
.. _Installation: INSTALL.rst
.. _Usage: USAGE.rst
.. _GPLv3: COPYING.rst
.. _here: https://dev.0l.de/wiki/projects/transwhat/
.. _blog: http://www.steffenvogel.de/2013/06/29/transwhat/

0
Spectrum2/__init__.py Normal file
View file

659
Spectrum2/backend.py Normal file
View file

@ -0,0 +1,659 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
import protocol_pb2
import socket
import struct
import sys
import os
import logging
import google.protobuf
import resource
def WRAP(MESSAGE, TYPE):
wrap = protocol_pb2.WrapperMessage()
wrap.type = TYPE
wrap.payload = bytes(MESSAGE)
return wrap.SerializeToString()
class SpectrumBackend:
"""
Creates new NetworkPlugin and connects the Spectrum2 NetworkPluginServer.
@param loop: Event loop.
@param host: Host where Spectrum2 NetworkPluginServer runs.
@param port: Port.
"""
def __init__(self):
self.m_pingReceived = False
self.m_data = bytes("")
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
m.buddyName = legacyName
m.message = msg
m.nickname = nickname
m.xhtml = xhtml
m.timestamp = str(timestamp)
message = WRAP(m.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_CONV_MESSAGE)
self.send(message)
def handleMessageAck(self, user, legacyName, ID):
m = protocol_pb2.ConversationMessage()
m.userName = user
m.buddyName = legacyName
m.message = ""
m.id = ID
message = WRAP(m.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_CONV_MESSAGE_ACK)
self.send(message)
def handleAttention(self, user, buddyName, msg):
m = protocol_pb2.ConversationMessage()
m.userName = user
m.buddyName = buddyName
m.message = msg
message = WRAP(m.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_ATTENTION)
self.send(message)
def handleVCard(self, user, ID, legacyName, fullName, nickname, photo):
vcard = protocol_pb2.VCard()
vcard.userName = user
vcard.buddyName = legacyName
vcard.id = ID
vcard.fullname = fullName
vcard.nickname = nickname
vcard.photo = bytes(photo)
message = WRAP(vcard.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_VCARD)
self.send(message)
def handleSubject(self, user, legacyName, msg, nickname = ""):
m = protocol_pb2.ConversationMessage()
m.userName = user
m.buddyName = legacyName
m.message = msg
m.nickname = nickname
message = WRAP(m.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_ROOM_SUBJECT_CHANGED)
self.send(message)
def handleBuddyChanged(self, user, buddyName, alias, groups, status, statusMessage = "", iconHash = "", blocked = False):
buddy = protocol_pb2.Buddy()
buddy.userName = user
buddy.buddyName = buddyName
buddy.alias = alias
buddy.group.extend(groups)
buddy.status = status
buddy.statusMessage = statusMessage
buddy.iconHash = iconHash
buddy.blocked = blocked
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_CHANGED)
self.send(message)
def handleBuddyRemoved(self, user, buddyName):
buddy = protocol_pb2.Buddy()
buddy.userName = user
buddy.buddyName = buddyName
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_REMOVED)
self.send(message);
def handleBuddyTyping(self, user, buddyName):
buddy = protocol_pb2.Buddy()
buddy.userName = user
buddy.buddyName = buddyName
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPING)
self.send(message);
def handleBuddyTyped(self, user, buddyName):
buddy = protocol_pb2.Buddy()
buddy.userName = user
buddy.buddyName = buddyName
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPED)
self.send(message);
def handleBuddyStoppedTyping(self, user, buddyName):
buddy = protocol_pb2.Buddy()
buddy.userName = user
buddy.buddyName = buddyName
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BUDDY_STOPPED_TYPING)
self.send(message)
def handleAuthorization(self, user, buddyName):
buddy = protocol_pb2.Buddy()
buddy.userName = user
buddy.buddyName = buddyName
message = WRAP(buddy.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_AUTH_REQUEST)
self.send(message)
def handleConnected(self, user):
d = protocol_pb2.Connected()
d.user = user
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_CONNECTED)
self.send(message);
def handleDisconnected(self, user, error = 0, msg = ""):
d = protocol_pb2.Disconnected()
d.user = user
d.error = error
d.message = msg
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_DISCONNECTED)
self.send(message);
def handleParticipantChanged(self, user, nickname, room, flags, status, statusMessage = "", newname = "", iconHash = ""):
d = protocol_pb2.Participant()
d.userName = user
d.nickname = nickname
d.room = room
d.flag = flags
d.newname = newname
d.iconHash = iconHash
d.status = status
d.statusMessage = statusMessage
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_PARTICIPANT_CHANGED)
self.send(message);
def handleRoomNicknameChanged(self, user, r, nickname):
room = protocol_pb2.Room()
room.userName = user
room.nickname = nickname
room.room = r
room.password = ""
message = WRAP(room.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_ROOM_NICKNAME_CHANGED)
self.send(message);
def handleRoomList(self, rooms):
roomList = protocol_pb2.RoomList()
for room in rooms:
roomList.room.append(room[0])
roomList.name.append(room[1])
message = WRAP(roomList.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_ROOM_LIST)
self.send(message);
def handleFTStart(self, user, buddyName, fileName, size):
room = protocol_pb2.File()
room.userName = user
room.buddyName = buddyName
room.fileName = fileName
room.size = size
message = WRAP(room.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_START)
self.send(message);
def handleFTFinish(self, user, buddyName, fileName, size, ftid):
room = protocol_pb2.File()
room.userName = user
room.buddyName = buddyName
room.fileName = fileName
room.size = size
# Check later
if ftid != 0:
room.ftID = ftid
message = WRAP(room.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_FINISH)
self.send(message)
def handleFTData(self, ftID, data):
d = protocol_pb2.FileTransferData()
d.ftid = ftID
d.data = bytes(data)
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA);
self.send(message)
def handleBackendConfig(self, data):
"""
data is a dictionary, whose keys are sections and values are a list of
tuples of configuration key and configuration value.
"""
c = protocol_pb2.BackendConfig()
config = []
for section, rest in data.items():
config.append('[%s]' % section)
for key, value in rest:
config.append('%s = %s' % (key, value))
c.config = '\n'.join(config)
message = WRAP(c.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BACKEND_CONFIG);
self.send(message)
def handleQuery(self, command):
c = protocol_pb2.BackendConfig()
c.config = command
message = WRAP(c.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_QUERY);
self.send(message)
def handleLoginPayload(self, data):
payload = protocol_pb2.Login()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleLoginRequest(payload.user, payload.legacyName, payload.password, payload.extraFields)
def handleLogoutPayload(self, data):
payload = protocol_pb2.Logout()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleLogoutRequest(payload.user, payload.legacyName)
def handleStatusChangedPayload(self, data):
payload = protocol_pb2.Status()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleStatusChangeRequest(payload.userName, payload.status, payload.statusMessage)
def handleConvMessagePayload(self, data):
payload = protocol_pb2.ConversationMessage()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleMessageSendRequest(payload.userName, payload.buddyName, payload.message, payload.xhtml, payload.id)
def handleConvMessageAckPayload(self, data):
payload = protocol_pb2.ConversationMessage()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleMessageAckRequest(payload.userName, payload.buddyName, payload.id)
def handleAttentionPayload(self, data):
payload = protocol_pb2.ConversationMessage()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleAttentionRequest(payload.userName, payload.buddyName, payload.message)
def handleFTStartPayload(self, data):
payload = protocol_pb2.File()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleFTStartRequest(payload.userName, payload.buddyName, payload.fileName, payload.size, payload.ftID);
def handleFTFinishPayload(self, data):
payload = protocol_pb2.File()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleFTFinishRequest(payload.userName, payload.buddyName, payload.fileName, payload.size, payload.ftID)
def handleFTPausePayload(self, data):
payload = protocol_pb2.FileTransferData()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleFTPauseRequest(payload.ftID)
def handleFTContinuePayload(self, data):
payload = protocol_pb2.FileTransferData()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleFTContinueRequest(payload.ftID)
def handleJoinRoomPayload(self, data):
payload = protocol_pb2.Room()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleJoinRoomRequest(payload.userName, payload.room, payload.nickname, payload.password)
def handleLeaveRoomPayload(self, data):
payload = protocol_pb2.Room()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleLeaveRoomRequest(payload.userName, payload.room)
def handleVCardPayload(self, data):
payload = protocol_pb2.VCard()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
if payload.HasField('photo'):
self.handleVCardUpdatedRequest(payload.userName, payload.photo, payload.nickname)
elif len(payload.buddyName) > 0:
self.handleVCardRequest(payload.userName, payload.buddyName, payload.id)
def handleBuddyChangedPayload(self, data):
payload = protocol_pb2.Buddy()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
if payload.HasField('blocked'):
self.handleBuddyBlockToggled(payload.userName, payload.buddyName, payload.blocked)
else:
groups = [g for g in payload.group]
self.handleBuddyUpdatedRequest(payload.userName, payload.buddyName, payload.alias, groups);
def handleBuddyRemovedPayload(self, data):
payload = protocol_pb2.Buddy()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
groups = [g for g in payload.group]
self.handleBuddyRemovedRequest(payload.userName, payload.buddyName, groups);
def handleBuddiesPayload(self, data):
payload = protocol_pb2.Buddies()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
self.handleBuddies(payload);
def handleChatStatePayload(self, data, msgType):
payload = protocol_pb2.Buddy()
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
if msgType == protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPING:
self.handleTypingRequest(payload.userName, payload.buddyName)
elif msgType == protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPED:
self.handleTypedRequest(payload.userName, payload.buddyName)
elif msgType == protocol_pb2.WrapperMessage.TYPE_BUDDY_STOPPED_TYPING:
self.handleStoppedTypingRequest(payload.userName, payload.buddyName)
def handleDataRead(self, data):
self.m_data += data
while len(self.m_data) != 0:
expected_size = 0
if (len(self.m_data) >= 4):
expected_size = struct.unpack('!I', self.m_data[0:4])[0]
if (len(self.m_data) - 4 < expected_size):
self.logger.debug("Data packet incomplete")
return
else:
self.logger.debug("Data packet incomplete")
return
packet = self.m_data[4:4+expected_size]
wrapper = protocol_pb2.WrapperMessage()
try:
parseFromString = wrapper.ParseFromString(packet)
except:
self.m_data = self.m_data[expected_size+4:]
self.logger.error("Parse from String exception. Skipping packet.")
return
if parseFromString == False:
self.m_data = self.m_data[expected_size+4:]
self.logger.error("Parse from String failed. Skipping packet.")
return
self.m_data = self.m_data[4+expected_size:]
if wrapper.type == protocol_pb2.WrapperMessage.TYPE_LOGIN:
self.handleLoginPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_LOGOUT:
self.handleLogoutPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_PING:
self.sendPong()
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_CONV_MESSAGE:
self.handleConvMessagePayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_JOIN_ROOM:
self.handleJoinRoomPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_LEAVE_ROOM:
self.handleLeaveRoomPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_VCARD:
self.handleVCardPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_CHANGED:
self.handleBuddyChangedPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_REMOVED:
self.handleBuddyRemovedPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_STATUS_CHANGED:
self.handleStatusChangedPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPING:
self.handleChatStatePayload(wrapper.payload, protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPING)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPED:
self.handleChatStatePayload(wrapper.payload, protocol_pb2.WrapperMessage.TYPE_BUDDY_TYPED)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDY_STOPPED_TYPING:
self.handleChatStatePayload(wrapper.payload, protocol_pb2.WrapperMessage.TYPE_BUDDY_STOPPED_TYPING)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_ATTENTION:
self.handleAttentionPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_FT_START:
self.handleFTStartPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_FT_FINISH:
self.handleFTFinishPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_FT_PAUSE:
self.handleFTPausePayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_FT_CONTINUE:
self.handleFTContinuePayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_EXIT:
self.handleExitRequest()
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_CONV_MESSAGE_ACK:
self.handleConvMessageAckPayload(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_RAW_XML:
self.handleRawXmlRequest(wrapper.payload)
elif wrapper.type == protocol_pb2.WrapperMessage.TYPE_BUDDIES:
self.handleBuddiesPayload(wrapper.payload)
def send(self, data):
header = struct.pack('!I',len(data))
self.sendData(header + data)
def checkPing(self):
if (self.m_pingReceived == False):
self.handleExitRequest()
self.m_pingReceived = False
def sendPong(self):
self.m_pingReceived = True
wrap = protocol_pb2.WrapperMessage()
wrap.type = protocol_pb2.WrapperMessage.TYPE_PONG
message = wrap.SerializeToString()
self.send(message)
self.sendMemoryUsage()
def sendMemoryUsage(self):
stats = protocol_pb2.Stats()
stats.init_res = self.m_init_res
res = 0
shared = 0
e_res, e_shared = self.handleMemoryUsage()
stats.res = res + e_res
stats.shared = shared + e_shared
stats.id = str(os.getpid())
message = WRAP(stats.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_STATS)
self.send(message)
def handleLoginRequest(self, user, legacyName, password, extra):
"""
Called when XMPP user wants to connect legacy network.
You should connect him to legacy network and call handleConnected or handleDisconnected function later.
@param user: XMPP JID of user for which this event occurs.
@param legacyName: Legacy network name of this user used for login.
@param password: Legacy network password of this user.
"""
#\msc
#NetworkPlugin,YourNetworkPlugin,LegacyNetwork;
#NetworkPlugin->YourNetworkPlugin [label="handleLoginRequest(...)", URL="\ref NetworkPlugin::handleLoginRequest()"];
#YourNetworkPlugin->LegacyNetwork [label="connect the legacy network"];
#--- [label="If password was valid and user is connected and logged in"];
#YourNetworkPlugin<-LegacyNetwork [label="connected"];
#YourNetworkPlugin->NetworkPlugin [label="handleConnected()", URL="\ref NetworkPlugin::handleConnected()"];
#--- [label="else"];
#YourNetworkPlugin<-LegacyNetwork [label="disconnected"];
#YourNetworkPlugin->NetworkPlugin [label="handleDisconnected()", URL="\ref NetworkPlugin::handleDisconnected()"];
#\endmsc
raise NotImplementedError, "Implement me"
def handleBuddies(self, buddies):
pass
def handleLogoutRequest(self, user, legacyName):
"""
Called when XMPP user wants to disconnect legacy network.
You should disconnect him from legacy network.
@param user: XMPP JID of user for which this event occurs.
@param legacyName: Legacy network name of this user used for login.
"""
raise NotImplementedError, "Implement me"
def handleMessageSendRequest(self, user, legacyName, message, xhtml = "", ID = 0):
"""
Called when XMPP user sends message to legacy network.
@param user: XMPP JID of user for which this event occurs.
@param legacyName: Legacy network name of buddy or room.
@param message: Plain text message.
@param xhtml: XHTML message.
@param ID: message ID
"""
raise NotImplementedError, "Implement me"
def handleMessageAckRequest(self, user, legacyName, ID = 0):
"""
Called when XMPP user sends message to legacy network.
@param user: XMPP JID of user for which this event occurs.
@param legacyName: Legacy network name of buddy or room.
@param ID: message ID
"""
# raise NotImplementedError, "Implement me"
pass
def handleVCardRequest(self, user, legacyName, ID):
""" Called when XMPP user requests VCard of buddy.
@param user: XMPP JID of user for which this event occurs.
@param legacyName: Legacy network name of buddy whose VCard is requested.
@param ID: ID which is associated with this request. You have to pass it to handleVCard function when you receive VCard."""
#\msc
#NetworkPlugin,YourNetworkPlugin,LegacyNetwork;
#NetworkPlugin->YourNetworkPlugin [label="handleVCardRequest(...)", URL="\ref NetworkPlugin::handleVCardRequest()"];
#YourNetworkPlugin->LegacyNetwork [label="start VCard fetching"];
#YourNetworkPlugin<-LegacyNetwork [label="VCard fetched"];
#YourNetworkPlugin->NetworkPlugin [label="handleVCard()", URL="\ref NetworkPlugin::handleVCard()"];
#\endmsc
pass
def handleVCardUpdatedRequest(self, user, photo, nickname):
"""
Called when XMPP user updates his own VCard.
You should update the VCard in legacy network too.
@param user: XMPP JID of user for which this event occurs.
@param photo: Raw photo data.
"""
pass
def handleJoinRoomRequest(self, user, room, nickname, pasword):
pass
def handleLeaveRoomRequest(self, user, room):
pass
def handleStatusChangeRequest(self, user, status, statusMessage):
pass
def handleBuddyUpdatedRequest(self, user, buddyName, alias, groups):
pass
def handleBuddyRemovedRequest(self, user, buddyName, groups):
pass
def handleBuddyBlockToggled(self, user, buddyName, blocked):
pass
def handleTypingRequest(self, user, buddyName):
pass
def handleTypedRequest(self, user, buddyName):
pass
def handleStoppedTypingRequest(self, user, buddyName):
pass
def handleAttentionRequest(self, user, buddyName, message):
pass
def handleFTStartRequest(self, user, buddyName, fileName, size, ftID):
pass
def handleFTFinishRequest(self, user, buddyName, fileName, size, ftID):
pass
def handleFTPauseRequest(self, ftID):
pass
def handleFTContinueRequest(self, ftID):
pass
def handleMemoryUsage(self):
return (resource.getrusage(resource.RUSAGE_SELF).ru_maxrss, 0)
def handleExitRequest(self):
sys.exit(1)
def handleRawXmlRequest(self, xml):
pass
def sendData(self, data):
pass

141
Spectrum2/config.py Normal file
View file

@ -0,0 +1,141 @@
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
# I'm guessing this is the format of the spectrum config file in BNF
# <config_file> ::= <line>*
# <line> ::= <space>* <expr> <space>* <newline> | <space*>
# <expr> ::= <section> | <assignment>
# <section> ::= [<identifier>*]
# <assignment> ::= <identifier> <space>* = <space>* <value>
class SpectrumConfig:
"""
Represents spectrum2 configuration options.
"""
def __init__(self, path_to_config_file):
"""
Initialises configuration file.
Args:
path_to_config_file: The absolute path to the configuration file.
"""
self.config_path = path_to_config_file
self.options = self.loadConfig(self.config_path)
# Load backend_logging information
self.options.update(self.loadConfig(self['logging.backend_config']))
def loadConfig(self, file_name):
section = {'a': ""} # Current section heading,
# It's a dictionary because variables in python closures can't be
# assigned to.
options = dict()
# Recursive descent parser
def consume_spaces(line):
i = 0
for c in line:
if c != ' ':
break
i += 1
return line[i:]
def read_identifier(line):
i = 0
for c in line:
if c == ' ' or c==']' or c=='[' or c=='=':
break
i += 1
# no identifier
if i == 0:
return (None, 'No identifier')
return (line[:i], line[i:])
def parse_section(line):
if len(line) == 0 or line[0] != '[':
return (None, 'expected [')
line = line[1:]
identifier, line = read_identifier(line)
if len(line) == 0 or line[0] != ']' or identifier is None:
return (None, line)
return (identifier, line[1:])
def parse_assignment(line):
key, line = read_identifier(line)
if key is None:
return (None, None, line)
line = consume_spaces(line)
if len(line) == 0 or line[0] != '=':
return (None, None, 'Expected =')
line = consume_spaces(line[1:])
value = line[:-1]
return (key, value, '\n')
def expr(line):
sec, newline = parse_section(line)
if sec is not None:
section['a'] = sec
else:
key, value, newline = parse_assignment(line)
if key is not None:
if section['a'] != '':
options[section['a'] + '.' + key] = value
else:
options[key] = value
else:
return (None, newline)
return (newline, None)
def parse_line(line, line_number):
line = consume_spaces(line)
if line == '\n':
return
newline, error = expr(line)
if newline is None:
raise ConfigParseError(str(line_number) + ': ' + error + ': ' + repr(line))
newline = consume_spaces(newline)
if newline != '\n':
raise ConfigParseError(str(line_number) + ': Expected newline got ' + repr(newline))
def strip_comments(line):
i = 0
for c in line:
if c == '#' or c == '\n':
break
i += 1
return line[:i] + '\n'
with open(file_name, 'r') as f:
i = 1
while True:
line = f.readline()
if line == '':
break
parse_line(strip_comments(line), i)
i += 1
return options
def __getitem__(self, key):
return self.options[key]
class ConfigParseError(Exception):
pass

69
Spectrum2/iochannel.py Normal file
View file

@ -0,0 +1,69 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
import asyncore, socket
import logging
import sys
class IOChannel(asyncore.dispatcher):
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 = bytes("")
def sendData(self, data):
self.buffer += data
def handle_connect(self):
pass
def handle_close(self):
self.close()
def handle_read(self):
data = self.recv(65536)
self.callback(data)
def handle_write(self):
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)
def readable(self):
return True

203
Spectrum2/protocol.proto Normal file
View file

@ -0,0 +1,203 @@
package pbnetwork;
enum ConnectionError {
CONNECTION_ERROR_NETWORK_ERROR = 0;
CONNECTION_ERROR_INVALID_USERNAME = 1;
CONNECTION_ERROR_AUTHENTICATION_FAILED = 2;
CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE = 3;
CONNECTION_ERROR_NO_SSL_SUPPORT = 4;
CONNECTION_ERROR_ENCRYPTION_ERROR = 5;
CONNECTION_ERROR_NAME_IN_USE = 6;
CONNECTION_ERROR_INVALID_SETTINGS = 7;
CONNECTION_ERROR_CERT_NOT_PROVIDED = 8;
CONNECTION_ERROR_CERT_UNTRUSTED = 9;
CONNECTION_ERROR_CERT_EXPIRED = 10;
CONNECTION_ERROR_CERT_NOT_ACTIVATED = 11;
CONNECTION_ERROR_CERT_HOSTNAME_MISMATCH = 12;
CONNECTION_ERROR_CERT_FINGERPRINT_MISMATCH = 13;
CONNECTION_ERROR_CERT_SELF_SIGNED = 14;
CONNECTION_ERROR_CERT_OTHER_ERROR = 15;
CONNECTION_ERROR_OTHER_ERROR = 16;
}
enum StatusType {
STATUS_ONLINE = 0;
STATUS_AWAY = 1;
STATUS_FFC = 2;
STATUS_XA = 3;
STATUS_DND = 4;
STATUS_NONE = 5;
STATUS_INVISIBLE = 6;
}
message Connected {
required string user = 1;
}
message Disconnected {
required string user = 1;
required int32 error = 2;
optional string message = 3;
}
message Login {
required string user = 1;
required string legacyName = 2;
required string password = 3;
repeated string extraFields = 4;
}
message Logout {
required string user = 1;
required string legacyName = 2;
}
message Buddy {
required string userName = 1;
required string buddyName = 2;
optional string alias = 3;
repeated string group = 4;
optional StatusType status = 5;
optional string statusMessage = 6;
optional string iconHash = 7;
optional bool blocked = 8;
}
message Buddies {
repeated Buddy buddy = 1;
}
message ConversationMessage {
required string userName = 1;
required string buddyName = 2;
required string message = 3;
optional string nickname = 4;
optional string xhtml = 5;
optional string timestamp = 6;
optional bool headline = 7;
optional string id = 8;
optional bool pm = 9;
}
message Room {
required string userName = 1;
required string nickname = 2;
required string room = 3;
optional string password = 4;
}
message RoomList {
repeated string room = 1;
repeated string name = 2;
optional string user = 3;
}
enum ParticipantFlag {
PARTICIPANT_FLAG_NONE = 0;
PARTICIPANT_FLAG_MODERATOR = 1;
PARTICIPANT_FLAG_CONFLICT = 2;
PARTICIPANT_FLAG_BANNED = 4;
PARTICIPANT_FLAG_NOT_AUTHORIZED = 8;
PARTICIPANT_FLAG_ME = 16;
PARTICIPANT_FLAG_KICKED = 32;
PARTICIPANT_FLAG_ROOM_NOT_FOUND = 64;
}
message Participant {
required string userName = 1;
required string room = 2;
required string nickname = 3;
required int32 flag = 4;
required StatusType status = 5;
optional string statusMessage = 6;
optional string newname = 7;
optional string iconHash = 8;
optional string alias = 9;
}
message VCard {
required string userName = 1;
required string buddyName = 2;
required int32 id = 3;
optional string fullname = 4;
optional string nickname = 5;
optional bytes photo = 6;
}
message Status {
required string userName = 1;
required StatusType status = 3;
optional string statusMessage = 4;
}
message Stats {
required int32 res = 1;
required int32 init_res = 2;
required int32 shared = 3;
required string id = 4;
}
message File {
required string userName = 1;
required string buddyName = 2;
required string fileName = 3;
required int32 size = 4;
optional int32 ftID = 5;
}
message FileTransferData {
required int32 ftID = 1;
required bytes data = 2;
}
message BackendConfig {
required string config = 1;
}
message APIVersion {
required int32 version = 1;
}
message WrapperMessage {
enum Type {
TYPE_CONNECTED = 1;
TYPE_DISCONNECTED = 2;
TYPE_LOGIN = 3;
TYPE_LOGOUT = 4;
TYPE_BUDDY_CHANGED = 6;
TYPE_BUDDY_REMOVED = 7;
TYPE_CONV_MESSAGE = 8;
TYPE_PING = 9;
TYPE_PONG = 10;
TYPE_JOIN_ROOM = 11;
TYPE_LEAVE_ROOM = 12;
TYPE_PARTICIPANT_CHANGED = 13;
TYPE_ROOM_NICKNAME_CHANGED = 14;
TYPE_ROOM_SUBJECT_CHANGED = 15;
TYPE_VCARD = 16;
TYPE_STATUS_CHANGED = 17;
TYPE_BUDDY_TYPING = 18;
TYPE_BUDDY_STOPPED_TYPING = 19;
TYPE_BUDDY_TYPED = 20;
TYPE_AUTH_REQUEST = 21;
TYPE_ATTENTION = 22;
TYPE_STATS = 23;
TYPE_FT_START = 24;
TYPE_FT_FINISH = 25;
TYPE_FT_DATA = 26;
TYPE_FT_PAUSE = 27;
TYPE_FT_CONTINUE = 28;
TYPE_EXIT = 29;
TYPE_BACKEND_CONFIG = 30;
TYPE_QUERY = 31;
TYPE_ROOM_LIST = 32;
TYPE_CONV_MESSAGE_ACK = 33;
TYPE_RAW_XML = 34;
TYPE_BUDDIES = 35;
TYPE_API_VERSION = 36;
}
required Type type = 1;
optional bytes payload = 2;
}
;

1452
Spectrum2/protocol_pb2.py Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,4 +0,0 @@
Component "whatsapp.0l.de"
component_secret = "whatsappsucks"
component_ports = { 5221 }
component_interface = "127.0.0.1"

View file

@ -1,30 +0,0 @@
[service]
user = spectrum
group = spectrum
jid = whatsapp.0l.de
server = localhost
password = whatsappsucks
port = 5221
backend_host = localhost
backend = /usr/bin/transwhat
users_per_backend = 10
more_resources = 1
admin_jid = your@jid.example
[identity]
name = transWhat
type = xmpp
category = gateway
[logging]
config = /etc/spectrum2/logging.cfg
backend_config = /etc/spectrum2/backend-logging.cfg
[database]
type = sqlite3

View file

@ -4,14 +4,17 @@ import os
import codecs
from setuptools import setup
def read_file(filename, encoding='utf8'):
"""Read unicode from given file."""
with codecs.open(filename, encoding=encoding) as fd:
return fd.read()
here = os.path.abspath(os.path.dirname(__file__))
readme = read_file(os.path.join(here, 'README.rst'))
setup(name='transwhat',
version='0.2.2',
description='A gateway between the XMPP and the WhatsApp IM networks',
@ -20,7 +23,6 @@ setup(name='transwhat',
url='https://github.com/stv0g/transwhat',
author='Steffen Vogel',
author_email='stv0g@0l.de',
python_requires='>=3.5',
classifiers=[
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Development Status :: 4 - Beta',
@ -30,16 +32,18 @@ setup(name='transwhat',
],
license='GPL-3+',
packages=[
'transWhat'
'transWhat',
'Spectrum2'
],
scripts=[
'transWhat/transwhat.py'
],
install_requires=[
'protobuf',
'yowsup',
'pyspectrum2',
'python-dateutil',
'yowsup2',
'e4u',
'Pillow',
'python-dateutil'
],
entry_points={
'console_scripts': ['transwhat=transWhat.transwhat:main'],

View file

@ -1,9 +1,36 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
import threading
import inspect
import re
import urllib
import time
import os
import utils
class Bot():
def __init__(self, session, name = "Bot"):
@ -47,13 +74,13 @@ class Bot():
# commands
def _help(self):
self.send("""following bot commands are available:
\\help show thi
\\help show this message
following user commands are available:
\\lastseen
\\lastseen request last online timestamp from buddy
following group commands are available
\\leave permanentlhat
\\leave permanently leave group chat
\\groups print all attended groups
\\getgroups get current groups from WA""")

View file

@ -1,10 +1,39 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
from Spectrum2 import protocol_pb2
import logging
import time
import utils
import base64
import hashlib
import Spectrum2
from . import deferred
import deferred
from deferred import call
class Buddy():
def __init__(self, owner, number, nick, statusMsg, groups, image_hash):
@ -88,7 +117,7 @@ class BuddyList(dict):
if status is None:
buddy.statusMsg = ""
else:
buddy.statusMsg = status
buddy.statusMsg = utils.softToUni(status)
self.updateSpectrum(buddy)
def load(self, buddies):
@ -115,11 +144,11 @@ class BuddyList(dict):
def updateSpectrum(self, buddy):
if buddy.presence == 0:
status = Spectrum2.protocol_pb2.STATUS_NONE
status = protocol_pb2.STATUS_NONE
elif buddy.presence == 'unavailable':
status = Spectrum2.protocol_pb2.STATUS_AWAY
status = protocol_pb2.STATUS_AWAY
else:
status = Spectrum2.protocol_pb2.STATUS_ONLINE
status = protocol_pb2.STATUS_ONLINE
statusmsg = buddy.statusMsg
if buddy.lastseen != 0:
@ -139,7 +168,7 @@ class BuddyList(dict):
buddy = self[number]
del self[number]
self.backend.handleBuddyChanged(self.user, number, "", [],
Spectrum2.protocol_pb2.STATUS_NONE)
protocol_pb2.STATUS_NONE)
self.backend.handleBuddyRemoved(self.user, number)
self.session.unsubscribePresence(number)
# TODO Sync remove
@ -177,9 +206,9 @@ class BuddyList(dict):
pictureData = response.pictureData()
# Send VCard
if ID != None:
deferred.call(self.logger.debug, 'Sending VCard (%s) with image id %s: %s' %
call(self.logger.debug, 'Sending VCard (%s) with image id %s: %s' %
(ID, response.pictureId(), pictureData.then(base64.b64encode)))
deferred.call(self.backend.handleVCard, self.user, ID, buddy, "", "",
call(self.backend.handleVCard, self.user, ID, buddy, "", "",
pictureData)
# If error
error.when(self.logger.debug, 'Sending VCard (%s) without image' % ID)
@ -194,14 +223,9 @@ class BuddyList(dict):
except KeyError:
nick = ""
groups = []
def sha1hash(data):
hashlib.sha1(data).hexdigest()
image_hash = pictureData.then(sha1hash)
deferred.call(self.logger.debug, 'Image hash is %s' % image_hash)
deferred.call(self.update, buddynr, nick, groups, image_hash)
image_hash = pictureData.then(utils.sha1hash)
call(self.logger.debug, 'Image hash is %s' % image_hash)
call(self.update, buddynr, nick, groups, image_hash)
# No image
error.when(self.logger.debug, 'No image')
error.when(self.update, buddynr, nick, groups, '')

View file

@ -1,3 +1,29 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
from functools import partial
class Deferred(object):

View file

@ -1,4 +1,30 @@
import Spectrum2
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
from Spectrum2 import protocol_pb2
class Group():
@ -39,40 +65,40 @@ class Group():
def sendParticipantsToSpectrum(self, yourNumber):
for number, nick in self.participants.iteritems():
if number == self.owner:
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_MODERATOR
flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
else:
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_NONE
flags = protocol_pb2.PARTICIPANT_FLAG_NONE
if number == yourNumber:
flags = flags | Spectrum2.protocol_pb2.PARTICIPANT_FLAG_ME
flags = flags | protocol_pb2.PARTICIPANT_FLAG_ME
try:
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_ONLINE,
self._updateParticipant(number, flags, protocol_pb2.STATUS_ONLINE,
self.backend.sessions[self.user].buddies[number].image_hash)
except KeyError:
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_ONLINE)
self._updateParticipant(number, flags, protocol_pb2.STATUS_ONLINE)
def removeParticipants(self, participants):
for jid in participants:
number = jid.split('@')[0]
nick = self.participants[number]
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_NONE
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_NONE)
flags = protocol_pb2.PARTICIPANT_FLAG_NONE
self._updateParticipant(number, flags, protocol_pb2.STATUS_NONE)
del self.participants[number]
def leaveRoom(self):
for number in self.participants:
nick = self.participants[number]
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_ROOM_NOT_FOUND
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_NONE)
flags = protocol_pb2.PARTICIPANT_FLAG_ROOM_NOT_FOUND
self._updateParticipant(number, flags, protocol_pb2.STATUS_NONE)
def changeNick(self, number, new_nick):
if self.participants[number] == new_nick:
return
if number == self.owner:
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_MODERATOR
flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
else:
flags = Spectrum2.protocol_pb2.PARTICIPANT_FLAG_NONE
self._updateParticipant(number, flags, Spectrum2.protocol_pb2.STATUS_ONLINE, new_nick)
flags = protocol_pb2.PARTICIPANT_FLAG_NONE
self._updateParticipant(number, flags, protocol_pb2.STATUS_ONLINE, new_nick)
self.participants[number] = new_nick
def _updateParticipant(self, number, flags, status, imageHash = "", newNick = ""):

View file

@ -1,11 +1,35 @@
import sys
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
from Spectrum2 import protocol_pb2
from yowsupwrapper import YowsupApp
import logging
import Spectrum2
from .yowsupwrapper import YowsupApp
from . import threadutils
import threadutils
import sys
class RegisterSession(YowsupApp):
"""
@ -24,7 +48,7 @@ class RegisterSession(YowsupApp):
def login(self, password=""):
self.backend.handleConnected(self.user)
self.backend.handleBuddyChanged(self.user, 'bot', 'bot',
['Admin'], Spectrum2.protocol_pb2.STATUS_ONLINE)
['Admin'], protocol_pb2.STATUS_ONLINE)
self.backend.handleMessage(self.user, 'bot',
'Please enter your country code')

View file

@ -1,35 +1,53 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
import utils
import logging
import urllib
import time
# from PIL import Image
import sys
import os
reload(sys)
sys.setdefaultencoding("utf-8")
from yowsup.layers.protocol_media.mediauploader import MediaUploader
from yowsup.layers.protocol_media.mediadownloader import MediaDownloader
import Spectrum2
from Spectrum2 import protocol_pb2
from . import deferred
from .buddy import BuddyList
from .group import Group
from .bot import Bot
from .yowsupwrapper import YowsupApp
def ago(secs):
periods = ["second", "minute", "hour", "day", "week", "month", "year", "decade"]
lengths = [60, 60, 24, 7,4.35, 12, 10]
j = 0
diff = secs
while diff >= lengths[j]:
diff /= lengths[j]
diff = round(diff)
j += 1
period = periods[j]
if diff > 1: period += "s"
return "%d %s ago" % (diff, period)
from buddy import BuddyList
from threading import Timer
from group import Group
from bot import Bot
import deferred
from deferred import call
from yowsupwrapper import YowsupApp
class MsgIDs:
@ -50,7 +68,7 @@ class Session(YowsupApp):
self.user = user
self.legacyName = legacyName
self.status = Spectrum2.protocol_pb2.STATUS_NONE
self.status = protocol_pb2.STATUS_NONE
self.statusMessage = ''
self.groups = {}
@ -126,7 +144,7 @@ class Session(YowsupApp):
if room not in self.groups:
owner = group.getOwner().split('@')[0]
subjectOwner = group.getSubjectOwner().split('@')[0]
subject = group.getSubject()
subject = utils.softToUni(group.getSubject())
self.groups[room] = Group(room, owner, subject, subjectOwner,
self.backend, self.user)
# add/update room participants
@ -204,7 +222,7 @@ class Session(YowsupApp):
self.backend.handleConnected(self.user)
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name,
["Admin"], Spectrum2.protocol_pb2.STATUS_ONLINE)
["Admin"], protocol_pb2.STATUS_ONLINE)
# Initialisation?
self.requestPrivacyList()
self.requestClientConfig()
@ -261,7 +279,7 @@ class Session(YowsupApp):
def onTextMessage(self, _id, _from, to, notify, timestamp, participant,
offline, retry, body):
buddy = _from.split('@')[0]
messageContent = body
messageContent = utils.softToUni(body)
self.sendReceipt(_id, _from, None, participant)
self.recvMsgIDs.append((_id, _from, participant, timestamp))
self.logger.info("Message received from %s to %s: %s (at ts=%s)" %
@ -412,7 +430,7 @@ class Session(YowsupApp):
self.logger.info("Paused typing: %s" % buddy)
if buddy != 'bot':
self.backend.handleBuddyTyped(self.user, buddy)
self.timer = threading.Timer(3, self.backend.handleBuddyStoppedTyping,
self.timer = Timer(3, self.backend.handleBuddyStoppedTyping,
(self.user, buddy)).start()
# Called by superclass
@ -421,7 +439,7 @@ class Session(YowsupApp):
room = group.getGroupId()
owner = group.getCreatorJid(full = False)
subjectOwner = group.getSubjectOwnerJid(full = False)
subject = group.getSubject()
subject = utils.softToUni(group.getSubject())
self.groups[room] = Group(room, owner, subject, subjectOwner, self.backend, self.user)
self.groups[room].addParticipants(group.getParticipants(), self.buddies, self.legacyName)
@ -664,7 +682,7 @@ class Session(YowsupApp):
def onSuccess(buddy, lastseen):
timestamp = time.localtime(time.localtime()-lastseen)
timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp)
self.sendMessageToXMPP(buddy, "%s (%s) %s" % (timestring, ago(lastseen), str(lastseen)))
self.sendMessageToXMPP(buddy, "%s (%s) %s" % (timestring, utils.ago(lastseen), str(lastseen)))
def onError(errorIqEntity, originalIqEntity):
self.sendMessageToXMPP(errorIqEntity.getFrom(), "LastSeen Error")
@ -738,8 +756,8 @@ class Session(YowsupApp):
self.logger.info("Status changed: %s" % status)
self.status = status
if status == Spectrum2.protocol_pb2.STATUS_ONLINE \
or status == Spectrum2.protocol_pb2.STATUS_FFC:
if status == protocol_pb2.STATUS_ONLINE \
or status == protocol_pb2.STATUS_FFC:
self.sendPresence(True)
else:
self.sendPresence(False)
@ -823,7 +841,7 @@ class Session(YowsupApp):
self.logger.info("Removed %s from room %s" % (buddy, room))
self.backend.handleParticipantChanged(self.user, buddy, room, Spectrum2.protocol_pb2.PARTICIPANT_FLAG_NONE, Spectrum2.protocol_pb2.STATUS_NONE) # TODO
self.backend.handleParticipantChanged(self.user, buddy, room, protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_NONE) # TODO
if receiptRequested: self.call("notification_ack", (gjid, messageId))

View file

@ -1,8 +1,34 @@
import queue
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
import Queue
import threading
# This queue is for other threads that want to execute code in the main thread
eventQueue = queue.Queue()
eventQueue = Queue.Queue()
def runInThread(threadFunc, callback):
"""
@ -15,6 +41,5 @@ def runInThread(threadFunc, callback):
result = threadFunc()
# Queue callback to be call in main thread
eventQueue.put(lambda: callback(result))
thread = threading.Thread(target=helper)
thread.start()

View file

@ -1,19 +1,48 @@
#!/usr/bin/python
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
import argparse
import traceback
import logging
import asyncore
import sys
import queue
import sys, os
import e4u
import Queue
import transWhat.threadutils
import Spectrum2
sys.path.insert(0, os.getcwd())
from Spectrum2.iochannel import IOChannel
from Spectrum2.config import SpectrumConfig
from transWhat.whatsappbackend import WhatsAppBackend
from yowsup.common import YowConstants
from yowsup.stacks import YowStack
from .whatsappbackend import WhatsAppBackend
from . import threadutils
# Arguments
parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true')
@ -21,8 +50,8 @@ parser.add_argument('--log', type=str)
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('-j', type=str, metavar="JID", required=True)
parser.add_argument('config', type=str)
parser.add_argument('-j', type=str, metavar="JID", required=True)
args, unknown = parser.parse_known_args()
@ -39,7 +68,7 @@ logging.basicConfig(
)
if args.config is not None:
specConf = Spectrum2.Config(args.config)
specConf = SpectrumConfig(args.config)
else:
specConf = None
@ -53,13 +82,15 @@ def handleTransportData(data):
logger = logging.getLogger('transwhat')
logger.error(traceback.format_exc())
e4u.load()
closed = False
def connectionClosed():
global closed
closed = True
# Main
io = Spectrum2.IOChannel(args.host, args.port, handleTransportData, connectionClosed)
io = IOChannel(args.host, args.port, handleTransportData, connectionClosed)
plugin = WhatsAppBackend(io, args.j, specConf)
@ -77,18 +108,16 @@ def main():
try:
callback = YowStack._YowStack__detachedQueue.get(False) #doesn't block
callback()
except queue.Empty:
except Queue.Empty:
pass
else:
break
if closed:
break
while True:
try:
callback = threadutils.eventQueue.get_nowait()
except queue.Empty:
callback = transWhat.threadutils.eventQueue.get_nowait()
except Queue.Empty:
break
else:
callback()

56
transWhat/utils.py Normal file
View file

@ -0,0 +1,56 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
import e4u
import base64
import hashlib
def ago(secs):
periods = ["second", "minute", "hour", "day", "week", "month", "year", "decade"]
lengths = [60, 60, 24, 7,4.35, 12, 10]
j = 0
diff = secs
while diff >= lengths[j]:
diff /= lengths[j]
diff = round(diff)
j += 1
period = periods[j]
if diff > 1: period += "s"
return "%d %s ago" % (diff, period)
def softToUni(message):
return e4u.translate(message.encode("utf-8"), reverse=False, **e4u.SOFTBANK_TRANSLATE_PROFILE)
def decodePassword(password):
return base64.b64decode(bytes(password))
def sha1hash(data):
return hashlib.sha1(data).hexdigest()

View file

@ -1,12 +1,41 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
from Spectrum2.backend import SpectrumBackend
from Spectrum2 import protocol_pb2
from session import Session
from registersession import RegisterSession
import logging
import Spectrum2
from .session import Session
from .registersession import RegisterSession
class WhatsAppBackend(Spectrum2.Backend):
class WhatsAppBackend(SpectrumBackend):
def __init__(self, io, spectrum_jid, specConf):
Spectrum2.Backend.__init__(self)
SpectrumBackend.__init__(self)
self.logger = logging.getLogger(self.__class__.__name__)
self.io = io
self.specConf = specConf

View file

@ -1,19 +1,47 @@
# use unicode encoding for all literals by default (for python2.x)
from __future__ import unicode_literals
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2015-2017, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
"""
This file is part of transWhat
transWhat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
transwhat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with transWhat. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
import os
from yowsup import env
from yowsup.env import YowsupEnv
from yowsup.env import S40YowsupEnv
from yowsup.stacks import YowStack, YowStackBuilder
from yowsup.common import YowConstants
from yowsup.layers import YowLayerEvent, YowParallelLayer
from yowsup.layers.auth import AuthError
# Layers
from yowsup.layers.axolotl import AxolotlSendLayer, AxolotlControlLayer, AxolotlReceivelayer
from yowsup.layers.auth import YowAuthenticationProtocolLayer
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
@ -74,6 +102,8 @@ class YowsupApp(object):
AxolotlControlLayer,
YowParallelLayer((AxolotlSendLayer, AxolotlReceivelayer)),
YowCoderLayer,
YowCryptLayer,
YowStanzaRegulator,
YowNetworkLayer
)
@ -81,7 +111,7 @@ class YowsupApp(object):
stackBuilder = YowStackBuilder()
self.stack = stackBuilder \
.pushDefaultLayers() \
.pushDefaultLayers(True) \
.push(YowsupAppLayer) \
.build()
self.stack.broadcastEvent(
@ -178,7 +208,7 @@ class YowsupApp(object):
mediaUploader = MediaUploader(jid, ownNumber, filePath,
resultRequestUploadIqProtocolEntity.getUrl(),
resultRequestUploadIqProtocolEntity.getResumeOffset(),
successFn, self.onUploadError, self.onUploadProgress, asynchronous=False)
successFn, self.onUploadError, self.onUploadProgress, async=False)
mediaUploader.start()
def onRequestUploadError(self, jid, path, errorRequestUploadIqProtocolEntity, requestUploadIqProtocolEntity):
@ -715,12 +745,12 @@ class YowsupAppLayer(YowInterfaceLayer):
@ProtocolEntityCallback('success')
def onAuthSuccess(self, entity):
# entity is SuccessProtocolEntity
status = entity.location
kind = "" #entity.kind
status = entity.status
kind = entity.kind
creation = entity.creation
expiration = "" #entity.expiration
expiration = entity.expiration
props = entity.props
nonce = "" #entity.nonce
nonce = entity.nonce
t = entity.t # I don't know what this is
self.caller.onAuthSuccess(status, kind, creation, expiration, props, nonce, t)
@ -736,7 +766,7 @@ class YowsupAppLayer(YowInterfaceLayer):
# entity is IncomingReceiptProtocolEntity
#ack = OutgoingAckProtocolEntity(entity.getId(),
# 'receipt', entity.getType(), entity.getFrom())
#self.toLower(entity.ack())
self.toLower(entity.ack())
_id = entity._id
_from = entity._from
timestamp = entity.timestamp