Merge pull request #11 from moyamo/yowsup2-port

Port to Yowsup 2! Thanks  a lot!
This commit is contained in:
Steffen Vogel 2015-09-07 15:56:26 +02:00
commit 2496809bf1
12 changed files with 970 additions and 295 deletions

View File

@ -22,6 +22,15 @@ Use my patched version at https://github.com/stv0g/yowsup
#### Google Atom and GData Python wrappers #### Google Atom and GData Python wrappers
required for Google contacts import required for Google contacts import
#### MySQLdb
required
#### Google protobuf
required
#### date.util
required
## Contribute ## Contribute
Pull requests, bug reports etc. are welcome. Pull requests, bug reports etc. are welcome.

View File

@ -204,7 +204,7 @@ class SpectrumBackend:
def handleFTData(self, ftID, data): def handleFTData(self, ftID, data):
d = protocol_pb2.FileTransferData() d = protocol_pb2.FileTransferData()
d.ftid = ftID d.ftID = ftID
d.data = data d.data = data
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA); message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA);

View File

@ -1,13 +1,17 @@
import asyncore, socket import asyncore, socket
import logging
import sys
class IOChannel(asyncore.dispatcher): class IOChannel(asyncore.dispatcher):
def __init__(self, host, port, callback): def __init__(self, host, port, callback, closeCallback):
asyncore.dispatcher.__init__(self) asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect((host, port)) self.connect((host, port))
self.logger = logging.getLogger(self.__class__.__name__)
self.callback = callback self.callback = callback
self.closeCallback = closeCallback
self.buffer = "" self.buffer = ""
def sendData(self, data): def sendData(self, data):
@ -27,6 +31,11 @@ class IOChannel(asyncore.dispatcher):
sent = self.send(self.buffer) sent = self.send(self.buffer)
self.buffer = self.buffer[sent:] self.buffer = self.buffer[sent:]
def handle_close(self):
self.logger.info('Connection to backend closed, terminating.')
self.close()
self.closeCallback()
def writable(self): def writable(self):
return (len(self.buffer) > 0) return (len(self.buffer) > 0)

110
bot.py
View File

@ -31,7 +31,7 @@ import os
import utils import utils
from constants import * from constants import *
from googleclient import GoogleClient #from googleclient import GoogleClient
from Yowsup.Contacts.contacts import WAContactsSyncRequest from Yowsup.Contacts.contacts import WAContactsSyncRequest
@ -40,10 +40,10 @@ class Bot():
self.session = session self.session = session
self.name = name self.name = name
self.google = GoogleClient() # self.google = GoogleClient()
self.commands = { self.commands = {
"import": self._import, # "import": self._import,
"help": self._help, "help": self._help,
"prune": self._prune, "prune": self._prune,
"welcome": self._welcome, "welcome": self._welcome,
@ -79,40 +79,40 @@ class Bot():
def send(self, message): def send(self, message):
self.session.backend.handleMessage(self.session.user, self.name, message) self.session.backend.handleMessage(self.session.user, self.name, message)
def __do_import(self, token): # def __do_import(self, token):
# Google # # Google
google = self.google.getContacts(token) # google = self.google.getContacts(token)
self.send("%d buddies imported from google" % len(google)) # self.send("%d buddies imported from google" % len(google))
#
result = { } # result = { }
for number, name in google.iteritems(): # for number, name in google.iteritems():
number = re.sub("[^0-9]", "", number) # number = re.sub("[^0-9]", "", number)
number = number if number[0] == "0" else "+" + number # number = number if number[0] == "0" else "+" + number
#
result[number] = { 'nick': name, 'state': 0 } # result[number] = { 'nick': name, 'state': 0 }
#
# WhatsApp # # WhatsApp
user = self.session.legacyName # user = self.session.legacyName
password = self.session.password # password = self.session.password
sync = WAContactsSyncRequest(user, password, result.keys()) # sync = WAContactsSyncRequest(user, password, result.keys())
whatsapp = sync.send()['c'] # whatsapp = sync.send()['c']
#
for w in whatsapp: # for w in whatsapp:
result[w['p']]['state'] = w['w'] # result[w['p']]['state'] = w['w']
result[w['p']]['number'] = w['n'] # result[w['p']]['number'] = w['n']
#
self.send("%d buddies are using whatsapp" % len(filter(lambda w: w['w'], whatsapp))) # self.send("%d buddies are using whatsapp" % len(filter(lambda w: w['w'], whatsapp)))
#
for r in result.values(): # for r in result.values():
if r['nick']: # if r['nick']:
self.session.buddies.add( # self.session.buddies.add(
number = r['number'], # number = r['number'],
nick = r['nick'], # nick = r['nick'],
groups = [u'Google'], # groups = [u'Google'],
state = r['state'] # state = r['state']
) # )
#
self.send("%d buddies imported" % len(whatsapp)) # self.send("%d buddies imported" % len(whatsapp))
def __get_token(self, filename, timeout = 30): def __get_token(self, filename, timeout = 30):
file = open(filename, 'r') file = open(filename, 'r')
@ -135,24 +135,24 @@ class Bot():
file.close() file.close()
# commands # commands
def _import(self, token = None): # def _import(self, token = None):
if not token: # if not token:
token_url = self.google.getTokenUrl("http://whatsapp.0l.de/auth.py") # token_url = self.google.getTokenUrl("http://whatsapp.0l.de/auth.py")
auth_url = "http://whatsapp.0l.de/auth.py?number=%s&auth_url=%s" % (self.session.legacyName, urllib.quote(token_url)) # auth_url = "http://whatsapp.0l.de/auth.py?number=%s&auth_url=%s" % (self.session.legacyName, urllib.quote(token_url))
short_url = utils.shorten(auth_url) # short_url = utils.shorten(auth_url)
self.send("please visit this url to auth: %s" % short_url) # self.send("please visit this url to auth: %s" % short_url)
#
self.send("waiting for authorization...") # self.send("waiting for authorization...")
token = self.__get_token(TOKEN_FILE) # token = self.__get_token(TOKEN_FILE)
if token: # if token:
self.send("got token: %s" % token) # self.send("got token: %s" % token)
self.__do_import(token) # self.__do_import(token)
self.session.updateRoster() # self.session.updateRoster()
else: # else:
self.send("timeout! please use \"\\import [token]\"") # self.send("timeout! please use \"\\import [token]\"")
else: # else:
self.__do_import(token) # self.__do_import(token)
self.session.updateRoster() # self.session.updateRoster()
def _sync(self): def _sync(self):
user = self.session.legacyName user = self.session.legacyName

View File

@ -48,7 +48,7 @@ class Number():
class Buddy(): class Buddy():
def __init__(self, owner, number, nick, groups, id, db): def __init__(self, owner, number, nick, groups, image_hash, id, db):
self.id = id self.id = id
self.db = db self.db = db
@ -56,14 +56,16 @@ class Buddy():
self.owner = owner self.owner = owner
self.number = number self.number = number
self.groups = groups self.groups = groups
self.image_hash = image_hash
def update(self, nick, groups): def update(self, nick, groups, image_hash):
self.nick = nick self.nick = nick
self.groups = groups self.groups = groups
self.image_hash = image_hash
groups = u",".join(groups).encode("latin-1") groups = u",".join(groups).encode("latin-1")
cur = self.db.cursor() cur = self.db.cursor()
cur.execute("UPDATE buddies SET nick = %s, groups = %s WHERE owner_id = %s AND buddy_id = %s", (self.nick, groups, self.owner.id, self.number.id)) cur.execute("UPDATE buddies SET nick = %s, groups = %s, image_hash = %s WHERE owner_id = %s AND buddy_id = %s", (self.nick, groups, image_hash, self.owner.id, self.number.id))
self.db.commit() self.db.commit()
def delete(self): def delete(self):
@ -73,13 +75,13 @@ class Buddy():
self.id = None self.id = None
@staticmethod @staticmethod
def create(owner, number, nick, groups, db): def create(owner, number, nick, groups, image_hash, db):
groups = u",".join(groups).encode("latin-1") groups = u",".join(groups).encode("latin-1")
cur = db.cursor() cur = db.cursor()
cur.execute("REPLACE buddies (owner_id, buddy_id, nick, groups) VALUES (%s, %s, %s, %s)", (owner.id, number.id, nick, groups)) cur.execute("REPLACE buddies (owner_id, buddy_id, nick, groups, image_hash) VALUES (%s, %s, %s, %s, %s)", (owner.id, number.id, nick, groups, image_hash))
db.commit() db.commit()
return Buddy(owner, number, nick, groups, cur.lastrowid, db) return Buddy(owner, number, nick, groups, image_hash, cur.lastrowid, db)
def __str__(self): def __str__(self):
return "%s (nick=%s, id=%s)" % (self.number, self.nick, self.id) return "%s (nick=%s, id=%s)" % (self.number, self.nick, self.id)
@ -99,7 +101,8 @@ class BuddyList(dict):
n.number AS number, n.number AS number,
b.nick AS nick, b.nick AS nick,
b.groups AS groups, b.groups AS groups,
n.state AS state n.state AS state,
b.image_hash AS image_hash
FROM buddies AS b FROM buddies AS b
LEFT JOIN numbers AS n LEFT JOIN numbers AS n
ON b.buddy_id = n.id ON b.buddy_id = n.id
@ -109,26 +112,28 @@ class BuddyList(dict):
ORDER BY b.owner_id DESC""", self.owner.id) ORDER BY b.owner_id DESC""", self.owner.id)
for i in range(cur.rowcount): for i in range(cur.rowcount):
id, number, nick, groups, state = cur.fetchone() id, number, nick, groups, state, image_hash = cur.fetchone()
self[number] = Buddy(self.owner, Number(number, state, self.db), nick.decode('latin1'), groups.split(","), id, self.db) self[number] = Buddy(self.owner, Number(number, state, self.db), nick.decode('latin1'), groups.split(","), image_hash, id, self.db)
def update(self, number, nick, groups): def update(self, number, nick, groups, image_hash):
if number in self: if number in self:
buddy = self[number] buddy = self[number]
buddy.update(nick, groups) buddy.update(nick, groups, image_hash)
else: else:
buddy = self.add(number, nick, groups, 1) buddy = self.add(number, nick, groups, 1, image_hash)
return buddy return buddy
def add(self, number, nick, groups = [], state = 0): def add(self, number, nick, groups = [], state = 0, image_hash = ""):
return Buddy.create(self.owner, Number(number, state, self.db), nick, groups, self.db) return Buddy.create(self.owner, Number(number, state, self.db), nick, groups, image_hash, self.db)
def remove(self, number): def remove(self, number):
buddy = self[number] try:
buddy.delete() buddy = self[number]
buddy.delete()
return buddy return buddy
except KeyError:
return None
def prune(self): def prune(self):
cur = self.db.cursor() cur = self.db.cursor()

View File

@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS `buddies` (
`buddy_id` int(11) NOT NULL, `buddy_id` int(11) NOT NULL,
`nick` varchar(255) NOT NULL, `nick` varchar(255) NOT NULL,
`groups` varchar(255) NOT NULL, `groups` varchar(255) NOT NULL,
`image_hash` varchar(40),
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1; ) ENGINE=InnoDB DEFAULT CHARSET=latin1;

View File

@ -37,12 +37,12 @@ class GoogleClient():
def __init__(self): def __init__(self):
self.client = gdata.contacts.client.ContactsClient() self.client = gdata.contacts.client.ContactsClient()
self.token = gdata.gauth.OAuth2Token( # self.token = gdata.gauth.OAuth2Token(
client_id = GOOGLE_CLIENT_ID, # client_id = GOOGLE_CLIENT_ID,
client_secret = GOOGLE_CLIENT_SECRET, # client_secret = GOOGLE_CLIENT_SECRET,
scope = 'https://www.google.com/m8/feeds/contacts', # scope = 'https://www.google.com/m8/feeds/contacts',
user_agent = 'whatTrans' # user_agent = 'whatTrans'
) # )
def getTokenUrl(self, uri = 'urn:ietf:wg:oauth:2.0:oob'): def getTokenUrl(self, uri = 'urn:ietf:wg:oauth:2.0:oob'):
return self.token.generate_authorize_url(redirect_uri=uri) return self.token.generate_authorize_url(redirect_uri=uri)

View File

@ -27,7 +27,6 @@ import logging
import urllib import urllib
import time import time
from Yowsup.connectionmanager import YowsupConnectionManager
from Spectrum2 import protocol_pb2 from Spectrum2 import protocol_pb2
from buddy import BuddyList from buddy import BuddyList
@ -35,10 +34,12 @@ from threading import Timer
from group import Group from group import Group
from bot import Bot from bot import Bot
from constants import * from constants import *
from yowsupwrapper import YowsupApp
class Session: class Session(YowsupApp):
def __init__(self, backend, user, legacyName, extra, db): def __init__(self, backend, user, legacyName, extra, db):
super(Session, self).__init__()
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
self.logger.info("Created: %s", legacyName) self.logger.info("Created: %s", legacyName)
@ -46,6 +47,8 @@ class Session:
self.backend = backend self.backend = backend
self.user = user self.user = user
self.legacyName = legacyName self.legacyName = legacyName
self.buddies = BuddyList(self.legacyName, self.db)
self.bot = Bot(self)
self.status = protocol_pb2.STATUS_NONE self.status = protocol_pb2.STATUS_NONE
self.statusMessage = '' self.statusMessage = ''
@ -58,76 +61,273 @@ class Session:
self.timer = None self.timer = None
self.password = None self.password = None
self.initialized = False self.initialized = False
self.loggedin = False
self.buddies = BuddyList(legacyName, db)
self.frontend = YowsupConnectionManager()
self.bot = Bot(self) self.bot = Bot(self)
# Events
self.listen("auth_success", self.onAuthSuccess)
self.listen("auth_fail", self.onAuthFailed)
self.listen("disconnected", self.onDisconnected)
self.listen("contact_typing", self.onContactTyping)
self.listen("contact_paused", self.onContactPaused)
self.listen("presence_updated", self.onPrecenceUpdated)
self.listen("presence_available", self.onPrecenceAvailable)
self.listen("presence_unavailable", self.onPrecenceUnavailable)
self.listen("message_received", self.onMessageReceived)
self.listen("image_received", self.onMediaReceived)
self.listen("video_received", self.onMediaReceived)
self.listen("audio_received", self.onMediaReceived)
self.listen("location_received", self.onLocationReceived)
self.listen("vcard_received", self.onVcardReceived)
self.listen("group_messageReceived", self.onGroupMessageReceived)
self.listen("group_gotInfo", self.onGroupGotInfo)
self.listen("group_gotParticipants", self.onGroupGotParticipants)
self.listen("group_subjectReceived", self.onGroupSubjectReceived)
self.listen("notification_groupParticipantAdded", self.onGroupParticipantAdded)
self.listen("notification_groupParticipantRemoved", self.onGroupParticipantRemoved)
self.listen("notification_contactProfilePictureUpdated", self.onContactProfilePictureUpdated)
self.listen("notification_groupPictureUpdated", self.onGroupPictureUpdated)
def __del__(self): # handleLogoutRequest def __del__(self): # handleLogoutRequest
self.logout() self.logout()
def call(self, method, args = ()): def call(self, method, **kwargs):
args = [str(s) for s in args] self.logger.debug("%s(%s)", method,
self.logger.debug("%s(%s)", method, ", ".join(args)) ", ".join(str(k) + ': ' + str(v) for k, v in kwargs.items()))
self.frontend.methodInterface.call(method, args) ##self.stack.broadcastEvent(YowLayerEvent(method, **kwargs))
def listen(self, event, callback):
self.frontend.signalInterface.registerListener(event, callback)
def logout(self): def logout(self):
self.call("disconnect", ("logout",)) self.loggedin = False
super(Session, self).logout()
def login(self, password): def login(self, password):
self.password = utils.decodePassword(password) self.loggedin = True
self.call("auth_login", (self.legacyName, self.password)) self.password = password
super(Session, self).login(self.legacyName, self.password)
def updateRoomList(self): def updateRoomList(self):
rooms = [] rooms = []
for room, group in self.groups.iteritems(): for room, group in self.groups.iteritems():
rooms.append([room, group.subject]) rooms.append([room, group.subject])
self.logger.debug("Got rooms: %s", rooms)
self.backend.handleRoomList(rooms) self.backend.handleRoomList(rooms)
def updateRoster(self):
self.logger.debug("Update roster")
old = self.buddies.keys()
self.buddies.load()
new = self.buddies.keys()
add = set(new) - set(old)
remove = set(old) - set(new)
self.logger.debug("Roster remove: %s", str(list(remove)))
self.logger.debug("Roster add: %s", str(list(add)))
for number in remove:
self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE)
self.backend.handleBuddyRemoved(self.user, number)
self.unsubscribePresence(number)
for number in add:
buddy = self.buddies[number]
self.subscribePresence(number)
self.backend.handleBuddyChanged(self.user, number, buddy.nick, buddy.groups, protocol_pb2.STATUS_NONE, iconHash = buddy.image_hash if buddy.image_hash is not None else "")
#self.requestLastSeen(number, self._lastSeen)
self.logger.debug('Requesting groups list')
self.requestGroupsList(self._updateGroups)
def _updateGroups(self, response, request):
self.logger.debug('Received groups list %s', response)
# This XMPP client is not receiving this for some reason.
groups = response.getGroups()
for group in groups:
room = group.getId()
owner = group.getOwner()
subjectOwner = group.getSubjectOwner()
subject = group.getSubject()
if room in self.groups:
oroom = self.groups[room]
oroom.owner = owner
oroom.subjectOwner = subjectOwner
oroom.subject = subject
else:
self.groups[room] = Group(room, owner, subject, subjectOwner)
# A crude implemtation of groups that act like buddies
self.backend.handleBuddyChanged(self.user, room, subject, [], protocol_pb2.STATUS_NONE)
# This XMPP client is not receiving this for some reason.
# self.updateRoomList()
# for group in groups:
# room = group.getId()
# subjectOwner = group.getSubjectOwner()
# subject = group.getSubject()
# self.backend.handleSubject(self.user, room, subject, subjectOwner)
# for participant in group.getParticipants():
# buddy = participant.split('@')[0]
# self.logger.debug("Added %s to room %s", buddy, room)
# self.backend.handleParticipantChanged(self.user, buddy, room,
# protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_ONLINE)
def _lastSeen(self, number, seconds):
self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds)))
if seconds < 60:
self.onPresenceAvailable(number)
else:
self.onPresenceUnavailable(number)
# Called by superclass
def onAuthSuccess(self, status, kind, creation,
expiration, props, nonce, t):
self.logger.info("Auth success: %s", self.user)
self.backend.handleConnected(self.user)
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE)
self.initialized = True
self.sendPresence(True)
self.updateRoster()
# Called by superclass
def onAuthFailed(self, reason):
self.logger.info("Auth failed: %s (%s)", self.user, reason)
self.backend.handleDisconnected(self.user, 0, reason)
self.password = None
# Called by superclass
def onDisconnect(self):
self.logger.debug('Disconnected')
self.backend.handleDisconnected(self.user, 0, 'Disconnected for unknown reasons')
self.loggedin = False
# Called by superclass
def onReceipt(self, _id, _from, timestamp, type, participant, offline, items):
self.logger.debug("received receipt, sending ack: " +
' '.join(map(str, [_id, _from, timestamp,
type, participant, offline, items]))
)
buddy = self.buddies[_from.split('@')[0]]
self.backend.handleBuddyChanged(self.user, buddy.number.number,
buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE)
# Called by superclass
def onAck(self, _id, _class, _from, timestamp):
self.logger.debug('received ack ' +
' '.join(map(str, [_id, _class, _from,timestamp,]))
)
# Called by superclass
def onTextMessage(self, _id, _from, to, notify, timestamp, participant, offline, retry, body):
self.logger.debug('received TextMessage' +
' '.join(map(str, [
_id, _from, to, notify, timestamp,
participant, offline, retry, body
]))
)
buddy = _from.split('@')[0]
messageContent = utils.softToUni(body)
self.sendReceipt(_id, _from, None, participant)
self.logger.info("Message received from %s to %s: %s (at ts=%s)",
buddy, self.legacyName, messageContent, timestamp)
if participant is not None:
partname = participant.split('@')[0]
message = partname + ': ' + messageContent
self.sendMessageToXMPP(buddy, message, timestamp)
else:
self.sendMessageToXMPP(buddy, messageContent, timestamp)
# isBroadcast always returns false, I'm not sure how to get a broadcast
# message.
#if messageEntity.isBroadcast():
# self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\
# buddy, self.legacyName, messageContent, timestamp)
# messageContent = "[Broadcast] " + messageContent
# Called by superclass
def onImage(self, image):
self.logger.debug('Received image message %s', str(image))
buddy = image._from.split('@')[0]
message = image.url + ' ' + image.caption
self.sendMessageToXMPP(buddy, message, image.timestamp)
self.sendReceipt(image._id, image._from, None, image.participant)
# Called by superclass
def onAudio(self, audio):
self.logger.debug('Received audio message %s', str(audio))
buddy = audio._from.split('@')[0]
message = audio.url
self.sendMessageToXMPP(buddy, message, audio.timestamp)
self.sendReceipt(audio._id, audio._from, None, audio.participant)
# Called by superclass
def onVideo(self, video):
self.logger.debug('Received video message %s', str(video))
buddy = video._from.split('@')[0]
message = video.url
self.sendMessageToXMPP(buddy, message, video.timestamp)
self.sendReceipt(video._id, video._from, None, video.participant)
# Called by superclass
def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant):
self.logger.debug('received VCard' +
' '.join(map(str, [
_id, _from, name, card_data, to, notify, timestamp, participant
]))
)
buddy = _from.split("@")[0]
self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)")
self.sendMessageToXMPP(buddy, card_data)
self.transferFile(buddy, str(name), card_data)
self.sendReceipt(_id, _from, None, participant)
def transferFile(self, buddy, name, data):
# Not working
self.logger.debug('transfering file %s', name)
self.backend.handleFTStart(self.user, buddy, name, len(data))
self.backend.handleFTData(0, data)
self.backend.handleFTFinish(self.user, buddy, name, len(data), 0)
# Called by superclass
def onContactTyping(self, buddy):
self.logger.info("Started typing: %s", buddy)
self.sendPresence(True)
self.backend.handleBuddyTyping(self.user, buddy)
if self.timer != None:
self.timer.cancel()
# Called by superclass
def onContactPaused(self, buddy):
self.logger.info("Paused typing: %s", buddy)
self.backend.handleBuddyTyped(self.user, buddy)
self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, (self.user, buddy)).start()
def onPresenceReceived(self, _type, name, jid, lastseen):
self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen)
buddy = jid.split("@")[0]
# seems to be causing an error
# self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen))
if buddy in self.presenceRequested:
timestamp = time.localtime(time.time() - lastseen)
timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp)
self.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen)))
self.presenceRequested.remove(buddy)
if lastseen < 60:
self.onPresenceAvailable(buddy)
else:
self.onPresenceUnavailable(buddy)
def onPresenceAvailable(self, buddy):
try:
buddy = self.buddies[buddy]
self.logger.info("Is available: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE)
except KeyError:
self.logger.error("Buddy not found: %s", buddy)
def onPresenceUnavailable(self, buddy):
try:
buddy = self.buddies[buddy]
self.logger.info("Is unavailable: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA)
except KeyError:
self.logger.error("Buddy not found: %s", buddy)
# spectrum RequestMethods # spectrum RequestMethods
def sendTypingStarted(self, buddy): def sendTypingStarted(self, buddy):
if buddy != "bot": if buddy != "bot":
self.logger.info("Started typing: %s to %s", self.legacyName, buddy) self.logger.info("Started typing: %s to %s", self.legacyName, buddy)
self.call("typing_send", (buddy + "@s.whatsapp.net",)) self.sendTyping(buddy, True)
# If he is typing he is present
# I really don't know where else to put this.
# Ideally, this should be sent if the user is looking at his client
self.sendPresence(True)
def sendTypingStopped(self, buddy): def sendTypingStopped(self, buddy):
if buddy != "bot": if buddy != "bot":
self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy) self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy)
self.call("typing_paused", (buddy + "@s.whatsapp.net",)) self.sendTyping(buddy, False)
def sendMessageToWA(self, sender, message): def sendMessageToWA(self, sender, message):
self.logger.info("Message sent from %s to %s: %s", self.legacyName, sender, message) self.logger.info("Message sent from %s to %s: %s", self.legacyName, sender, message)
@ -138,31 +338,34 @@ class Session:
elif "-" in sender: # group msg elif "-" in sender: # group msg
if "/" in sender: if "/" in sender:
room, buddy = sender.split("/") room, buddy = sender.split("/")
self.call("message_send", (buddy + "@s.whatsapp.net", message)) self.sendTextMessage(buddy + '@s.whatsapp.net', message)
else: else:
room = sender room = sender
group = self.groups[room] # group = self.groups[room]
# self.backend.handleMessage(self.user, room, message, group.nick)
self.sendTextMessage(room + '@g.us', message)
self.backend.handleMessage(self.user, room, message, group.nick)
self.call("message_send", (room + "@g.us", message))
else: # private msg else: # private msg
buddy = sender buddy = sender
if message == "\\lastseen": # if message == "\\lastseen":
self.presenceRequested.append(buddy) # self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",))
self.call("presence_request", (buddy + "@s.whatsapp.net",)) # else:
else: self.sendTextMessage(sender + '@s.whatsapp.net', message)
self.call("message_send", (buddy + "@s.whatsapp.net", message))
def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): def sendMessageToXMPP(self, buddy, messageContent, timestamp = "", nickname = ""):
if timestamp: if timestamp:
timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp))
if self.initialized == False: if self.initialized == False:
self.logger.debug("Message queued from %s to %s: %s", buddy, self.legacyName, messageContent) self.logger.debug("Message queued from %s to %s: %s",
buddy, self.legacyName, messageContent)
self.offlineQueue.append((buddy, messageContent, timestamp)) self.offlineQueue.append((buddy, messageContent, timestamp))
else: else:
self.logger.debug("Message sent from %s to %s: %s", buddy, self.legacyName, messageContent) self.logger.debug("Message sent from %s to %s: %s", buddy,
self.backend.handleMessage(self.user, buddy, messageContent, "", "", timestamp) self.legacyName, messageContent)
self.backend.handleMessage(self.user, buddy, messageContent, "",
"", timestamp)
def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""): def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""):
if timestamp: if timestamp:
@ -185,14 +388,14 @@ class Session:
self.status = status self.status = status
if status == protocol_pb2.STATUS_ONLINE or status == protocol_pb2.STATUS_FFC: if status == protocol_pb2.STATUS_ONLINE or status == protocol_pb2.STATUS_FFC:
self.call("presence_sendAvailable") self.sendPresence(True)
else: else:
self.call("presence_sendUnavailable") self.sendPresence(False)
def changeStatusMessage(self, statusMessage): def changeStatusMessage(self, statusMessage):
if (statusMessage != self.statusMessage) or (self.initialized == False): if (statusMessage != self.statusMessage) or (self.initialized == False):
self.statusMessage = statusMessage self.statusMessage = statusMessage
self.call("profile_setStatus", (statusMessage.encode("utf-8"),)) self.setStatus(statusMessage.encode('utf-8'))
self.logger.info("Status message changed: %s", statusMessage) self.logger.info("Status message changed: %s", statusMessage)
if self.initialized == False: if self.initialized == False:
@ -207,9 +410,9 @@ class Session:
self.backend.handleMessage(self.user, msg[0], msg[1], "", "", msg[2]) self.backend.handleMessage(self.user, msg[0], msg[1], "", "", msg[2])
# also for adding a new buddy # also for adding a new buddy
def updateBuddy(self, buddy, nick, groups): def updateBuddy(self, buddy, nick, groups, image_hash =""):
if buddy != "bot": if buddy != "bot":
self.buddies.update(buddy, nick, groups) self.buddies.update(buddy, nick, groups, image_hash)
self.updateRoster() self.updateRoster()
def removeBuddy(self, buddy): def removeBuddy(self, buddy):
@ -230,72 +433,22 @@ class Session:
self.backend.handleSubject(self.user, room, group.subject, group.subjectOwner) self.backend.handleSubject(self.user, room, group.subject, group.subjectOwner)
else: else:
self.logger.warn("Room doesn't exist: %s", room) self.logger.warn("Room doesn't exist: %s", room)
def requestVCard(self, buddy, ID):
def updateRoster(self): def onSuccess(response, request):
self.logger.debug("Update roster") self.logger.debug('Sending VCard (%s) with image id %s',
ID, response.pictureId)
image_hash = utils.sha1hash(response.pictureData)
self.logger.debug('Image hash is %s', image_hash)
self.backend.handleVCard(self.user, ID, buddy, "", "", response.pictureData)
obuddy = self.buddies[buddy]
self.updateBuddy(buddy, obuddy.nick, obuddy.groups, image_hash)
old = self.buddies.keys() self.logger.debug('Requesting profile picture of %s', buddy)
self.buddies.load() self.requestProfilePicture(buddy, onSuccess = onSuccess)
new = self.buddies.keys()
add = set(new) - set(old)
remove = set(old) - set(new)
self.logger.debug("Roster remove: %s", str(list(remove)))
self.logger.debug("Roster add: %s", str(list(add)))
for number in remove:
self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE)
self.backend.handleBuddyRemoved(self.user, number)
self.call("presence_unsubscribe", (number + "@s.whatsapp.net",))
for number in add:
buddy = self.buddies[number]
self.backend.handleBuddyChanged(self.user, number, buddy.nick, buddy.groups, protocol_pb2.STATUS_NONE)
self.call("presence_request", (number + "@s.whatsapp.net",)) # includes presence_subscribe
# yowsup Signals
def onAuthSuccess(self, user):
self.logger.info("Auth success: %s", user)
self.backend.handleConnected(self.user)
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE)
self.updateRoster()
self.call("ready")
self.call("group_getGroups", ("participating",))
def onAuthFailed(self, user, reason):
self.logger.info("Auth failed: %s (%s)", user, reason)
self.backend.handleDisconnected(self.user, 0, reason)
self.password = None
def onDisconnected(self, reason):
self.logger.info("Disconnected from whatsapp: %s (%s)", self.legacyName, reason)
self.backend.handleDisconnected(self.user, 0, reason)
def onMessageReceived(self, messageId, jid, messageContent, timestamp, receiptRequested, pushName, isBroadCast):
buddy = jid.split("@")[0]
messageContent = utils.softToUni(messageContent)
if isBroadCast:
self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp)
messageContent = "[Broadcast] " + messageContent
else:
self.logger.info("Message received from %s to %s: %s (at ts=%s)", buddy, self.legacyName, messageContent, timestamp)
self.sendMessageToXMPP(buddy, messageContent, timestamp)
if receiptRequested: self.call("message_ack", (jid, messageId))
def onMediaReceived(self, messageId, jid, preview, url, size, receiptRequested, isBroadcast):
buddy = jid.split("@")[0]
self.logger.info("Media received from %s: %s", buddy, url)
self.sendMessageToXMPP(buddy, utils.shorten(url))
if receiptRequested: self.call("message_ack", (jid, messageId))
# Not used
def onLocationReceived(self, messageId, jid, name, preview, latitude, longitude, receiptRequested, isBroadcast): def onLocationReceived(self, messageId, jid, name, preview, latitude, longitude, receiptRequested, isBroadcast):
buddy = jid.split("@")[0] buddy = jid.split("@")[0]
self.logger.info("Location received from %s: %s, %s", buddy, latitude, longitude) self.logger.info("Location received from %s: %s, %s", buddy, latitude, longitude)
@ -304,75 +457,6 @@ class Session:
self.sendMessageToXMPP(buddy, utils.shorten(url)) self.sendMessageToXMPP(buddy, utils.shorten(url))
if receiptRequested: self.call("message_ack", (jid, messageId)) if receiptRequested: self.call("message_ack", (jid, messageId))
def onVcardReceived(self, messageId, jid, name, data, receiptRequested, isBroadcast): # TODO
buddy = jid.split("@")[0]
self.logger.info("VCard received from %s", buddy)
self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)")
if receiptRequested: self.call("message_ack", (jid, messageId))
def onContactTyping(self, jid):
buddy = jid.split("@")[0]
self.logger.info("Started typing: %s", buddy)
self.backend.handleBuddyTyping(self.user, buddy)
if self.timer != None:
self.timer.cancel()
def onContactPaused(self, jid):
buddy = jid.split("@")[0]
self.logger.info("Paused typing: %s", buddy)
self.backend.handleBuddyTyped(self.user, jid.split("@")[0])
self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, (self.user, buddy)).start()
def onPrecenceUpdated(self, jid, lastseen):
buddy = jid.split("@")[0]
self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen))
if buddy in self.presenceRequested:
timestamp = time.localtime(time.time() - lastseen)
timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp)
self.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen)))
self.presenceRequested.remove(buddy)
if lastseen < 60:
self.onPrecenceAvailable(jid)
else:
self.onPrecenceUnavailable(jid)
def onPrecenceAvailable(self, jid):
buddy = jid.split("@")[0]
try:
buddy = self.buddies[buddy]
self.logger.info("Is available: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE)
except KeyError:
self.logger.error("Buddy not found: %s", buddy)
def onPrecenceUnavailable(self, jid):
buddy = jid.split("@")[0]
try:
buddy = self.buddies[buddy]
self.logger.info("Is unavailable: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA)
except KeyError:
self.logger.error("Buddy not found: %s", buddy)
def onGroupGotInfo(self, gjid, owner, subject, subjectOwner, subjectTimestamp, creationTimestamp):
room = gjid.split("@")[0]
owner = owner.split("@")[0]
subjectOwner = subjectOwner.split("@")[0]
if room in self.groups:
room = self.groups[room]
room.owner = owner
room.subjectOwner = subjectOwner
room.subject = subject
else:
self.groups[room] = Group(room, owner, subject, subjectOwner)
self.updateRoomList()
def onGroupGotParticipants(self, gjid, jids): def onGroupGotParticipants(self, gjid, jids):
room = gjid.split("@")[0] room = gjid.split("@")[0]
@ -412,15 +496,6 @@ class Session:
if receiptRequested: self.call("subject_ack", (gjid, messageId)) if receiptRequested: self.call("subject_ack", (gjid, messageId))
# Yowsup Notifications # Yowsup Notifications
def onGroupParticipantAdded(self, gjid, jid, author, timestamp, messageId, receiptRequested):
room = gjid.split("@")[0]
buddy = jid.split("@")[0]
loggin.info("Added % to room %s", buddy, room)
self.backend.handleParticipantChanged(self.user, buddy, room, protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_ONLINE)
if receiptRequested: self.call("notification_ack", (gjid, messageId))
def onGroupParticipantRemoved(self, gjid, jid, author, timestamp, messageId, receiptRequested): def onGroupParticipantRemoved(self, gjid, jid, author, timestamp, messageId, receiptRequested):
room = gjid.split("@")[0] room = gjid.split("@")[0]
buddy = jid.split("@")[0] buddy = jid.split("@")[0]

View File

@ -25,12 +25,14 @@ __status__ = "Prototype"
""" """
import argparse import argparse
import traceback
import logging import logging
import asyncore import asyncore
import sys, os import sys, os
import MySQLdb import MySQLdb
import e4u import e4u
import threading import threading
import Queue
sys.path.insert(0, os.getcwd()) sys.path.insert(0, os.getcwd())
@ -38,6 +40,8 @@ from Spectrum2.iochannel import IOChannel
from whatsappbackend import WhatsAppBackend from whatsappbackend import WhatsAppBackend
from constants import * from constants import *
from yowsup.common import YowConstants
from yowsup.stacks import YowStack
# Arguments # Arguments
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -46,11 +50,15 @@ parser.add_argument('--host', type=str, required=True)
parser.add_argument('--port', type=int, required=True) parser.add_argument('--port', type=int, required=True)
parser.add_argument('--service.backend_id', metavar="ID", type=int, required=True) parser.add_argument('--service.backend_id', metavar="ID", type=int, required=True)
parser.add_argument('config', type=str) parser.add_argument('config', type=str)
parser.add_argument('-j', type=str, required=True)
args, unknown = parser.parse_known_args() args, unknown = parser.parse_known_args()
YowConstants.PATH_STORAGE='/var/lib/spectrum2/' + args.j
loggingfile = '/var/log/spectrum2/' + args.j + '/backends/backend.log'
# Logging # Logging
logging.basicConfig( \ logging.basicConfig( \
filename=loggingfile,\
format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s", \ format = "%(asctime)-15s %(levelname)s %(name)s: %(message)s", \
level = logging.DEBUG if args.debug else logging.INFO \ level = logging.DEBUG if args.debug else logging.INFO \
) )
@ -61,10 +69,31 @@ def handleTransportData(data):
e4u.load() e4u.load()
closed = False
def connectionClosed():
global closed
closed = True
# Main # Main
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) io = IOChannel(args.host, args.port, handleTransportData, connectionClosed)
plugin = WhatsAppBackend(io, db) plugin = WhatsAppBackend(io, db)
asyncore.loop(1) while True:
try:
asyncore.loop(timeout=1.0, count=10, use_poll = True)
try:
callback = YowStack._YowStack__detachedQueue.get(False) #doesn't block
callback()
except Queue.Empty:
pass
else:
break
if closed:
break
except SystemExit:
break
except:
logger = logging.getLogger('transwhat')
logger.error(traceback.format_exc())

View File

@ -26,6 +26,7 @@ import urllib
import json import json
import e4u import e4u
import base64 import base64
import hashlib
def shorten(url): def shorten(url):
url = urllib.urlopen("http://d.0l.de/add.json?type=URL&rdata=%s" % urllib.quote(url)) url = urllib.urlopen("http://d.0l.de/add.json?type=URL&rdata=%s" % urllib.quote(url))
@ -61,3 +62,6 @@ def softToUni(message):
def decodePassword(password): def decodePassword(password):
return base64.b64decode(bytes(password.encode("utf-8"))) return base64.b64decode(bytes(password.encode("utf-8")))
def sha1hash(data):
return hashlib.sha1(data).hexdigest()

View File

@ -36,6 +36,8 @@ class WhatsAppBackend(SpectrumBackend):
self.io = io self.io = io
self.db = db self.db = db
self.sessions = { } self.sessions = { }
# Used to prevent duplicate messages
self.lastMessage = {}
self.logger.debug("Backend started") self.logger.debug("Backend started")
@ -45,6 +47,9 @@ class WhatsAppBackend(SpectrumBackend):
if user not in self.sessions: if user not in self.sessions:
self.sessions[user] = Session(self, user, legacyName, extra, self.db) self.sessions[user] = Session(self, user, legacyName, extra, self.db)
if user not in self.lastMessage:
self.lastMessage[user] = {}
self.sessions[user].login(password) self.sessions[user].login(password)
def handleLogoutRequest(self, user, legacyName): def handleLogoutRequest(self, user, legacyName):
@ -54,8 +59,18 @@ class WhatsAppBackend(SpectrumBackend):
del self.sessions[user] del self.sessions[user]
def handleMessageSendRequest(self, user, buddy, message, xhtml = ""): def handleMessageSendRequest(self, user, buddy, message, xhtml = ""):
self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s)", user, buddy, message) self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s, xhtml = %s)", user, buddy, message, xhtml)
self.sessions[user].sendMessageToWA(buddy, message) # For some reason spectrum occasionally sends to identical messages to
# a buddy, one to the bare jid and one to /bot. This causes duplicate
# messages. Since it is unlikely a user wants to send the same message
# twice, we should just ignore the second message
#
# TODO Proper fix, this work around drops all duplicate messages even
# intentional ones.
usersMessage = self.lastMessage[user]
if buddy not in usersMessage or usersMessage[buddy] != message:
self.sessions[user].sendMessageToWA(buddy, message)
usersMessage[buddy] = message
def handleJoinRoomRequest(self, user, room, nickname, pasword): def handleJoinRoomRequest(self, user, room, nickname, pasword):
self.logger.debug("handleJoinRoomRequest(user=%s, room=%s, nickname=%s)", user, room, nickname) self.logger.debug("handleJoinRoomRequest(user=%s, room=%s, nickname=%s)", user, room, nickname)
@ -86,6 +101,10 @@ class WhatsAppBackend(SpectrumBackend):
self.logger.debug("handleStoppedTypingRequest(user=%s, buddy=%s)", user, buddy) self.logger.debug("handleStoppedTypingRequest(user=%s, buddy=%s)", user, buddy)
self.sessions[user].sendTypingStopped(buddy) self.sessions[user].sendTypingStopped(buddy)
def handleVCardRequest(self, user, buddy, ID):
self.logger.debug("handleVCardRequest(user=%s, buddy=%s, ID=%s)", user, buddy, ID)
self.sessions[user].requestVCard(buddy, ID)
# TODO # TODO
def handleBuddyBlockToggled(self, user, buddy, blocked): def handleBuddyBlockToggled(self, user, buddy, blocked):
pass pass
@ -93,9 +112,6 @@ class WhatsAppBackend(SpectrumBackend):
def handleLeaveRoomRequest(self, user, room): def handleLeaveRoomRequest(self, user, room):
pass pass
def handleVCardRequest(self, user, buddy, ID):
pass
def handleVCardUpdatedRequest(self, user, photo, nickname): def handleVCardUpdatedRequest(self, user, photo, nickname):
pass pass
@ -103,7 +119,8 @@ class WhatsAppBackend(SpectrumBackend):
pass pass
def handleFTStartRequest(self, user, buddy, fileName, size, ftID): def handleFTStartRequest(self, user, buddy, fileName, size, ftID):
pass self.logger.debug('File send request %s, for user %s, from %s, size: %s',
fileName, user, buddy, size)
def handleFTFinishRequest(self, user, buddy, fileName, size, ftID): def handleFTFinishRequest(self, user, buddy, fileName, size, ftID):
pass pass

526
yowsupwrapper.py Normal file
View File

@ -0,0 +1,526 @@
from yowsup import env
from yowsup.stacks import YowStack
from yowsup.common import YowConstants
from yowsup.layers import YowLayerEvent, YowParallelLayer
from yowsup.layers.auth import AuthError
# Layers
from yowsup.layers.axolotl import YowAxolotlLayer
from yowsup.layers.auth import YowCryptLayer, YowAuthenticationProtocolLayer
from yowsup.layers.coder import YowCoderLayer
from yowsup.layers.logger import YowLoggerLayer
from yowsup.layers.network import YowNetworkLayer
from yowsup.layers.protocol_messages import YowMessagesProtocolLayer
from yowsup.layers.stanzaregulator import YowStanzaRegulator
from yowsup.layers.protocol_media import YowMediaProtocolLayer
from yowsup.layers.protocol_acks import YowAckProtocolLayer
from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer
from yowsup.layers.protocol_groups import YowGroupsProtocolLayer
from yowsup.layers.protocol_presence import YowPresenceProtocolLayer
from yowsup.layers.protocol_ib import YowIbProtocolLayer
from yowsup.layers.protocol_notifications import YowNotificationsProtocolLayer
from yowsup.layers.protocol_iq import YowIqProtocolLayer
from yowsup.layers.protocol_contacts import YowContactsIqProtocolLayer
from yowsup.layers.protocol_chatstate import YowChatstateProtocolLayer
from yowsup.layers.protocol_privacy import YowPrivacyProtocolLayer
from yowsup.layers.protocol_profiles import YowProfilesProtocolLayer
from yowsup.layers.protocol_calls import YowCallsProtocolLayer
# ProtocolEntities
from yowsup.layers.protocol_acks.protocolentities import *
from yowsup.layers.protocol_chatstate.protocolentities import *
from yowsup.layers.protocol_groups.protocolentities import *
from yowsup.layers.protocol_media.protocolentities import *
from yowsup.layers.protocol_messages.protocolentities import *
from yowsup.layers.protocol_presence.protocolentities import *
from yowsup.layers.protocol_profiles.protocolentities import *
from yowsup.layers.protocol_receipts.protocolentities import *
from functools import partial
class YowsupApp(object):
def __init__(self):
env.CURRENT_ENV = env.S40YowsupEnv()
layers = (YowsupAppLayer,
YowParallelLayer((YowAuthenticationProtocolLayer,
YowMessagesProtocolLayer,
YowReceiptProtocolLayer,
YowAckProtocolLayer,
YowMediaProtocolLayer,
YowIbProtocolLayer,
YowIqProtocolLayer,
YowNotificationsProtocolLayer,
YowContactsIqProtocolLayer,
YowChatstateProtocolLayer,
YowCallsProtocolLayer,
YowMediaProtocolLayer,
YowPrivacyProtocolLayer,
YowProfilesProtocolLayer,
YowGroupsProtocolLayer,
YowPresenceProtocolLayer)),
YowAxolotlLayer,
YowCoderLayer,
YowCryptLayer,
YowStanzaRegulator,
YowNetworkLayer
)
self.stack = YowStack(layers)
self.stack.broadcastEvent(
YowLayerEvent(YowsupAppLayer.EVENT_START, caller = self)
)
def login(self, username, password):
"""Login to yowsup
Should result in onAuthSuccess or onAuthFailure to be called.
Args:
- username: (str) username in the form of 1239482382 (country code
and cellphone number)
- password: (str) base64 encoded password
"""
self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS,
(username, password))
self.stack.setProp(YowNetworkLayer.PROP_ENDPOINT,
YowConstants.ENDPOINTS[0])
self.stack.setProp(YowCoderLayer.PROP_DOMAIN,
YowConstants.DOMAIN)
self.stack.setProp(YowCoderLayer.PROP_RESOURCE,
env.CURRENT_ENV.getResource())
# self.stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 5)
try:
self.stack.broadcastEvent(
YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT))
except TypeError as e: # Occurs when password is not correctly formated
self.onAuthFailure('password not base64 encoded')
# try:
# self.stack.loop(timeout=0.5, discrete=0.5)
# except AuthError as e: # For some reason Yowsup throws an exception
# self.onAuthFailure("%s" % e)
def logout(self):
"""
Logout from whatsapp
"""
self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT))
def sendReceipt(self, _id, _from, read, participant):
"""
Send a receipt (delivered: double-tick, read: blue-ticks)
Args:
- _id: id of message received
- _from: jid of person who sent the message
- read: ('read' or None) None is just delivered, 'read' is read
- participant
"""
receipt = OutgoingReceiptProtocolEntity(_id, _from, read, participant)
self.sendEntity(receipt)
def sendTextMessage(self, to, message):
"""
Sends a text message
Args:
- to: (xxxxxxxxxx@s.whatsapp.net) who to send the message to
- message: (str) the body of the message
"""
messageEntity = TextMessageProtocolEntity(message, to = to)
self.sendEntity(messageEntity)
def sendPresence(self, available):
"""
Send presence to whatsapp
Args:
- available: (boolean) True if available false otherwise
"""
if available:
self.sendEntity(AvailablePresenceProtocolEntity())
else:
self.sendEntity(UnavailablePresenceProtocolEntity())
def subscribePresence(self, phone_number):
"""
Subscribe to presence updates from phone_number
Args:
- phone_number: (str) The cellphone number of the person to
subscribe to
"""
jid = phone_number + '@s.whatsapp.net'
entity = SubscribePresenceProtocolEntity(jid)
self.sendEntity(entity)
def unsubscribePresence(self, phone_number):
"""
Unsubscribe to presence updates from phone_number
Args:
- phone_number: (str) The cellphone number of the person to
unsubscribe from
"""
jid = phone_number + '@s.whatsapp.net'
entity = UnsubscribePresenceProtocolEntity(jid)
self.sendEntity(entity)
def setStatus(self, statusText):
"""
Send status to whatsapp
Args:
- statusTest: (str) Your whatsapp status
"""
iq = SetStatusIqProtocolEntity(statusText)
self.sendIq(iq)
def sendTyping(self, phoneNumber, typing):
"""
Notify buddy using phoneNumber that you are typing to him
Args:
- phoneNumber: (str) cellphone number of the buddy you are typing to.
- typing: (bool) True if you are typing, False if you are not
"""
jid = phoneNumber + '@s.whatsapp.net'
if typing:
state = OutgoingChatstateProtocolEntity(
ChatstateProtocolEntity.STATE_TYPING, jid
)
else:
state = OutgoingChatstateProtocolEntity(
ChatstateProtocolEntity.STATE_PAUSED, jid
)
self.sendEntity(state)
def requestLastSeen(self, phoneNumber, success = None, failure = None):
"""
Requests when user was last seen.
Args:
- phone_number: (str) the phone number of the user
- success: (func) called when request is successfully processed.
The first argument is the number, second argument is the seconds
since last seen.
- failure: (func) called when request has failed
"""
iq = LastseenIqProtocolEntity(phoneNumber + '@s.whatsapp.net')
self.sendIq(iq, onSuccess = partial(self._lastSeenSuccess, success),
onError = failure)
def _lastSeenSuccess(self, success, response, request):
success(response._from.split('@')[0], response.seconds)
def requestProfilePicture(self, phoneNumber, onSuccess = None, onFailure = None):
"""
Requests profile picture of whatsapp user
Args:
- phoneNumber: (str) the phone number of the user
- success: (func) called when request is successfully processed.
- failure: (func) called when request has failed
"""
iq = GetPictureIqProtocolEntity(phoneNumber + '@s.whatsapp.net')
self.sendIq(iq, onSuccess = onSuccess, onError = onFailure)
def requestGroupsList(self, onSuccess = None, onFailure = None):
iq = ListGroupsIqProtocolEntity()
self.sendIq(iq, onSuccess = onSuccess, onError = onFailure)
def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t):
"""
Called when login is successful.
Args:
- status
- kind
- creation
- expiration
- props
- nonce
- t
"""
pass
def onAuthFailure(self, reason):
"""
Called when login is a failure
Args:
- reason: (str) Reason for the login failure
"""
pass
def onReceipt(self, _id, _from, timestamp, type, participant, offline, items):
"""
Called when a receipt is received (double tick or blue tick)
Args
- _id
- _from
- timestamp
- type: Is 'read' for blue ticks and None for double-ticks
- participant: (dxxxxxxxxxx@s.whatsapp.net) delivered to or
read by this participant in group
- offline: (True, False or None)
- items
"""
pass
def onAck(self, _id,_class, _from, timestamp):
"""
Called when Ack is received
Args:
- _id
- _class: ('message', 'receipt' or something else?)
- _from
- timestamp
"""
pass
def onPresenceReceived(self, _type, name, _from, last):
"""
Called when presence (e.g. available, unavailable) is received
from whatsapp
Args:
- _type: (str) 'available' or 'unavailable'
- _name
- _from
- _last
"""
pass
def onDisconnect(self):
"""
Called when disconnected from whatsapp
"""
def onContactTyping(self, number):
"""
Called when contact starts to type
Args:
- number: (str) cellphone number of contact
"""
pass
def onContactPaused(self, number):
"""
Called when contact stops typing
Args:
- number: (str) cellphone number of contact
"""
pass
def onTextMessage(self, _id, _from, to, notify, timestamp, participant, offline, retry, body):
"""
Called when text message is received
Args:
- _id:
- _from: (str) jid of of sender
- to:
- notify: (str) human readable name of _from (e.g. John Smith)
- timestamp:
- participant: (str) jid of user who sent the message in a groupchat
- offline:
- retry:
- body: The content of the message
"""
pass
def onImage(self, entity):
"""
Called when image message is received
Args:
- entity: ImageDownloadableMediaMessageProtocolEntity
"""
pass
def onAudio(self, entity):
"""
Called when audio message is received
Args:
- entity: AudioDownloadableMediaMessageProtocolEntity
"""
pass
def onVideo(self, entity):
"""
Called when video message is received
Args:
- entity: VideoDownloadableMediaMessageProtocolEntity
"""
pass
def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant):
"""
Called when VCard message is received
Args:
- _id: (str) id of entity
- _from:
- name:
- card_data:
- to:
- notify:
- timestamp:
- participant:
"""
pass
def sendEntity(self, entity):
"""Sends an entity down the stack (as if YowsupAppLayer called toLower)"""
self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT,
entity = entity
))
def sendIq(self, iq, onSuccess = None, onError = None):
self.stack.broadcastEvent(
YowLayerEvent(
YowsupAppLayer.SEND_IQ,
iq = iq,
success = onSuccess,
failure = onError,
)
)
from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback
class YowsupAppLayer(YowInterfaceLayer):
EVENT_START = 'transwhat.event.YowsupAppLayer.start'
TO_LOWER_EVENT = 'transwhat.event.YowsupAppLayer.toLower'
SEND_IQ = 'transwhat.event.YowsupAppLayer.sendIq'
def onEvent(self, layerEvent):
# We cannot pass instance varaibles in through init, so we use an event
# instead
# Return False if you want the event to propogate down the stack
# return True otherwise
if layerEvent.getName() == YowsupAppLayer.EVENT_START:
self.caller = layerEvent.getArg('caller')
return True
elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED:
self.caller.onDisconnect()
return True
elif layerEvent.getName() == YowsupAppLayer.TO_LOWER_EVENT:
self.toLower(layerEvent.getArg('entity'))
return True
elif layerEvent.getName() == YowsupAppLayer.SEND_IQ:
iq = layerEvent.getArg('iq')
success = layerEvent.getArg('success')
failure = layerEvent.getArg('failure')
self._sendIq(iq, success, failure)
return True
return False
@ProtocolEntityCallback('success')
def onAuthSuccess(self, entity):
# entity is SuccessProtocolEntity
status = entity.status
kind = entity.kind
creation = entity.creation
expiration = entity.expiration
props = entity.props
nonce = entity.nonce
t = entity.t # I don't know what this is
self.caller.onAuthSuccess(status, kind, creation, expiration, props, nonce, t)
@ProtocolEntityCallback('failure')
def onAuthFailure(self, entity):
# entity is FailureProtocolEntity
reason = entity.reason
self.caller.onAuthFailure(reason)
@ProtocolEntityCallback('receipt')
def onReceipt(self, entity):
"""Sends ack automatically"""
# entity is IncomingReceiptProtocolEntity
ack = OutgoingAckProtocolEntity(entity.getId(),
'receipt', entity.getType(), entity.getFrom())
self.toLower(ack)
_id = entity._id
_from = entity._from
timestamp = entity.timestamp
type = entity.type
participant = entity.participant
offline = entity.offline
items = entity.items
self.caller.onReceipt(_id, _from, timestamp, type, participant, offline, items)
@ProtocolEntityCallback('ack')
def onAck(self, entity):
# entity is IncomingAckProtocolEntity
self.caller.onAck(
entity._id,
entity._class,
entity._from,
entity.timestamp
)
@ProtocolEntityCallback('notification')
def onNotification(self, entity):
"""
Sends ack automatically
"""
self.toLower(entity.ack())
@ProtocolEntityCallback('message')
def onMessageReceived(self, entity):
if entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_TEXT:
self.caller.onTextMessage(
entity._id,
entity._from,
entity.to,
entity.notify,
entity.timestamp,
entity.participant,
entity.offline,
entity.retry,
entity.body
)
elif entity.getType() == MessageProtocolEntity.MESSAGE_TYPE_MEDIA:
if isinstance(entity, ImageDownloadableMediaMessageProtocolEntity):
# There is just way too many fields to pass them into the
# function
self.caller.onImage(entity)
elif isinstance(entity, AudioDownloadableMediaMessageProtocolEntity):
self.caller.onAudio(entity)
elif isinstance(entity, VideoDownloadableMediaMessageProtocolEntity):
self.caller.onVideo(entity)
elif isinstance(entity, VCardMediaMessageProtocolEntity):
self.caller.onVCard(
entity._id,
entity._from,
entity.name,
entity.card_data,
entity.to,
entity.notify,
entity.timestamp,
entity.participant
)
@ProtocolEntityCallback('presence')
def onPresenceReceived(self, presence):
_type = presence.getType()
name = presence.getName()
_from = presence.getFrom()
last = presence.getLast()
self.caller.onPresenceReceived(_type, name, _from, last)
@ProtocolEntityCallback('chatstate')
def onChatstate(self, chatstate):
number = chatstate._from.split('@')[0]
if chatstate.getState() == ChatstateProtocolEntity.STATE_TYPING:
self.caller.onContactTyping(number)
else:
self.caller.onContactPaused(number)