Compare commits

..

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

12 changed files with 320 additions and 447 deletions

View file

@ -2,7 +2,7 @@ Installation and configuration guide
====================================
I assume that you have a basic understanding of XMPP and the the concept
of a XMPP component/transport. If not, please get a book about Jabber
of a XMPP component / transport. If not, please get a book about Jabber
or read the standards.
transWhat is a XMPP transport. By this means it extends the
@ -44,14 +44,14 @@ Spectrum 2
----------
Installation
~~~~~~~~~~~~
^^^^^^^^^^^^
Manual compile latest version from `Github`_. You can use the following
guide:
http://spectrum.im/documentation/installation/from\_source\_code.html.
Configuration
~~~~~~~~~~~~~
^^^^^^^^^^^^^
Create a new file ``/etc/spectrum2/transports/whatsapp.cfg`` with the
following content:
@ -69,7 +69,7 @@ following content:
port = 5221
backend_host = localhost
backend = /usr/bin/transwhat
backend = /location/to/transwhat/transwhat.py
users_per_backend = 10
more_resources = 1
@ -106,12 +106,11 @@ Install required dependencies:
$ 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
- **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 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
.. _**yowsup**: https://github.com/tgalal/yowsup

View file

@ -3,10 +3,24 @@ transWhat
transWhat is a WhatsApp XMPP Gateway based on `Spectrum 2`_ and `Yowsup 2`_.
Support
-------
Branches
^^^^^^^^
For support and discussions please join the XMPP MUC: **transwhat@conference.0l.de**.
- `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.
`Installation`_
---------------
`Usage`_
--------
`License`_
----------
Features
--------
@ -17,24 +31,6 @@ Features
- Set status message
- Groupchats
Installation
------------
transWhat requires a running and configured XMPP server.
Detailed instructions can be found on the `Installation`_ page.
Users find details on the `Usage`_ page.
Branches
--------
- `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
------------
@ -48,16 +44,12 @@ The following persons have contributed major parts of this code:
- @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
-----
- An *outdated* project wiki is available `here`_.
- An *outdated* writeup of this project is also availabe at my `blog`_.
An *outdated* project wiki is available `here`_.
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
@ -65,6 +57,6 @@ Links
.. _yowsup-2: http://github.com/stv0g/transwhat/tree/yowsup-2
.. _Installation: INSTALL.rst
.. _Usage: USAGE.rst
.. _GPLv3: COPYING.rst
.. _License: COPYING.rst
.. _here: https://dev.0l.de/wiki/projects/transwhat/
.. _blog: http://www.steffenvogel.de/2013/06/29/transwhat/

View file

@ -89,7 +89,6 @@ message Room {
message RoomList {
repeated string room = 1;
repeated string name = 2;
optional string user = 3;
}
enum ParticipantFlag {
@ -112,7 +111,6 @@ message Participant {
optional string statusMessage = 6;
optional string newname = 7;
optional string iconHash = 8;
optional string alias = 9;
}
message VCard {
@ -154,10 +152,6 @@ message BackendConfig {
required string config = 1;
}
message APIVersion {
required int32 version = 1;
}
message WrapperMessage {
enum Type {
TYPE_CONNECTED = 1;
@ -194,10 +188,8 @@ message WrapperMessage {
TYPE_CONV_MESSAGE_ACK = 33;
TYPE_RAW_XML = 34;
TYPE_BUDDIES = 35;
TYPE_API_VERSION = 36;
}
required Type type = 1;
optional bytes payload = 2;
}
;

File diff suppressed because one or more lines are too long

5
setup.py Executable file → Normal file
View file

@ -1,5 +1,3 @@
#!/usr/bin/env python
import os
import codecs
from setuptools import setup
@ -35,9 +33,6 @@ setup(name='transwhat',
'transWhat',
'Spectrum2'
],
scripts=[
'transWhat/transwhat.py'
],
install_requires=[
'protobuf',
'yowsup2',

View file

@ -39,6 +39,7 @@ class Bot():
self.commands = {
"help": self._help,
"prune": self._prune,
"groups": self._groups,
"getgroups": self._getgroups
}
@ -75,6 +76,7 @@ class Bot():
def _help(self):
self.send("""following bot commands are available:
\\help show this message
\\prune clear your buddylist
following user commands are available:
\\lastseen request last online timestamp from buddy
@ -84,6 +86,11 @@ following group commands are available
\\groups print all attended groups
\\getgroups get current groups from WA""")
def _prune(self):
self.session.buddies.prune()
self.session.updateRoster()
self.send("buddy list cleared")
def _groups(self):
for group in self.session.groups:
buddy = self.session.groups[group].owner

View file

@ -109,6 +109,7 @@ class BuddyList(dict):
try: del self[number]
except KeyError: self.logger.warn("non-existing buddy really didn't exist: %s" % number)
def onStatus(self, contacts):
self.logger.debug("%s received statuses of: %s" % (self.user, contacts))
for number, (status, time) in contacts.iteritems():
@ -120,6 +121,7 @@ class BuddyList(dict):
buddy.statusMsg = utils.softToUni(status)
self.updateSpectrum(buddy)
def load(self, buddies):
if self.session.loggedIn:
self._load(buddies)
@ -163,6 +165,7 @@ class BuddyList(dict):
self.backend.handleBuddyChanged(self.user, buddy.number, buddy.nick,
buddy.groups, status, statusMessage=statusmsg, iconHash=iconHash)
def remove(self, number):
try:
buddy = self[number]
@ -171,7 +174,7 @@ class BuddyList(dict):
protocol_pb2.STATUS_NONE)
self.backend.handleBuddyRemoved(self.user, number)
self.session.unsubscribePresence(number)
# TODO Sync remove
# TODO Sync remove
return buddy
except KeyError:
return None

View file

@ -33,9 +33,6 @@ import time
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
@ -135,34 +132,39 @@ class Session(YowsupApp):
"\n".join(text) + "\nIf you do not join them you will lose messages"
#self.bot.send(message)
def _updateGroups(self, response, _):
def _updateGroups(self, response, request):
self.logger.debug('Received groups list %s' % response)
groups = response.getGroups()
for group in groups:
room = group.getId()
# ensure self.groups[room] exists
if room not in self.groups:
owner = group.getOwner().split('@')[0]
subjectOwner = group.getSubjectOwner().split('@')[0]
subject = utils.softToUni(group.getSubject())
self.groups[room] = Group(room, owner, subject, subjectOwner,
self.backend, self.user)
# add/update room participants
owner = group.getOwner().split('@')[0]
subjectOwner = group.getSubjectOwner().split('@')[0]
subject = utils.softToUni(group.getSubject())
if room in self.groups:
oroom = self.groups[room]
oroom.owner = owner
oroom.subjectOwner = subjectOwner
oroom.subject = subject
else:
self.groups[room] = Group(room, owner, subject, subjectOwner, self.backend, self.user)
# self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0])
self.groups[room].addParticipants(group.getParticipants().keys(),
self.buddies, self.legacyName)
self.buddies, self.legacyName)
#self._addParticipantsToRoom(room, group.getParticipants())
if room in self.groupOfflineQueue:
while self.groupOfflineQueue[room]:
msg = self.groupOfflineQueue[room].pop(0)
self.backend.handleMessage(self.user, room, msg[1],
msg[0], "", msg[2])
self.logger.debug("Send queued group message to: %s %s %s" %
(msg[0],msg[1], msg[2]))
self.gotGroupList = True
# join rooms
while self.joinRoomQueue:
self.joinRoom(*self.joinRoomQueue.pop(0))
# deliver queued offline messages
for room in self.groupOfflineQueue:
while self.groupOfflineQueue[room]:
msg = self.groupOfflineQueue[room].pop(0)
self.backend.handleMessage(self.user, room, msg[1], msg[0], "",
msg[2])
self.logger.debug("Send queued group message to: %s %s %s" %
(msg[0], msg[1], msg[2]))
# pass update to backend
for room, nick in self.joinRoomQueue:
self.joinRoom(room, nick)
self.joinRoomQueue = []
self.updateRoomList()
def joinRoom(self, room, nick):
@ -300,61 +302,81 @@ class Session(YowsupApp):
# Called by superclass
def onImage(self, image):
self.logger.debug('Received image message: %s' % image)
buddy = image._from.split('@')[0]
participant = image.participant
if image.caption is None:
image.caption = ''
self.onMedia(image, "image")
if image.isEncrypted():
self.logger.debug('Received encrypted image message')
if self.backend.specConf is not None and self.backend.specConf.__getitem__("service.web_directory") is not None and self.backend.specConf.__getitem__("service.web_url") is not None :
ipath = "/" + str(image.timestamp) + image.getExtension()
with open(self.backend.specConf.__getitem__("service.web_directory") + ipath,"wb") as f:
f.write(image.getMediaContent())
url = self.backend.specConf.__getitem__("service.web_url") + ipath
else:
self.logger.warn('Received encrypted image: web storage not set in config!')
url = image.url
else:
url = image.url
if participant is not None: # Group message
partname = participant.split('@')[0]
if image._from.split('@')[1] == 'broadcast': # Broadcast message
self.sendMessageToXMPP(partname, self.broadcast_prefix, image.timestamp)
self.sendMessageToXMPP(partname, url, image.timestamp)
self.sendMessageToXMPP(partname, image.caption, image.timestamp)
else: # Group message
self.sendGroupMessageToXMPP(buddy, partname, url, image.timestamp)
self.sendGroupMessageToXMPP(buddy, partname, image.caption, image.timestamp)
else:
self.sendMessageToXMPP(buddy, url, image.timestamp)
self.sendMessageToXMPP(buddy, image.caption, image.timestamp)
self.sendReceipt(image._id, image._from, None, image.participant)
self.recvMsgIDs.append((image._id, image._from, image.participant, image.timestamp))
# Called by superclass
def onAudio(self, audio):
self.onMedia(audio, "audio")
self.logger.debug('Received audio message: %s' % audio)
buddy = audio._from.split('@')[0]
participant = audio.participant
message = audio.url
if participant is not None: # Group message
partname = participant.split('@')[0]
if audio._from.split('@')[1] == 'broadcast': # Broadcast message
self.sendMessageToXMPP(partname, self.broadcast_prefix, audio.timestamp)
self.sendMessageToXMPP(partname, message, audio.timestamp)
else: # Group message
self.sendGroupMessageToXMPP(buddy, partname, message, audio.timestamp)
else:
self.sendMessageToXMPP(buddy, message, audio.timestamp)
self.sendReceipt(audio._id, audio._from, None, audio.participant)
self.recvMsgIDs.append((audio._id, audio._from, audio.participant, audio.timestamp))
# Called by superclass
def onVideo(self, video):
self.onMedia(video, "video")
def onMedia(self, media, type):
self.logger.debug('Received %s message: %s' % (type, media))
buddy = media._from.split('@')[0]
participant = media.participant
caption = ''
if media.isEncrypted():
self.logger.debug('Received encrypted media message')
if self.backend.specConf is not None and self.backend.specConf.__getitem__("service.web_directory") is not None and self.backend.specConf.__getitem__("service.web_url") is not None :
ipath = "/" + str(media.timestamp) + media.getExtension()
with open(self.backend.specConf.__getitem__("service.web_directory") + ipath,"wb") as f:
f.write(media.getMediaContent())
url = self.backend.specConf.__getitem__("service.web_url") + ipath
else:
self.logger.warn('Received encrypted media: web storage not set in config!')
url = media.url
else:
url = media.url
if type == 'image':
caption = media.caption
self.logger.debug('Received video message: %s' % video)
buddy = video._from.split('@')[0]
participant = video.participant
message = video.url
if participant is not None: # Group message
partname = participant.split('@')[0]
if media._from.split('@')[1] == 'broadcast': # Broadcast message
self.sendMessageToXMPP(partname, self.broadcast_prefix, media.timestamp)
self.sendMessageToXMPP(partname, url, media.timestamp)
self.sendMessageToXMPP(partname, caption, media.timestamp)
if video._from.split('@')[1] == 'broadcast': # Broadcast message
self.sendMessageToXMPP(partname, self.broadcast_prefix, video.timestamp)
self.sendMessageToXMPP(partname, message, video.timestamp)
else: # Group message
self.sendGroupMessageToXMPP(buddy, partname, url, media.timestamp)
self.sendGroupMessageToXMPP(buddy, partname, caption, media.timestamp)
self.sendGroupMessageToXMPP(buddy, partname, message, video.timestamp)
else:
self.sendMessageToXMPP(buddy, url, media.timestamp)
self.sendMessageToXMPP(buddy, caption, media.timestamp)
self.sendMessageToXMPP(buddy, message, video.timestamp)
self.sendReceipt(video._id, video._from, None, video.participant)
self.recvMsgIDs.append((video._id, video._from, video.participant, video.timestamp))
self.sendReceipt(media._id, media._from, None, media.participant)
self.recvMsgIDs.append((media._id, media._from, media.participant, media.timestamp))
def onLocation(self, location):
buddy = location._from.split('@')[0]
@ -668,7 +690,7 @@ class Session(YowsupApp):
waId = self.sendTextMessage(sender + '@s.whatsapp.net', message)
self.msgIDs[waId] = MsgIDs(ID, waId)
# self.logger.info("WA Message send to %s with ID %s", buddy, waId)
self.logger.info("WA Message send to %s with ID %s", buddy, waId)
def executeCommand(self, command, room):
if command == '\\leave':

View file

@ -33,13 +33,13 @@ import asyncore
import sys, os
import e4u
import Queue
import transWhat.threadutils
import threadutils
sys.path.insert(0, os.getcwd())
from Spectrum2.iochannel import IOChannel
from Spectrum2.config import SpectrumConfig
from transWhat.whatsappbackend import WhatsAppBackend
from config import SpectrumConfig
from whatsappbackend import WhatsAppBackend
from yowsup.common import YowConstants
from yowsup.stacks import YowStack
@ -58,36 +58,36 @@ args, unknown = parser.parse_known_args()
YowConstants.PATH_STORAGE='/var/lib/spectrum2/' + args.j
if args.log is None:
args.log = '/var/log/spectrum2/' + args.j + '/backends/backend.log'
args.log = '/var/log/spectrum2/' + args.j + '/backends/backend.log'
# Logging
logging.basicConfig(
filename = args.log,
format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s",
level = logging.DEBUG if args.debug else logging.INFO
filename = args.log,
format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s",
level = logging.DEBUG if args.debug else logging.INFO
)
if args.config is not None:
specConf = SpectrumConfig(args.config)
specConf = SpectrumConfig(args.config)
else:
specConf = None
specConf = None
# Handler
def handleTransportData(data):
try:
plugin.handleDataRead(data)
except SystemExit as e:
raise e
except:
logger = logging.getLogger('transwhat')
logger.error(traceback.format_exc())
try:
plugin.handleDataRead(data)
except SystemExit as e:
raise e
except:
logger = logging.getLogger('transwhat')
logger.error(traceback.format_exc())
e4u.load()
closed = False
def connectionClosed():
global closed
closed = True
global closed
closed = True
# Main
io = IOChannel(args.host, args.port, handleTransportData, connectionClosed)
@ -95,34 +95,34 @@ io = IOChannel(args.host, args.port, handleTransportData, connectionClosed)
plugin = WhatsAppBackend(io, args.j, specConf)
plugin.handleBackendConfig({
'features': [
('send_buddies_on_login', 1),
('muc', 'true'),
],
'features': [
('send_buddies_on_login', 1),
('muc', 'true'),
],
})
def main():
while True:
try:
asyncore.loop(timeout=1.0, count=10, use_poll = True)
try:
callback = YowStack._YowStack__detachedQueue.get(False) #doesn't block
callback()
except Queue.Empty:
pass
else:
break
if closed:
break
while True:
try:
callback = transWhat.threadutils.eventQueue.get_nowait()
except Queue.Empty:
break
else:
callback()
except SystemExit:
break
except:
logger = logging.getLogger('transwhat')
logger.error(traceback.format_exc())
while True:
try:
asyncore.loop(timeout=1.0, count=10, use_poll = True)
try:
callback = YowStack._YowStack__detachedQueue.get(False) #doesn't block
callback()
except Queue.Empty:
pass
else:
break
if closed:
break
while True:
try:
callback = threadutils.eventQueue.get_nowait()
except Queue.Empty:
break
else:
callback()
except SystemExit:
break
except:
logger = logging.getLogger('transwhat')
logger.error(traceback.format_exc())

View file

@ -165,7 +165,7 @@ class WhatsAppBackend(SpectrumBackend):
def handleRawXmlRequest(self, xml):
pass
def handleMessageAckRequest(self, user, legacyName, ID = 0):
def handleMessageAckRequest(self, user, legacyName, ID = 0):
self.logger.info("Meassage ACK request for %s !!" % legacyName)
def sendData(self, data):

View file

@ -25,14 +25,16 @@ __email__ = "post@steffenvogel.de"
"""
import logging
import os
from yowsup import env
from yowsup.env import S40YowsupEnv
from yowsup.stacks import YowStack, YowStackBuilder
from yowsup.stacks import YowStack
from yowsup.common import YowConstants
from yowsup.layers import YowLayerEvent, YowParallelLayer
from yowsup.layers.auth import AuthError
from yowsup.stacks import YowStack
from yowsup.stacks import YowStackBuilder
from yowsup.common import YowConstants
# Layers
from yowsup.layers.axolotl import AxolotlSendLayer, AxolotlControlLayer, AxolotlReceivelayer
@ -54,10 +56,10 @@ from yowsup.layers.protocol_contacts import YowContactsIqProtocolLayer
from yowsup.layers.protocol_chatstate import YowChatstateProtocolLayer
from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer
from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer
from yowsup.layers.protocol_calls import YowCallsProtocolLayer
from yowsup.layers.axolotl.props import PROP_IDENTITY_AUTOTRUST
from yowsup.layers.protocol_calls import YowCallsProtocolLayer
# ProtocolEntities
from yowsup.layers.protocol_acks.protocolentities import *
from yowsup.layers.protocol_chatstate.protocolentities import *
from yowsup.layers.protocol_contacts.protocolentities import *
@ -73,7 +75,9 @@ from yowsup.layers.protocol_iq.protocolentities import *
from yowsup.layers.protocol_media.mediauploader import MediaUploader
from yowsup.layers.protocol_media.mediadownloader import MediaDownloader
# Registration
from yowsup.registration import WACodeRequest
from yowsup.registration import WARegRequest
@ -128,10 +132,9 @@ class YowsupApp(object):
and cellphone number)
- password: (str) base64 encoded password
"""
"""
self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS,
(username, password))
self.stack.setProp(PROP_IDENTITY_AUTOTRUST, True)
# self.stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 5)
try: