diff --git a/INSTALL.md b/INSTALL.md
index 29cb6ca..6c30fb4 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -63,6 +63,9 @@ Create a new file `/etc/spectrum2/transports/whatsapp.cfg` with the following co
[logging]
config = /etc/spectrum2/logging.cfg
backend_config = /etc/spectrum2/backend-logging.cfg
+
+ [database]
+ type = sqlite3
## transWhat
@@ -74,7 +77,7 @@ Checkout the latest version of transWhat from GitHub:
Install required dependencies:
- $ pip install --pre e4u protobuf python-dateutil yowsup
+ $ pip install --pre e4u protobuf python-dateutil yowsup2
- **e4u**: is a simple emoji4unicode python bindings
- [**yowsup**](https://github.com/tgalal/yowsup): is a python library that enables you build application which use WhatsApp service.
diff --git a/Spectrum2/backend.py b/Spectrum2/backend.py
index feb2e94..b2e5891 100644
--- a/Spectrum2/backend.py
+++ b/Spectrum2/backend.py
@@ -222,13 +222,29 @@ class SpectrumBackend:
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA);
self.send(message)
- def handleBackendConfig(self, section, key, value):
+ 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()
- c.config = "[%s]\n%s = %s\n" % (section, key, value)
+ 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):
@@ -252,7 +268,6 @@ class SpectrumBackend:
def handleConvMessagePayload(self, data):
payload = protocol_pb2.ConversationMessage()
- self.logger.error("handleConvMessagePayload")
if (payload.ParseFromString(data) == False):
#TODO: ERROR
return
diff --git a/USAGE.md b/USAGE.md
index dcea058..67b4c8b 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -28,7 +28,6 @@ The bot is one of the contacts every user has in its contact list. It offers you
| ------------ | --------------- |
| `\help` | show this message |
| `\prune` | clear your buddylist |
-| `\sync` | sync your imported contacts with WhatsApp |
| `\lastseen` | request last online timestamp from buddy |
| `\leave` | permanently leave group chat |
| `\groups` | print all attended groups |
diff --git a/bot.py b/bot.py
index dcd3708..631d82a 100644
--- a/bot.py
+++ b/bot.py
@@ -37,13 +37,12 @@ class Bot():
self.commands = {
"help": self._help,
"prune": self._prune,
- "sync": self._sync,
"groups": self._groups,
"getgroups": self._getgroups
}
def parse(self, message):
- args = message.split(" ")
+ args = message.strip().split(" ")
cmd = args.pop(0)
if cmd[0] == '\\':
@@ -57,7 +56,7 @@ class Bot():
self.send("a valid command starts with a backslash")
def call(self, cmd, args = []):
- func = self.commands[cmd]
+ func = self.commands[cmd.lower()]
spec = inspect.getargspec(func)
maxs = len(spec.args) - 1
reqs = maxs - len(spec.defaults or [])
@@ -71,23 +70,10 @@ class Bot():
self.session.backend.handleMessage(self.session.user, self.name, message)
# commands
- def _sync(self):
- user = self.session.legacyName
- password = self.session.password
-
- count = self.session.buddies.sync(user, password)
- self.session.updateRoster()
-
- if count:
- self.send("sync complete, %d buddies are using WhatsApp" % count)
- else:
- self.send("sync failed, sorry something went wrong")
-
def _help(self):
self.send("""following bot commands are available:
\\help show this message
\\prune clear your buddylist
-\\sync sync your imported contacts with WhatsApp
following user commands are available:
\\lastseen request last online timestamp from buddy
diff --git a/buddy.py b/buddy.py
index 0823163..d8d3da7 100644
--- a/buddy.py
+++ b/buddy.py
@@ -24,6 +24,12 @@ __email__ = "post@steffenvogel.de"
from Spectrum2 import protocol_pb2
import logging
+import time
+import utils
+import base64
+
+import deferred
+from deferred import call
class Buddy():
@@ -33,11 +39,10 @@ class Buddy():
self.number = number
self.groups = groups
self.image_hash = image_hash if image_hash is not None else ""
- self.statusMsg = ""
+ self.statusMsg = u""
self.lastseen = 0
self.presence = 0
-
def update(self, nick, groups, image_hash):
self.nick = nick
self.groups = groups
@@ -55,13 +60,12 @@ class BuddyList(dict):
self.session = session
self.user = user
self.logger = logging.getLogger(self.__class__.__name__)
- self.synced = False
def _load(self, buddies):
for buddy in buddies:
number = buddy.buddyName
nick = buddy.alias
- statusMsg = buddy.statusMessage
+ statusMsg = buddy.statusMessage.decode('utf-8')
groups = [g for g in buddy.group]
image_hash = buddy.iconHash
self[number] = Buddy(self.owner, number, nick, statusMsg,
@@ -69,35 +73,45 @@ class BuddyList(dict):
self.logger.debug("Update roster")
-# old = self.buddies.keys()
-# self.buddies.load()
-# new = self.buddies.keys()
-# contacts = new
contacts = self.keys()
+ contacts.remove('bot')
- if self.synced == False:
- self.session.sendSync(contacts, delta = False, interactive = True)
- self.synced = True
+ self.session.sendSync(contacts, delta=False, interactive=True,
+ success=self.onSync)
-# 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(contacts)))
-# 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 contacts:
buddy = self[number]
- if number != 'bot':
- 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.session.subscribePresence(number)
+ self.updateSpectrum(buddy)
+
+ def onSync(self, existing, nonexisting, invalid):
+ """We should only presence subscribe to existing numbers"""
+
+ for number in existing:
+ self.session.subscribePresence(number)
+ self.logger.debug("%s is requesting statuses of: %s", self.user, existing)
+ self.session.requestStatuses(existing, success = self.onStatus)
+
+ self.logger.debug("Removing nonexisting buddies %s", nonexisting)
+ for number in nonexisting:
+ self.remove(number)
+ del self[number]
+
+ self.logger.debug("Removing invalid buddies %s", invalid)
+ for number in invalid:
+ self.remove(number)
+ del self[number]
+
+ def onStatus(self, contacts):
+ self.logger.debug("%s received statuses of: %s", self.user, contacts)
+ for number, (status, time) in contacts.iteritems():
+ buddy = self[number]
+ if status is None:
+ buddy.statusMsg = ""
+ else:
+ buddy.statusMsg = utils.softToUni(status)
+ self.updateSpectrum(buddy)
def load(self, buddies):
@@ -111,23 +125,38 @@ class BuddyList(dict):
buddy = self[number]
buddy.update(nick, groups, image_hash)
else:
- self.session.sendSync([number], delta = True, interactive = True)
- self.session.subscribePresence(number)
buddy = Buddy(self.owner, number, nick, "", groups, image_hash)
self[number] = buddy
self.logger.debug("Roster add: %s", buddy)
+ self.session.sendSync([number], delta = True, interactive = True)
+ self.session.subscribePresence(number)
+ self.session.requestStatuses([number], success = self.onStatus)
+ if image_hash == "" or image_hash is None:
+ self.requestVCard(number)
+ self.updateSpectrum(buddy)
+ return buddy
+ def updateSpectrum(self, buddy):
if buddy.presence == 0:
status = protocol_pb2.STATUS_NONE
elif buddy.presence == 'unavailable':
status = protocol_pb2.STATUS_AWAY
else:
status = protocol_pb2.STATUS_ONLINE
- self.backend.handleBuddyChanged(self.user, number, buddy.nick,
- buddy.groups, status,
- iconHash = buddy.image_hash if buddy.image_hash is not None else "")
- return buddy
+ statusmsg = buddy.statusMsg
+ if buddy.lastseen != 0:
+ timestamp = time.localtime(buddy.lastseen)
+ statusmsg += time.strftime("\n Last seen: %a, %d %b %Y %H:%M:%S", timestamp)
+
+ iconHash = buddy.image_hash if buddy.image_hash is not None else ""
+
+ self.logger.debug("Updating buddy %s (%s) in %s, image_hash = %s",
+ buddy.nick, buddy.number, buddy.groups, iconHash)
+ self.logger.debug("Status Message: %s", statusmsg)
+ self.backend.handleBuddyChanged(self.user, buddy.number, buddy.nick,
+ buddy.groups, status, statusMessage=statusmsg, iconHash=iconHash)
+
def remove(self, number):
try:
@@ -141,3 +170,49 @@ class BuddyList(dict):
return buddy
except KeyError:
return None
+
+ def requestVCard(self, buddy, ID=None):
+ if buddy == self.user or buddy == self.user.split('@')[0]:
+ buddy = self.session.legacyName
+
+ # Get profile picture
+ self.logger.debug('Requesting profile picture of %s', buddy)
+ response = deferred.Deferred()
+ # Error probably means image doesn't exist
+ error = deferred.Deferred()
+ self.session.requestProfilePicture(buddy, onSuccess=response.run,
+ onFailure=error.run)
+ response = response.arg(0)
+
+ pictureData = response.pictureData()
+ # Send VCard
+ if ID != None:
+ call(self.logger.debug, 'Sending VCard (%s) with image id %s: %s',
+ ID, response.pictureId(), pictureData.then(base64.b64encode))
+ call(self.backend.handleVCard, self.user, ID, buddy, "", "",
+ pictureData)
+ # If error
+ error.when(self.logger.debug, 'Sending VCard (%s) without image', ID)
+ error.when(self.backend.handleVCard, self.user, ID, buddy, "", "", "")
+
+ # Send image hash
+ if not buddy == self.session.legacyName:
+ try:
+ obuddy = self[buddy]
+ nick = obuddy.nick
+ groups = obuddy.groups
+ except KeyError:
+ nick = ""
+ groups = []
+ image_hash = pictureData.then(utils.sha1hash)
+ call(self.logger.debug, 'Image hash is %s', image_hash)
+ call(self.update, buddy, nick, groups, image_hash)
+ # No image
+ error.when(self.logger.debug, 'No image')
+ error.when(self.update, buddy, nick, groups, '')
+
+ def refresh(self, number):
+ self.session.unsubscribePresence(number)
+ self.session.subscribePresence(number)
+ self.requestVCard(number)
+ self.session.requestStatuses([number], success = self.onStatus)
diff --git a/deferred.py b/deferred.py
new file mode 100644
index 0000000..e270c5d
--- /dev/null
+++ b/deferred.py
@@ -0,0 +1,139 @@
+from functools import partial
+
+class Deferred(object):
+ """
+ Represents a delayed computation. This is a more elegant way to deal with
+ callbacks.
+
+ A Deferred object can be thought of as a computation whose value is yet to
+ be determined. We can manipulate the Deferred as if it where a regular
+ value by using the then method. Computations dependent on the Deferred will
+ only proceed when the run method is called.
+
+ Attributes of a Deferred can be accessed directly as methods. The result of
+ calling these functions will be Deferred.
+
+ Example:
+ image = Deferred()
+ getImageWithCallback(image.run)
+ image.then(displayFunc)
+
+ colors = Deferred()
+ colors.append('blue')
+ colors.then(print)
+ colors.run(['red', 'green']) #=> ['red', 'green', 'blue']
+ """
+
+ def __init__(self):
+ self.subscribers = []
+ self.computed = False
+ self.args = None
+ self.kwargs = None
+
+ def run(self, *args, **kwargs):
+ """
+ Give a value to the deferred. Calling this method more than once will
+ result in a DeferredHasValue exception to be raised.
+ """
+ if self.computed:
+ raise DeferredHasValue("Deferred object already has a value.")
+ else:
+ self.args = args
+ self.kwargs = kwargs
+ for func, deferred in self.subscribers:
+ deferred.run(func(*args, **kwargs))
+ self.computed = True
+
+ def then(self, func):
+ """
+ Apply func to Deferred value. Returns a Deferred whose value will be
+ the result of applying func.
+ """
+ result = Deferred()
+ if self.computed:
+ result.run(func(*self.args, **self.kwargs))
+ else:
+ self.subscribers.append((func, result))
+ return result
+
+ def arg(self, n):
+ """
+ Returns the nth positional argument of a deferred as a deferred
+
+ Args:
+ n - the index of the positional argument
+ """
+ def helper(*args, **kwargs):
+ return args[n]
+ return self.then(helper)
+
+ def when(self, func, *args, **kwargs):
+ """ Calls when func(*args, **kwargs) when deferred gets a value """
+ def helper(*args2, **kwargs2):
+ func(*args, **kwargs)
+ return self.then(helper)
+
+ def __getattr__(self, method_name):
+ return getattr(Then(self), method_name)
+
+
+class Then(object):
+ """
+ Allows you to call methods on a Deferred.
+
+ Example:
+ colors = Deferred()
+ Then(colors).append('blue')
+ colors.run(['red', 'green'])
+ colors.then(print) #=> ['red', 'green', 'blue']
+ """
+ def __init__(self, deferred):
+ self.deferred = deferred
+
+ def __getattr__(self, name):
+ def tryCall(obj, *args, **kwargs):
+ if callable(obj):
+ return obj(*args, **kwargs)
+ else:
+ return obj
+ def helper(*args, **kwargs):
+ func = (lambda x: tryCall(getattr(x, name), *args, **kwargs))
+ return self.deferred.then(func)
+ return helper
+
+def call(func, *args, **kwargs):
+ """
+ Call a function with deferred arguments
+
+ Example:
+ colors = Deferred()
+ colors.append('blue')
+ colors.run(['red', 'green'])
+ call(print, colors) #=> ['red', 'green', 'blue']
+ call(print, 'hi', colors) #=> hi ['red', 'green', 'blue']
+ """
+ for i, c in enumerate(args):
+ if isinstance(c, Deferred):
+ # Function without deferred arguments
+ normalfunc = partial(func, *args[:i])
+ # Function with deferred and possibly deferred arguments
+ def restfunc(*arg2, **kwarg2):
+ apply_deferred = partial(normalfunc, *arg2, **kwarg2)
+ return call(apply_deferred, *args[i + 1:], **kwargs)
+ return c.then(restfunc)
+ items = kwargs.items()
+ for i, (k, v) in enumerate(items):
+ if isinstance(v, Deferred):
+ # Function without deferred arguments
+ normalfunc = partial(func, *args, **dict(items[:i]))
+ # Function with deferred and possibly deferred arguments
+ def restfunc2(*arg2, **kwarg2):
+ apply_deferred = partial(normalfunc, *arg2, **kwarg2)
+ return call(apply_deferred, **dict(items[i + 1:]))
+ return v.then(restfunc2)
+ # No items deferred
+ return func(*args, **kwargs)
+
+class DeferredHasValue(Exception):
+ def __init__(self, string):
+ super(DeferredHasValue, self).__init__(string)
diff --git a/group.py b/group.py
index 8708fe7..224b183 100644
--- a/group.py
+++ b/group.py
@@ -21,14 +21,83 @@ __email__ = "post@steffenvogel.de"
along with transWhat. If not, see .
"""
+from Spectrum2 import protocol_pb2
+
class Group():
- def __init__(self, id, owner, subject, subjectOwner):
+ def __init__(self, id, owner, subject, subjectOwner, backend, user):
self.id = id
self.subject = subject
self.subjectOwner = subjectOwner
self.owner = owner
self.joined = False
+ self.backend = backend
+ self.user = user
self.nick = "me"
- self.participants = []
+ # Participants is a number -> nickname dict
+ self.participants = {}
+
+ def addParticipants(self, participants, buddies, yourNumber):
+ """
+ Adds participants to the group.
+
+ Args:
+ - participants: (Iterable) phone numbers of participants
+ - buddies: (dict) Used to get the nicknames of the participants
+ - yourNumber: The number you are using
+ """
+ for jid in participants:
+ number = jid.split('@')[0]
+ try:
+ nick = buddies[number].nick
+ except KeyError:
+ nick = number
+ if number == yourNumber:
+ nick = self.nick
+ if nick == "":
+ nick = number
+ self.participants[number] = nick
+
+ def sendParticipantsToSpectrum(self, yourNumber):
+ for number, nick in self.participants.iteritems():
+ if number == self.owner:
+ flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
+ else:
+ flags = protocol_pb2.PARTICIPANT_FLAG_NONE
+ if number == yourNumber:
+ flags = flags | protocol_pb2.PARTICIPANT_FLAG_ME
+
+ 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 = 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 = 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 = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
+ else:
+ 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, newNick = ""):
+ nick = self.participants[number]
+ # Notice the status message is the buddy's number
+ if self.joined:
+ self.backend.handleParticipantChanged(
+ self.user, nick, self.id, flags,
+ status, number, newname = newNick)
diff --git a/reader.py b/reader.py
deleted file mode 100644
index 1137973..0000000
--- a/reader.py
+++ /dev/null
@@ -1,48 +0,0 @@
-__author__ = "Steffen Vogel"
-__copyright__ = "Copyright 2015, 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 .
-"""
-
-import time
-
-def get_token(number, timeout = 30):
- file = open('tokens')
- file.seek(-1, 2)
-
- count = 0
- while count < timeout:
- line = file.readline()
-
- if line in ["", "\n"]:
- time.sleep(1)
- count += 1
- continue
- else:
- t, n, tk = line[:-1].split("\t")
-
- if (n == number):
- file.close()
- return tk
-
- file.close()
-
-
-print get_token("4917696978528")
diff --git a/registersession.py b/registersession.py
new file mode 100644
index 0000000..2fc63ca
--- /dev/null
+++ b/registersession.py
@@ -0,0 +1,147 @@
+from Spectrum2 import protocol_pb2
+
+from yowsupwrapper import YowsupApp
+import logging
+import threadutils
+import sys
+
+class RegisterSession(YowsupApp):
+ """
+ A dummy Session object that is used to register a user to whatsapp
+ """
+ WANT_CC = 0
+ WANT_SMS = 1
+ def __init__(self, backend, user, legacyName, extra):
+ self.user = user
+ self.number = legacyName
+ self.backend = backend
+ self.countryCode = ''
+ self.logger = logging.getLogger(self.__class__.__name__)
+ self.state = self.WANT_CC
+
+ def login(self, password=""):
+ self.backend.handleConnected(self.user)
+ self.backend.handleBuddyChanged(self.user, 'bot', 'bot',
+ ['Admin'], protocol_pb2.STATUS_ONLINE)
+ self.backend.handleMessage(self.user, 'bot',
+ 'Please enter your country code')
+
+ def sendMessageToWA(self, buddy, message, ID='', xhtml=''):
+ if buddy == 'bot' and self.state == self.WANT_CC:
+ try:
+ country_code = int(message.strip())
+ except ValueError:
+ self.backend.handleMessage(self.user, 'bot',
+ 'Country code must be a number')
+ else: # Succeded in decoding country code
+ country_code = str(country_code)
+ if country_code != self.number[:len(country_code)]:
+ self.backend.handleMessage(self.user,
+ 'bot', 'Number does not start with provided country code')
+ else:
+ self.backend.handleMessage(self.user, 'bot', 'Requesting sms code')
+ self.logger.debug('Requesting SMS code for %s', self.user)
+ self.countryCode = country_code
+ self._requestSMSCodeNonBlock()
+ elif buddy == 'bot' and self.state == self.WANT_SMS:
+ code = message.strip()
+ if self._checkSMSFormat(code):
+ self._requestPassword(code)
+ else:
+ self.backend.handleMessage(self.user,
+ 'bot', 'Invalid code. Must be of the form XXX-XXX.')
+ else:
+ self.logger.warn('Unauthorised user (%s) attempting to send messages',
+ self.user)
+ self.backend.handleMessage(self.user, buddy,
+ 'You are not logged in yet. You can only send messages to bot.')
+
+ def _checkSMSFormat(self, sms):
+ splitting = sms.split('-')
+ if len(splitting) != 2:
+ return False
+ a, b = splitting
+ if len(a) != 3 and len(b) != 3:
+ return False
+ try:
+ int(a)
+ int(b)
+ except ValueError:
+ return False
+ return True
+
+ def _requestSMSCodeNonBlock(self):
+ number = self.number[len(self.countryCode):]
+ threadFunc = lambda: self.requestSMSCode(self.countryCode, number)
+ threadutils.runInThread(threadFunc, self._confirmation)
+ self.backend.handleMessage(self.user, 'bot', 'SMS Code Sent')
+
+ def _confirmation(self, result):
+ self.state = self.WANT_SMS
+ resultStr = self._resultToString(result)
+ self.backend.handleMessage(self.user, 'bot', 'Response:')
+ self.backend.handleMessage(self.user, 'bot', resultStr)
+ self.backend.handleMessage(self.user, 'bot', 'Please enter SMS Code')
+
+ def _requestPassword(self, smsCode):
+ cc = self.countryCode
+ number = self.number[len(cc):]
+ threadFunc = lambda: self.requestPassword(cc, number, smsCode)
+ threadutils.runInThread(threadFunc, self._gotPassword)
+ self.backend.handleMessage(self.user, 'bot', 'Getting Password')
+
+ def _gotPassword(self, result):
+ resultStr = self._resultToString(result)
+ self.backend.handleMessage(self.user, 'bot', 'Response:')
+ self.backend.handleMessage(self.user, 'bot', resultStr)
+ self.backend.handleMessage(self.user, 'bot', 'Logging you in')
+ password = result['pw']
+ self.backend.relogin(self.user, self.number, password, None)
+
+ def _resultToString(self, result):
+ unistr = str if sys.version_info >= (3, 0) else unicode
+ out = []
+ for k, v in result.items():
+ if v is None:
+ continue
+ out.append("%s: %s" %(k, v.encode("utf-8") if type(v) is unistr else v))
+
+ return "\n".join(out)
+
+ # Dummy methods. Whatsapp backend might call these, but they should have no
+ # effect
+ def logout(self):
+ pass
+
+ def joinRoom(self, room, nickname):
+ pass
+
+ def leaveRoom(self, room):
+ pass
+
+ def changeStatusMessage(self, statusMessage):
+ pass
+
+ def changeStatus(self, status):
+ pass
+
+ def loadBuddies(self, buddies):
+ pass
+
+ def updateBuddy(self, buddies):
+ pass
+
+ def removeBuddy(self, buddies):
+ pass
+
+ def sendTypingStarted(self, buddy):
+ pass
+
+ def sendTypingStopped(self, buddy):
+ pass
+
+ def requestVCard(self, buddy, ID):
+ pass
+
+ def setProfilePicture(self, previewPicture, fullPicture = None):
+ pass
diff --git a/session.py b/session.py
index 6dfed66..f1f6c34 100644
--- a/session.py
+++ b/session.py
@@ -30,7 +30,6 @@ from PIL import Image
import sys
import os
-from yowsup.common.tools import TimeTools
from yowsup.layers.protocol_media.mediauploader import MediaUploader
from yowsup.layers.protocol_media.mediadownloader import MediaDownloader
@@ -40,6 +39,8 @@ 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
@@ -53,6 +54,7 @@ class MsgIDs:
class Session(YowsupApp):
+ broadcast_prefix = u'\U0001F4E2 '
def __init__(self, backend, user, legacyName, extra):
super(Session, self).__init__()
@@ -144,9 +146,10 @@ class Session(YowsupApp):
oroom.subjectOwner = subjectOwner
oroom.subject = subject
else:
- self.groups[room] = Group(room, owner, subject, subjectOwner)
+ self.groups[room] = Group(room, owner, subject, subjectOwner, self.backend, self.user)
# self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0])
- self.groups[room].participants = group.getParticipants().keys()
+ self.groups[room].addParticipants(group.getParticipants().keys(),
+ self.buddies, self.legacyName)
#self._addParticipantsToRoom(room, group.getParticipants())
@@ -173,13 +176,15 @@ class Session(YowsupApp):
self.legacyName, room, nick)
group = self.groups[room]
+ group.joined = True
group.nick = nick
+ group.participants[self.legacyName] = nick
try:
- ownerNick = self.buddies[group.subjectOwner].nick
+ ownerNick = group.participants[group.subjectOwner]
except KeyError:
ownerNick = group.subjectOwner
- self._refreshParticipants(room)
+ group.sendParticipantsToSpectrum(self.legacyName)
self.backend.handleSubject(self.user, self._shortenGroupId(room),
group.subject, ownerNick)
self.logger.debug("Room subject: room=%s, subject=%s",
@@ -187,7 +192,6 @@ class Session(YowsupApp):
self.backend.handleRoomNicknameChanged(
self.user, self._shortenGroupId(room), group.subject
)
- group.joined = True
else:
self.logger.warn("Room doesn't exist: %s", room)
@@ -199,29 +203,6 @@ class Session(YowsupApp):
else:
self.logger.warn("Room doesn't exist: %s. Unable to leave.", room)
- def _refreshParticipants(self, room):
- group = self.groups[room]
- for jid in group.participants:
- buddy = jid.split("@")[0]
- self.logger.info("Added %s to room %s", buddy, room)
- try:
- nick = self.buddies[buddy].nick
- except KeyError:
- nick = buddy
- if nick == "":
- nick = buddy
-
- if buddy == group.owner:
- flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
- else:
- flags = protocol_pb2.PARTICIPANT_FLAG_NONE
- if buddy == self.legacyName:
- nick = group.nick
- flags = flags | protocol_pb2.PARTICIPANT_FLAG_ME
- self.backend.handleParticipantChanged(
- self.user, nick, self._shortenGroupId(room), flags,
- protocol_pb2.STATUS_ONLINE, buddy)
-
def _lastSeen(self, number, seconds):
self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds)))
if seconds < 60:
@@ -237,16 +218,26 @@ class Session(YowsupApp):
self.backend.handleConnected(self.user)
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name,
["Admin"], protocol_pb2.STATUS_ONLINE)
- if self.initialized == False:
- self.sendOfflineMessages()
- #self.bot.call("welcome")
- self.initialized = True
+ # Initialisation?
+ self.requestPrivacyList()
+ self.requestClientConfig()
+ self.requestServerProperties()
+ # ?
+
+ self.logger.debug('Requesting groups list')
+ self.requestGroupsList(self._updateGroups)
+ # self.requestBroadcastList()
+
+ # This should handle, sync, statuses, and presence
self.sendPresence(True)
for func in self.loginQueue:
func()
- self.logger.debug('Requesting groups list')
- self.requestGroupsList(self._updateGroups)
+ if self.initialized == False:
+ self.sendOfflineMessages()
+ #self.bot.call("welcome")
+ self.initialized = True
+
self.loggedIn = True
# Called by superclass
@@ -268,16 +259,13 @@ class Session(YowsupApp):
type, participant, offline, items]))
)
try:
- buddy = self.buddies[_from.split('@')[0]]
- #self.backend.handleBuddyChanged(self.user, buddy.number.number,
- # buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE)
- self.backend.handleMessageAck(self.user, buddy.number, self.msgIDs[_id].xmppId)
- self.msgIDs[_id].cnt = self.msgIDs[_id].cnt +1
- if self.msgIDs[_id].cnt == 2:
- del self.msgIDs[_id]
-
+ number = _from.split('@')[0]
+ self.backend.handleMessageAck(self.user, number, self.msgIDs[_id].xmppId)
+ self.msgIDs[_id].cnt = self.msgIDs[_id].cnt + 1
+ if self.msgIDs[_id].cnt == 2:
+ del self.msgIDs[_id]
except KeyError:
- pass
+ self.logger.error("Message %s not found. Unable to send ack", _id)
# Called by superclass
def onAck(self, _id, _class, _from, timestamp):
@@ -299,29 +287,18 @@ class Session(YowsupApp):
self.sendReceipt(_id, _from, None, participant)
self.logger.info("Message received from %s to %s: %s (at ts=%s)",
buddy, self.legacyName, messageContent, timestamp)
- if participant is not None: # Group message
+ if participant is not None: # Group message or broadcast
partname = participant.split('@')[0]
- try:
- part = self.buddies[partname]
- if part.nick == "":
- part.nick = notify
- self.backend.handleParticipantChanged(
- self.user, partname, self._shortenGroupId(buddy),
- protocol_pb2.PARTICIPANT_FLAG_NONE,
- protocol_pb2.STATUS_ONLINE, "", part.nick
- ) # TODO
- except KeyError:
- self.updateBuddy(partname, notify, [])
- self.sendGroupMessageToXMPP(buddy, partname, messageContent,
- timestamp)
+ if _from.split('@')[1] == 'broadcast': # Broadcast message
+ message = self.broadcast_prefix + messageContent
+ self.sendMessageToXMPP(partname, message, timestamp)
+ else: # Group message
+ if notify is None:
+ notify = ""
+ self.sendGroupMessageToXMPP(buddy, partname, messageContent,
+ timestamp, notify)
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):
@@ -329,14 +306,19 @@ class Session(YowsupApp):
buddy = image._from.split('@')[0]
participant = image.participant
if image.caption is None:
- image.caption = ''
- message = image.url + ' ' + image.caption
+ image.caption = ''
if participant is not None: # Group message
- partname = participant.split('@')[0]
- self.sendGroupMessageToXMPP(buddy, partname, message, image.timestamp)
- else:
-
- self.sendMessageToXMPP(buddy, message, image.timestamp)
+ partname = participant.split('@')[0]
+ if image._from.split('@')[1] == 'broadcast': # Broadcast message
+ self.sendMessageToXMPP(partname, self.broadcast_prefix, image.timestamp)
+ self.sendMessageToXMPP(partname, image.url, image.timestamp)
+ self.sendMessageToXMPP(partname, image.caption, image.timestamp)
+ else: # Group message
+ self.sendGroupMessageToXMPP(buddy, partname, image.url, image.timestamp)
+ self.sendGroupMessageToXMPP(buddy, partname, image.caption, image.timestamp)
+ else:
+ self.sendMessageToXMPP(buddy, image.url, image.timestamp)
+ self.sendMessageToXMPP(buddy, image.caption, image.timestamp)
self.sendReceipt(image._id, image._from, None, image.participant)
# Called by superclass
@@ -346,10 +328,13 @@ class Session(YowsupApp):
participant = audio.participant
message = audio.url
if participant is not None: # Group message
- partname = participant.split('@')[0]
- self.sendGroupMessageToXMPP(buddy, partname, message, audio.timestamp)
- else:
-
+ 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)
@@ -361,10 +346,13 @@ class Session(YowsupApp):
message = video.url
if participant is not None: # Group message
- partname = participant.split('@')[0]
- self.sendGroupMessageToXMPP(buddy, partname, message, video.timestamp)
- else:
-
+ partname = participant.split('@')[0]
+ 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, message, video.timestamp)
+ else:
self.sendMessageToXMPP(buddy, message, video.timestamp)
self.sendReceipt(video._id, video._from, None, video.participant)
@@ -372,23 +360,30 @@ class Session(YowsupApp):
buddy = location._from.split('@')[0]
latitude = location.getLatitude()
longitude = location.getLongitude()
- url = location.getLocationUrl()
+ url = location.getLocationURL()
participant = location.participant
+ latlong = 'geo:' + latitude + ',' + longitude
self.logger.debug("Location received from %s: %s, %s",
buddy, latitude, longitude)
- if participant is not None: # Group message
- partname = participant.split('@')[0]
- self.sendGroupMessageToXMPP(buddy, partname, url, location.timestamp)
- self.sendGroupMessageToXMPP(buddy, partname, 'geo:' + latitude + ',' + longitude,
- location.timestamp)
- else:
- self.sendMessageToXMPP(buddy, url, location.timestamp)
- self.sendMessageToXMPP(buddy, 'geo:' + latitude + ',' + longitude,
- location.timestamp)
- self.sendReceipt(location._id, location._from, None, location.participant, location.timestamp)
+ if participant is not None: # Group message
+ partname = participant.split('@')[0]
+ if location._from.split('@')[1] == 'broadcast': # Broadcast message
+ self.sendMessageToXMPP(partname, self.broadcast_prefix, location.timestamp)
+ if url is not None:
+ self.sendMessageToXMPP(partname, url, location.timestamp)
+ self.sendMessageToXMPP(partname, latlong, location.timestamp)
+ else: # Group message
+ if url is not None:
+ self.sendGroupMessageToXMPP(buddy, partname, url, location.timestamp)
+ self.sendGroupMessageToXMPP(buddy, partname, latlong, location.timestamp)
+ else:
+ if url is not None:
+ self.sendMessageToXMPP(buddy, url, location.timestamp)
+ self.sendMessageToXMPP(buddy, latlong, location.timestamp)
+ self.sendReceipt(location._id, location._from, None, location.participant)
# Called by superclass
@@ -398,13 +393,17 @@ class Session(YowsupApp):
_id, _from, name, card_data, to, notify, timestamp, participant
]))
)
+ message = "Received VCard (not implemented yet)"
buddy = _from.split("@")[0]
if participant is not None: # Group message
- partname = participant.split('@')[0]
- self.sendGroupMessageToXMPP(buddy, partname, "Received VCard (not implemented yet)", timestamp)
- else:
-
- self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)")
+ partname = participant.split('@')[0]
+ if _from.split('@')[1] == 'broadcast': # Broadcast message
+ message = self.broadcast_prefix + message
+ self.sendMessageToXMPP(partname, message, timestamp)
+ else: # Group message
+ self.sendGroupMessageToXMPP(buddy, partname, message, timestamp)
+ else:
+ self.sendMessageToXMPP(buddy, message, timestamp)
# self.sendMessageToXMPP(buddy, card_data)
#self.transferFile(buddy, str(name), card_data)
self.sendReceipt(_id, _from, None, participant)
@@ -442,11 +441,8 @@ class Session(YowsupApp):
subjectOwner = group.getSubjectOwnerJid(full = False)
subject = utils.softToUni(group.getSubject())
- self.groups[room] = Group(room, owner, subject, subjectOwner)
- self.groups[room].participants = group.getParticipants().keys()
-# self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0])
-
- #self._addParticipantsToRoom(room, group.getParticipants())
+ self.groups[room] = Group(room, owner, subject, subjectOwner, self.backend, self.user)
+ self.groups[room].addParticipants(group.getParticipants, self.buddies, self.legacyName)
self.bot.send("You have been added to group: %s@%s (%s)"
% (self._shortenGroupId(room), subject, self.backend.spectrum_jid))
@@ -454,68 +450,110 @@ class Session(YowsupApp):
def onParticipantsAddedToGroup(self, group):
self.logger.debug("Participants added to group: %s", group)
room = group.getGroupId().split('@')[0]
- self.groups[room].participants.extend(group.getParticipants())
- self._refreshParticipants(room)
+ self.groups[room].addParticipants(group.getParticipants(), self.buddies, self.legacyName)
+ self.groups[room].sendParticipantsToSpectrum(self.legacyName)
+
+ # Called by superclass
+ def onSubjectChanged(self, room, subject, subjectOwner, timestamp):
+ self.logger.debug(
+ "onSubjectChange(rrom=%s, subject=%s, subjectOwner=%s, timestamp=%s)",
+ room, subject, subjectOwner, timestamp)
+ try:
+ group = self.groups[room]
+ except KeyError:
+ self.logger.error("Subject of non-existant group (%s) changed", group)
+ else:
+ group.subject = subject
+ group.subjectOwner = subjectOwner
+ if not group.joined:
+ # We have not joined group so we should not send subject
+ return
+ self.backend.handleSubject(self.user, room, subject, subjectOwner)
+ self.backend.handleRoomNicknameChanged(self.user, room, subject)
# Called by superclass
def onParticipantsRemovedFromGroup(self, room, participants):
self.logger.debug("Participants removed from group: %s, %s",
room, participants)
- group = self.groups[room]
- for jid in participants:
- group.participants.remove(jid)
- buddy = jid.split("@")[0]
- try:
- nick = self.buddies[buddy].nick
- except KeyError:
- nick = buddy
- if nick == "":
- nick = buddy
- if buddy == self.legacyName:
- nick = group.nick
- flags = protocol_pb2.PARTICIPANT_FLAG_NONE
- self.backend.handleParticipantChanged(
- self.user, nick, self._shortenGroupId(room), flags,
- protocol_pb2.STATUS_NONE, buddy)
+ self.groups[room].removeParticipants(participants)
+
+ # Called by superclass
+ def onContactStatusChanged(self, number, status):
+ self.logger.debug("%s changed their status to %s", number, status)
+ try:
+ buddy = self.buddies[number]
+ buddy.statusMsg = status
+ self.buddies.updateSpectrum(buddy)
+ except KeyError:
+ self.logger.debug("%s not in buddy list", number)
+
+ # Called by superclass
+ def onContactPictureChanged(self, number):
+ self.logger.debug("%s changed their profile picture", number)
+ self.buddies.requestVCard(number)
+
+ # Called by superclass
+ def onContactAdded(self, number, nick):
+ self.logger.debug("Adding new contact %s (%s)", nick, number)
+ self.updateBuddy(number, nick, [])
+
+ # Called by superclass
+ def onContactRemoved(self, number):
+ self.logger.debug("Removing contact %s", number)
+ self.removeBuddy(number)
+
+ def onContactUpdated(self, oldnumber, newnumber):
+ self.logger.debug("Contact has changed number from %s to %s",
+ oldnumber, newnumber)
+ if newnumber in self.buddies:
+ self.logger.warn("Contact %s exists, just updating", newnumber)
+ self.buddies.refresh(newnumber)
+ try:
+ buddy = self.buddies[oldnumber]
+ except KeyError:
+ self.logger.warn("Old contact (%s) not found. Adding new contact (%s)",
+ oldnumber, newnumber)
+ nick = ""
+ else:
+ self.removeBuddy(buddy.number)
+ nick = buddy.nick
+ self.updateBuddy(newnumber, nick, [])
def onPresenceReceived(self, _type, name, jid, lastseen):
self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen)
buddy = jid.split("@")[0]
- try:
- buddy = self.buddies[buddy]
+ try:
+ buddy = self.buddies[buddy]
except KeyError:
- self.logger.error("Buddy not found: %s", buddy)
+ # Sometimes whatsapp send our own presence
+ if buddy != self.legacyName:
+ self.logger.error("Buddy not found: %s", buddy)
return
if (lastseen == str(buddy.lastseen)) and (_type == buddy.presence):
return
- if ((lastseen != "deny") and (lastseen != None) and (lastseen != "none")):
+ if ((lastseen != "deny") and (lastseen != None) and (lastseen != "none")):
buddy.lastseen = int(lastseen)
if (_type == None):
buddy.lastseen = time.time()
buddy.presence = _type
- timestamp = time.localtime(buddy.lastseen)
- statusmsg = buddy.statusMsg + time.strftime("\n Last seen: %a, %d %b %Y %H:%M:%S", timestamp)
-
if _type == "unavailable":
- self.onPresenceUnavailable(buddy, statusmsg)
+ self.onPresenceUnavailable(buddy)
else:
- self.onPresenceAvailable(buddy, statusmsg)
+ self.onPresenceAvailable(buddy)
- def onPresenceAvailable(self, buddy, statusmsg):
+ def onPresenceAvailable(self, buddy):
self.logger.info("Is available: %s", buddy)
- self.backend.handleBuddyChanged(self.user, buddy.number,
- buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE, statusmsg, buddy.image_hash)
+ self.buddies.updateSpectrum(buddy)
- def onPresenceUnavailable(self, buddy, statusmsg):
+ def onPresenceUnavailable(self, buddy):
self.logger.info("Is unavailable: %s", buddy)
- self.backend.handleBuddyChanged(self.user, buddy.number,
- buddy.nick, buddy.groups, protocol_pb2.STATUS_AWAY, statusmsg, buddy.image_hash)
+ self.buddies.updateSpectrum(buddy)
# spectrum RequestMethods
def sendTypingStarted(self, buddy):
@@ -532,29 +570,65 @@ class Session(YowsupApp):
self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy)
self.sendTyping(buddy, False)
- def sendMessageToWA(self, sender, message, ID):
- self.logger.info("Message sent from %s to %s: %s",
- self.legacyName, sender, message)
+ def sendImage(self, message, ID, to):
+ if (".jpg" in message.lower()):
+ imgType = "jpg"
+ if (".webp" in message.lower()):
+ imgType = "webp"
+
+ success = deferred.Deferred()
+ error = deferred.Deferred()
+ self.downloadMedia(message, success.run, error.run)
+
+ # Success
+ path = success.arg(0)
+ call(self.logger.info, "Success: Image downloaded to %s", path)
+ pathWithExt = path.then(lambda p: p + "." + imgType)
+ call(os.rename, path, pathWithExt)
+ pathJpg = path.then(lambda p: p + ".jpg")
+ if imgType != "jpg":
+ im = call(Image.open, pathWithExt)
+ call(im.save, pathJpg)
+ call(os.remove, pathWithExt)
+ call(self.logger.info, "Sending image to %s", to)
+ waId = deferred.Deferred()
+ call(super(Session, self).sendImage, to, pathJpg, onSuccess = waId.run)
+ call(self.setWaId, ID, waId)
+ waId.when(call, os.remove, pathJpg)
+ waId.when(self.logger.info, "Image sent")
+
+ # Error
+ error.when(self.logger.info, "Download Error. Sending message as is.")
+ waId = error.when(self.sendTextMessage, to, message)
+ call(self.setWaId, ID, waId)
+
+ def setWaId(self, XmppId, waId):
+ self.msgIDs[waId] = MsgIDs(XmppId, waId)
+
+ def sendMessageToWA(self, sender, message, ID, xhtml=""):
+ self.logger.info("Message sent from %s to %s: %s (xhtml=%s)",
+ self.legacyName, sender, message, xhtml)
message = message.encode("utf-8")
- # FIXME: Fragile, should pass this in to onDlerror
- self.dlerror_message = message
- self.dlerror_sender = sender
- self.dlerror_ID = ID
- # End Fragile
if sender == "bot":
self.bot.parse(message)
elif "-" in sender: # group msg
if "/" in sender: # directed at single user
room, nick = sender.split("/")
- for buddy, buddy3 in self.buddies.iteritems():
- self.logger.info("Group buddy=%s nick=%s", buddy,
- buddy3.nick)
- if buddy3.nick == nick:
- nick = buddy
- waId = self.sendTextMessage(nick + '@s.whatsapp.net', message)
- self.msgIDs[waId] = MsgIDs( ID, waId)
+ group = self.groups[room]
+ number = None
+ for othernumber, othernick in group.participants.iteritems():
+ if othernick == nick:
+ number = othernumber
+ break
+ if number is not None:
+ self.logger.debug("Private message sent from %s to %s", self.legacyName, number)
+ waId = self.sendTextMessage(number + '@s.whatsapp.net', message)
+ self.msgIDs[waId] = MsgIDs( ID, waId)
+ else:
+ self.logger.error("Attempted to send private message to non-existent user")
+ self.logger.debug("%s to %s in %s", self.legacyName, nick, room)
else:
room = sender
if message[0] == '\\' and message[:1] != '\\\\':
@@ -566,30 +640,17 @@ class Session(YowsupApp):
self.logger.debug("Group Message from %s to %s Groups: %s",
group.nick , group , self.groups)
self.backend.handleMessage(
- self.user, room, message.decode('utf-8'), group.nick
+ self.user, room, message.decode('utf-8'), group.nick, xhtml=xhtml
)
except KeyError:
self.logger.error('Group not found: %s', room)
-
+
if (".jpg" in message.lower()) or (".webp" in message.lower()):
- if (".jpg" in message.lower()):
- self.imgType = "jpg"
- if (".webp" in message.lower()):
- self.imgType = "webp"
- self.imgMsgId = ID
- self.imgBuddy = room + "@g.us"
-
-
- downloader = MediaDownloader(self.onDlsuccess, self.onDlerror)
- downloader.download(message)
- #self.imgMsgId = ID
- #self.imgBuddy = room + "@g.us"
- elif "geo:" in message.lower():
- self._sendLocation(room + "@g.us", message, ID)
-
- else:
-
- self.sendTextMessage(self._lengthenGroupId(room) + '@g.us', message)
+ self.sendImage(message, ID, room + '@g.us')
+ elif "geo:" in message.lower():
+ self._sendLocation(room + "@g.us", message, ID)
+ else:
+ self.sendTextMessage(room + '@g.us', message)
else: # private msg
buddy = sender
# if message == "\\lastseen":
@@ -605,20 +666,8 @@ class Session(YowsupApp):
#self.call("contact_getProfilePicture", (buddy + "@s.whatsapp.net",))
self.requestVCard(buddy)
else:
- if (".jpg" in message.lower()) or (".webp" in message.lower()):
- #waId = self.call("message_imageSend", (buddy + "@s.whatsapp.net", message, None, 0, None))
- #waId = self.call("message_send", (buddy + "@s.whatsapp.net", message))
- if (".jpg" in message.lower()):
- self.imgType = "jpg"
- if (".webp" in message.lower()):
- self.imgType = "webp"
- self.imgMsgId = ID
- self.imgBuddy = buddy + "@s.whatsapp.net"
-
- downloader = MediaDownloader(self.onDlsuccess, self.onDlerror)
- downloader.download(message)
- #self.imgMsgId = ID
- #self.imgBuddy = buddy + "@s.whatsapp.net"
+ if (".jpg" in message.lower()) or (".webp" in message.lower()):
+ self.sendImage(message, ID, buddy + "@s.whatsapp.net")
elif "geo:" in message.lower():
self._sendLocation(buddy + "@s.whatsapp.net", message, ID)
else:
@@ -635,20 +684,7 @@ class Session(YowsupApp):
self.leaveGroup(room)
# Delete Room on spectrum side
group = self.groups[room]
- for jid in group.participants:
- buddy = jid.split("@")[0]
- try:
- nick = self.buddies[buddy].nick
- except KeyError:
- nick = buddy
- if nick == "":
- nick = buddy
- if buddy == self.legacyName:
- nick = group.nick
- flags = protocol_pb2.PARTICIPANT_FLAG_ROOM_NOT_FOUND
- self.backend.handleParticipantChanged(
- self.user, nick, self._shortenGroupId(room), flags,
- protocol_pb2.STATUS_NONE, buddy)
+ group.leaveRoom()
del self.groups[room]
def _requestLastSeen(self, buddy):
@@ -686,35 +722,36 @@ class Session(YowsupApp):
self.backend.handleMessage(self.user, buddy, messageContent, "",
"", timestamp)
- def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""):
- # self._refreshParticipants(room)
- try:
- nick = self.buddies[buddy].nick
- except KeyError:
- nick = buddy
- if nick == "":
- nick = buddy
-
+ def sendGroupMessageToXMPP(self, room, number, messageContent, timestamp = u"", defaultname = u""):
if timestamp:
timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp))
if self.initialized == False:
self.logger.debug("Group message queued from %s to %s: %s",
- buddy, room, messageContent)
+ number, room, messageContent)
if room not in self.groupOfflineQueue:
self.groupOfflineQueue[room] = [ ]
self.groupOfflineQueue[room].append(
- (buddy, messageContent, timestamp)
+ (number, messageContent, timestamp)
)
else:
- self.logger.debug("Group message sent from %s (%s) to %s: %s",
- buddy, nick, room, messageContent)
+ self.logger.debug("Group message sent from %s to %s: %s",
+ number, room, messageContent)
try:
group = self.groups[room]
+ # Update nickname
+ try:
+ if defaultname != "" and group.participants[number] == number:
+ group.changeNick(number, defaultname)
+ if self.buddies[number].nick != "":
+ group.changeNick(number, self.buddies[number].nick)
+ except KeyError:
+ pass
+ nick = group.participants[number]
if group.joined:
- self.backend.handleMessage(self.user,room, messageContent,
+ self.backend.handleMessage(self.user, room, messageContent,
nick, "", timestamp)
else:
self.bot.send("You have received a message in group: %s@%s"
@@ -724,7 +761,7 @@ class Session(YowsupApp):
except KeyError:
self.logger.warn("Group is not in group list")
self.backend.handleMessage(self.user, self._shortenGroupId(room),
- messageContent, nick, "", timestamp)
+ messageContent, number, "", timestamp)
def changeStatus(self, status):
@@ -770,61 +807,8 @@ class Session(YowsupApp):
self.buddies.remove(buddy)
def requestVCard(self, buddy, ID=None):
- def onSuccess(response, request):
- 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)
- if ID != None:
- self.backend.handleVCard(self.user, ID, buddy, "", "", response.pictureData)
- obuddy = self.buddies[buddy]
- self.updateBuddy(buddy, obuddy.nick, obuddy.groups, image_hash)
+ self.buddies.requestVCard(buddy, ID)
- self.logger.debug('Requesting profile picture of %s', buddy)
- self.requestProfilePicture(buddy, onSuccess = onSuccess)
-
- def onDlsuccess(self, path):
- self.logger.info("Success: Image downloaded to %s", path)
- os.rename(path, path+"."+self.imgType)
- if self.imgType != "jpg":
- im = Image.open(path+"."+self.imgType)
- im.save(path+".jpg")
- self.imgPath = path+".jpg"
- statinfo = os.stat(self.imgPath)
- name=os.path.basename(self.imgPath)
- self.logger.info("Buddy %s",self.imgBuddy)
- self.image_send(self.imgBuddy, self.imgPath)
-
- #self.logger.info("Sending picture %s of size %s with name %s",self.imgPath, statinfo.st_size, name)
- #mtype = "image"
-
- #sha1 = hashlib.sha256()
- #fp = open(self.imgPath, 'rb')
- #try:
- # sha1.update(fp.read())
- # hsh = base64.b64encode(sha1.digest())
- # self.call("media_requestUpload", (hsh, mtype, os.path.getsize(self.imgPath)))
- #finally:
- # fp.close()
-
-
- def onDlerror(self):
- self.logger.info("Download Error. Sending message as is.")
- waId = self.sendTextMessage(self.dlerror_sender + '@s.whatsapp.net', self.dlerror_message)
- self.msgIDs[waId] = MsgIDs(self.dlerror_ID, waId)
-
-
- def _doSendImage(self, filePath, url, to, ip = None, caption = None):
- waId = self.doSendImage(filePath, url, to, ip, caption)
- self.msgIDs[waId] = MsgIDs(self.imgMsgId, waId)
-
- def _doSendAudio(self, filePath, url, to, ip = None, caption = None):
- waId = self.doSendAudio(filePath, url, to, ip, caption)
- self.msgIDs[waId] = MsgIDs(self.imgMsgId, waId)
-
-
-
-
def createThumb(self, size=100, raw=False):
img = Image.open(self.imgPath)
width, height = img.size
diff --git a/threadutils.py b/threadutils.py
new file mode 100644
index 0000000..59e7d51
--- /dev/null
+++ b/threadutils.py
@@ -0,0 +1,19 @@
+import Queue
+import threading
+
+# This queue is for other threads that want to execute code in the main thread
+eventQueue = Queue.Queue()
+
+def runInThread(threadFunc, callback):
+ """
+ Executes threadFunc in a new thread. The result of threadFunc will be
+ pass as the first argument to callback. callback will be called in the main
+ thread.
+ """
+ def helper():
+ # Execute threadfunc in new thread
+ result = threadFunc()
+ # Queue callback to be call in main thread
+ eventQueue.put(lambda: callback(result))
+ thread = threading.Thread(target=helper)
+ thread.start()
diff --git a/transwhat.py b/transwhat.py
index aa58ad9..07e36bf 100755
--- a/transwhat.py
+++ b/transwhat.py
@@ -29,8 +29,8 @@ import logging
import asyncore
import sys, os
import e4u
-import threading
import Queue
+import threadutils
sys.path.insert(0, os.getcwd())
@@ -62,7 +62,13 @@ logging.basicConfig( \
# Handler
def handleTransportData(data):
- plugin.handleDataRead(data)
+ try:
+ plugin.handleDataRead(data)
+ except SystemExit as e:
+ raise e
+ except:
+ logger = logging.getLogger('transwhat')
+ logger.error(traceback.format_exc())
e4u.load()
@@ -76,7 +82,13 @@ io = IOChannel(args.host, args.port, handleTransportData, connectionClosed)
plugin = WhatsAppBackend(io, args.j)
-plugin.handleBackendConfig('features', 'send_buddies_on_login', 1)
+plugin.handleBackendConfig({
+ 'features': [
+ ('send_buddies_on_login', 1),
+ ('muc', 'true'),
+ ],
+})
+
while True:
try:
@@ -90,6 +102,13 @@ while True:
break
if closed:
break
+ while True:
+ try:
+ callback = threadutils.eventQueue.get_nowait()
+ except Queue.Empty:
+ break
+ else:
+ callback()
except SystemExit:
break
except:
diff --git a/whatsappbackend.py b/whatsappbackend.py
index a2ddc9e..691dc73 100644
--- a/whatsappbackend.py
+++ b/whatsappbackend.py
@@ -25,6 +25,7 @@ from Spectrum2.backend import SpectrumBackend
from Spectrum2 import protocol_pb2
from session import Session
+from registersession import RegisterSession
import logging
@@ -36,18 +37,20 @@ class WhatsAppBackend(SpectrumBackend):
self.sessions = { }
self.spectrum_jid = spectrum_jid
# Used to prevent duplicate messages
- self.lastMessage = {}
+ self.lastMsgId = {}
self.logger.debug("Backend started")
# RequestsHandlers
def handleLoginRequest(self, user, legacyName, password, extra):
self.logger.debug("handleLoginRequest(user=%s, legacyName=%s)", user, legacyName)
- if user not in self.sessions:
- self.sessions[user] = Session(self, user, legacyName, extra)
-
- if user not in self.lastMessage:
- self.lastMessage[user] = {}
+ # Key word means we should register a new password
+ if password == 'register':
+ if user not in self.sessions:
+ self.sessions[user] = RegisterSession(self, user, legacyName, extra)
+ else:
+ if user not in self.sessions:
+ self.sessions[user] = Session(self, user, legacyName, extra)
self.sessions[user].login(password)
@@ -57,20 +60,17 @@ class WhatsAppBackend(SpectrumBackend):
self.sessions[user].logout()
del self.sessions[user]
- def handleMessageSendRequest(self, user, buddy, message, xhtml = "", ID = 0):
- self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s, xhtml = %s)", user, buddy, message, xhtml)
+ def handleMessageSendRequest(self, user, buddy, message, xhtml="", ID=""):
+ self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s, xhtml=%s, ID=%s)", user, buddy, message, xhtml, ID)
# 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.
- # IDEA there is an ID field in ConvMessage. If it is extracted it will work
- usersMessage = self.lastMessage[user]
- if buddy not in usersMessage or usersMessage[buddy] != message:
- self.sessions[user].sendMessageToWA(buddy, message, ID)
- usersMessage[buddy] = message
+ # a buddy, one to the bare jid and one to the /bot resource. This
+ # causes duplicate messages. Thus we should not send consecutive
+ # messages with the same id
+ if ID == '':
+ self.sessions[user].sendMessageToWA(buddy, message, ID, xhtml)
+ elif user not in self.lastMsgId or self.lastMsgId[user] != ID:
+ self.sessions[user].sendMessageToWA(buddy, message, ID, xhtml)
+ self.lastMsgId[user] = ID
def handleJoinRoomRequest(self, user, room, nickname, pasword):
self.logger.debug("handleJoinRoomRequest(user=%s, room=%s, nickname=%s)", user, room, nickname)
@@ -117,14 +117,29 @@ class WhatsAppBackend(SpectrumBackend):
self.logger.debug("handleVCardRequest(user=%s, buddy=%s, ID=%s)", user, buddy, ID)
self.sessions[user].requestVCard(buddy, ID)
+ def handleVCardUpdatedRequest(self, user, photo, nickname):
+ self.logger.debug("handleVCardUpdatedRequest(user=%s, nickname=%s)", user, nickname)
+ self.sessions[user].setProfilePicture(photo)
+
+ def handleBuddyBlockToggled(self, user, buddy, blocked):
+ self.logger.debug("handleBuddyBlockedToggled(user=%s, buddy=%s, blocked=%s)", user, buddy, blocked)
+
+ def relogin(self, user, legacyName, password, extra):
+ """
+ Used to re-initialize the session object. Used when finished with
+ registration session and the user needs to login properly
+ """
+ self.logger.debug("relogin(user=%s, legacyName=%s)", user, legacyName)
+ # Change password in spectrum database
+ self.handleQuery('register %s %s %s' % (user, legacyName, password))
+ # Key word means we should register a new password
+ if password == 'register': # This shouldn't happen, but just in case
+ self.sessions[user] = RegisterSession(self, user, legacyName, extra)
+ else:
+ self.sessions[user] = Session(self, user, legacyName, extra)
+ self.sessions[user].login(password)
# TODO
- def handleBuddyBlockToggled(self, user, buddy, blocked):
- pass
-
- def handleVCardUpdatedRequest(self, user, photo, nickname):
- pass
-
def handleAttentionRequest(self, user, buddy, message):
pass
diff --git a/yowsupwrapper.py b/yowsupwrapper.py
index d7c3a79..f5bece2 100644
--- a/yowsupwrapper.py
+++ b/yowsupwrapper.py
@@ -35,11 +35,21 @@ from yowsup.layers.protocol_chatstate.protocolentities import *
from yowsup.layers.protocol_contacts.protocolentities import *
from yowsup.layers.protocol_groups.protocolentities import *
from yowsup.layers.protocol_media.protocolentities import *
+from yowsup.layers.protocol_notifications.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_privacy.protocolentities import *
from yowsup.layers.protocol_receipts.protocolentities import *
+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
from functools import partial
@@ -127,6 +137,10 @@ class YowsupApp(object):
receipt = OutgoingReceiptProtocolEntity(_id, _from, read, participant)
self.sendEntity(receipt)
+ def downloadMedia(self, url, onSuccess = None, onFailure = None):
+ downloader = MediaDownloader(onSuccess, onFailure)
+ downloader.download(url)
+
def sendTextMessage(self, to, message):
"""
Sends a text message
@@ -144,26 +158,27 @@ class YowsupApp(object):
self.sendEntity(messageEntity)
return messageEntity.getId()
- def image_send(self, jid, path, caption = None):
- entity = RequestUploadIqProtocolEntity(RequestUploadIqProtocolEntity.MEDIA_TYPE_IMAGE, filePath=path)
- successFn = lambda successEntity, originalEntity: self.onRequestUploadResult(jid, path, successEntity, originalEntity, caption)
+ def sendImage(self, jid, path, caption = None, onSuccess = None, onFailure = None):
+ entity = RequestUploadIqProtocolEntity(RequestUploadIqProtocolEntity.MEDIA_TYPE_IMAGE, filePath=path)
+ successFn = lambda successEntity, originalEntity: self.onRequestUploadResult(jid, path, successEntity, originalEntity, caption, onSuccess, onFailure)
errorFn = lambda errorEntity, originalEntity: self.onRequestUploadError(jid, path, errorEntity, originalEntity)
self.sendIq(entity, successFn, errorFn)
- def onRequestUploadResult(self, jid, filePath, resultRequestUploadIqProtocolEntity, requestUploadIqProtocolEntity, caption = None):
+ def onRequestUploadResult(self, jid, filePath, resultRequestUploadIqProtocolEntity, requestUploadIqProtocolEntity, caption = None, onSuccess=None, onFailure=None):
if requestUploadIqProtocolEntity.mediaType == RequestUploadIqProtocolEntity.MEDIA_TYPE_AUDIO:
- doSendFn = self._doSendAudio
+ doSendFn = self.doSendAudio
else:
- doSendFn = self._doSendImage
+ doSendFn = self.doSendImage
if resultRequestUploadIqProtocolEntity.isDuplicate():
doSendFn(filePath, resultRequestUploadIqProtocolEntity.getUrl(), jid,
resultRequestUploadIqProtocolEntity.getIp(), caption)
else:
- successFn = lambda filePath, jid, url: doSendFn(filePath, url, jid, resultRequestUploadIqProtocolEntity.getIp(), caption)
- mediaUploader = MediaUploader(jid, self.legacyName, filePath,
+ successFn = lambda filePath, jid, url: doSendFn(filePath, url, jid, resultRequestUploadIqProtocolEntity.getIp(), caption, onSuccess, onFailure)
+ ownNumber = self.stack.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(full=False)
+ mediaUploader = MediaUploader(jid, ownNumber, filePath,
resultRequestUploadIqProtocolEntity.getUrl(),
resultRequestUploadIqProtocolEntity.getResumeOffset(),
successFn, self.onUploadError, self.onUploadProgress, async=False)
@@ -181,17 +196,21 @@ class YowsupApp(object):
#sys.stdout.flush()
pass
- def doSendImage(self, filePath, url, to, ip = None, caption = None):
+ def doSendImage(self, filePath, url, to, ip = None, caption = None, onSuccess = None, onFailure = None):
entity = ImageDownloadableMediaMessageProtocolEntity.fromFilePath(filePath, url, ip, to, caption = caption)
self.sendEntity(entity)
- #self.msgIDs[entity.getId()] = MsgIDs(self.imgMsgId, entity.getId())
+ #self.msgIDs[entity.getId()] = MsgIDs(self.imgMsgId, entity.getId())
+ if onSuccess is not None:
+ onSuccess(entity.getId())
return entity.getId()
- def doSendAudio(self, filePath, url, to, ip = None, caption = None):
+ def doSendAudio(self, filePath, url, to, ip = None, caption = None, onSuccess = None, onFailure = None):
entity = AudioDownloadableMediaMessageProtocolEntity.fromFilePath(filePath, url, ip, to)
self.sendEntity(entity)
#self.msgIDs[entity.getId()] = MsgIDs(self.imgMsgId, entity.getId())
+ if onSuccess is not None:
+ onSuccess(entity.getId())
return entity.getId()
@@ -252,7 +271,20 @@ class YowsupApp(object):
"""
iq = SetStatusIqProtocolEntity(statusText)
self.sendIq(iq)
-
+
+ def setProfilePicture(self, previewPicture, fullPicture = None):
+ """
+ Requests profile picture of whatsapp user
+ Args:
+ - previewPicture: (bytes) The preview picture
+ - fullPicture: (bytes) The full profile picture
+ """
+ if fullPicture == None:
+ fullPicture = previewPicture
+ ownJid = self.stack.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(full = True)
+ iq = SetPictureIqProtocolEntity(ownJid, previewPicture, fullPicture)
+ self.sendIq(iq)
+
def sendTyping(self, phoneNumber, typing):
"""
Notify buddy using phoneNumber that you are typing to him
@@ -271,12 +303,12 @@ class YowsupApp(object):
ChatstateProtocolEntity.STATE_PAUSED, jid
)
self.sendEntity(state)
-
- def sendSync(self, contacts, delta = False, interactive = True):
+
+ def sendSync(self, contacts, delta = False, interactive = True, success = None, failure = None):
"""
You need to sync new contacts before you interact with
them, failure to do so could result in a temporary ban.
-
+
Args:
- contacts: ([str]) a list of phone numbers of the
contacts you wish to sync
@@ -285,12 +317,62 @@ class YowsupApp(object):
contact list.
- interactive: (bool; default: True) Set to false if you are
sure this is the first time registering
+ - success: (func) - Callback; Takes three arguments: existing numbers,
+ non-existing numbers, invalid numbers.
"""
- # TODO: Implement callbacks
mode = GetSyncIqProtocolEntity.MODE_DELTA if delta else GetSyncIqProtocolEntity.MODE_FULL
context = GetSyncIqProtocolEntity.CONTEXT_INTERACTIVE if interactive else GetSyncIqProtocolEntity.CONTEXT_REGISTRATION
+ # International contacts must be preceded by a plus. Other numbers are
+ # considered local.
+ contacts = ['+' + c for c in contacts]
iq = GetSyncIqProtocolEntity(contacts, mode, context)
- self.sendIq(iq)
+ def onSuccess(response, request):
+ # Remove leading plus
+ if success is not None:
+ existing = [s[1:] for s in response.inNumbers.keys()]
+ nonexisting = [s[1:] for s in response.outNumbers.keys()]
+ invalid = [s[1:] for s in response.invalidNumbers]
+ success(existing, nonexisting, invalid)
+
+ self.sendIq(iq, onSuccess = onSuccess, onError = failure)
+
+ def requestClientConfig(self, success = None, failure = None):
+ """I'm not sure what this does, but it might be required on first login."""
+ iq = PushIqProtocolEntity()
+ self.sendIq(iq, onSuccess = success, onError = failure)
+
+
+ def requestPrivacyList(self, success = None, failure = None):
+ """I'm not sure what this does, but it might be required on first login."""
+ iq = PrivacyListIqProtocolEntity()
+ self.sendIq(iq, onSuccess = success, onError = failure)
+
+ def requestServerProperties(self, success = None, failure = None):
+ """I'm not sure what this does, but it might be required on first login."""
+ iq = PropsIqProtocolEntity()
+ self.sendIq(iq, onSuccess = success, onError = failure)
+
+ def requestStatuses(self, contacts, success = None, failure = None):
+ """
+ Request the statuses of a number of users.
+
+ Args:
+ - contacts: ([str]) the phone numbers of users whose statuses you
+ wish to request
+ - success: (func) called when request is successful
+ - failure: (func) called when request has failed
+ """
+ iq = GetStatusesIqProtocolEntity([c + '@s.whatsapp.net' for c in contacts])
+ def onSuccess(response, request):
+ if success is not None:
+ self.logger.debug("Received Statuses %s", response)
+ s = {}
+ for k, v in response.statuses.iteritems():
+ s[k.split('@')[0]] = v
+ success(s)
+
+ self.sendIq(iq, onSuccess = onSuccess, onError = failure)
+
def requestLastSeen(self, phoneNumber, success = None, failure = None):
"""
@@ -336,6 +418,34 @@ class YowsupApp(object):
iq = InfoGroupsIqProtocolEntity(group + '@g.us')
self.sendIq(iq, onSuccess = onSuccess, onError = onFailure)
+ def requestSMSCode(self, countryCode, phoneNumber):
+ """
+ Request an sms regitration code. WARNING: this function is blocking
+
+ Args:
+ countryCode: The country code of the phone you wish to register
+ phoneNumber: phoneNumber of the phone you wish to register without
+ the country code.
+ """
+ request = WACodeRequest(countryCode, phoneNumber)
+ return request.send()
+
+ def requestPassword(self, countryCode, phoneNumber, smsCode):
+ """
+ Request a password. WARNING: this function is blocking
+
+ Args:
+ countryCode: The country code of the phone you wish to register
+ phoneNumber: phoneNumber of the phone you wish to register without
+ the country code.
+ smsCode: The sms code that you asked for previously
+ """
+ smsCode = smsCode.replace('-', '')
+ request = WARegRequest(countryCode, phoneNumber, smsCode)
+ return request.send()
+
+
+
def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t):
"""
Called when login is successful.
@@ -504,13 +614,66 @@ class YowsupApp(object):
def onParticipantsRemovedFromGroup(self, group, participants):
"""Called when participants have been removed from a group
-
+
Args:
- group: (str) id of the group (e.g. 27831788123-144024456)
- participants: (list) jids of participants that are removed
"""
pass
+ def onSubjectChanged(self, group, subject, subjectOwner, timestamp):
+ """Called when someone changes the grousp subject
+
+ Args:
+ - group: (str) id of the group (e.g. 27831788123-144024456)
+ - subject: (str) the new subject
+ - subjectOwner: (str) the number of the person who changed the subject
+ - timestamp: (str) time the subject was changed
+ """
+ pass
+
+ def onContactStatusChanged(self, number, status):
+ """Called when a contacts changes their status
+
+ Args:
+ number: (str) the number of the contact who changed their status
+ status: (str) the new status
+ """
+ pass
+
+ def onContactPictureChanged(self, number):
+ """Called when a contact changes their profile picture
+ Args
+ number: (str) the number of the contact who changed their picture
+ """
+ pass
+
+ def onContactRemoved(self, number):
+ """Called when a contact has been removed
+
+ Args:
+ number: (str) the number of the contact who has been removed
+ """
+ pass
+
+ def onContactAdded(self, number, nick):
+ """Called when a contact has been added
+
+ Args:
+ number: (str) contacts number
+ nick: (str) contacts nickname
+ """
+ pass
+
+ def onContactUpdated(self, oldNumber, newNumber):
+ """Called when a contact has changed their number
+
+ Args:
+ oldNumber: (str) the number the contact previously used
+ newNumber: (str) the new number of the contact
+ """
+ pass
+
def sendEntity(self, entity):
"""Sends an entity down the stack (as if YowsupAppLayer called toLower)"""
self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT,
@@ -617,6 +780,34 @@ class YowsupAppLayer(YowInterfaceLayer):
entity.getGroupId().split('@')[0],
entity.getParticipants().keys()
)
+ elif isinstance(entity, SubjectGroupsNotificationProtocolEntity):
+ self.caller.onSubjectChanged(
+ entity.getGroupId().split('@')[0],
+ entity.getSubject(),
+ entity.getSubjectOwner(full=False),
+ entity.getSubjectTimestamp()
+ )
+ elif isinstance(entity, StatusNotificationProtocolEntity):
+ self.caller.onContactStatusChanged(
+ entity._from.split('@')[0],
+ entity.status
+ )
+ elif isinstance(entity, SetPictureNotificationProtocolEntity):
+ self.caller.onContactPictureChanged(entity.setJid.split('@')[0])
+ elif isinstance(entity, DeletePictureNotificationProtocolEntity):
+ self.caller.onContactPictureChanged(entity.deleteJid.split('@')[0])
+ elif isinstance(entity, RemoveContactNotificationProtocolEntity):
+ self.caller.onContactRemoved(entity.contactJid.split('@')[0])
+ elif isinstance(entity, AddContactNotificationProtocolEntity):
+ self.caller.onContactAdded(
+ entity.contactJid.split('@')[0],
+ entity.notify
+ )
+ elif isinstance(entity, UpdateContactNotificationProtocolEntity):
+ self.caller.onContactUpdated(
+ entity._from.split('@')[0],
+ entity.contactJid.split('@')[0],
+ )
@ProtocolEntityCallback('message')
def onMessageReceived(self, entity):