Merge pull request #4 from stv0g/develop

Develop
This commit is contained in:
dazzzl 2016-02-11 00:41:01 +01:00
commit bb49001ab2
14 changed files with 1027 additions and 414 deletions

View file

@ -63,6 +63,9 @@ Create a new file `/etc/spectrum2/transports/whatsapp.cfg` with the following co
[logging] [logging]
config = /etc/spectrum2/logging.cfg config = /etc/spectrum2/logging.cfg
backend_config = /etc/spectrum2/backend-logging.cfg backend_config = /etc/spectrum2/backend-logging.cfg
[database]
type = sqlite3
## transWhat ## transWhat
@ -74,7 +77,7 @@ Checkout the latest version of transWhat from GitHub:
Install required dependencies: 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 - **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. - [**yowsup**](https://github.com/tgalal/yowsup): is a python library that enables you build application which use WhatsApp service.

View file

@ -222,13 +222,29 @@ class SpectrumBackend:
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA); message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA);
self.send(message) 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 = 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); message = WRAP(c.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_BACKEND_CONFIG);
self.send(message) 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): def handleLoginPayload(self, data):
payload = protocol_pb2.Login() payload = protocol_pb2.Login()
if (payload.ParseFromString(data) == False): if (payload.ParseFromString(data) == False):
@ -252,7 +268,6 @@ class SpectrumBackend:
def handleConvMessagePayload(self, data): def handleConvMessagePayload(self, data):
payload = protocol_pb2.ConversationMessage() payload = protocol_pb2.ConversationMessage()
self.logger.error("handleConvMessagePayload")
if (payload.ParseFromString(data) == False): if (payload.ParseFromString(data) == False):
#TODO: ERROR #TODO: ERROR
return return

View file

@ -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 | | `\help` | show this message |
| `\prune` | clear your buddylist | | `\prune` | clear your buddylist |
| `\sync` | sync your imported contacts with WhatsApp |
| `\lastseen` | request last online timestamp from buddy | | `\lastseen` | request last online timestamp from buddy |
| `\leave` | permanently leave group chat | | `\leave` | permanently leave group chat |
| `\groups` | print all attended groups | | `\groups` | print all attended groups |

18
bot.py
View file

@ -37,13 +37,12 @@ class Bot():
self.commands = { self.commands = {
"help": self._help, "help": self._help,
"prune": self._prune, "prune": self._prune,
"sync": self._sync,
"groups": self._groups, "groups": self._groups,
"getgroups": self._getgroups "getgroups": self._getgroups
} }
def parse(self, message): def parse(self, message):
args = message.split(" ") args = message.strip().split(" ")
cmd = args.pop(0) cmd = args.pop(0)
if cmd[0] == '\\': if cmd[0] == '\\':
@ -57,7 +56,7 @@ class Bot():
self.send("a valid command starts with a backslash") self.send("a valid command starts with a backslash")
def call(self, cmd, args = []): def call(self, cmd, args = []):
func = self.commands[cmd] func = self.commands[cmd.lower()]
spec = inspect.getargspec(func) spec = inspect.getargspec(func)
maxs = len(spec.args) - 1 maxs = len(spec.args) - 1
reqs = maxs - len(spec.defaults or []) reqs = maxs - len(spec.defaults or [])
@ -71,23 +70,10 @@ class Bot():
self.session.backend.handleMessage(self.session.user, self.name, message) self.session.backend.handleMessage(self.session.user, self.name, message)
# commands # 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): def _help(self):
self.send("""following bot commands are available: self.send("""following bot commands are available:
\\help show this message \\help show this message
\\prune clear your buddylist \\prune clear your buddylist
\\sync sync your imported contacts with WhatsApp
following user commands are available: following user commands are available:
\\lastseen request last online timestamp from buddy \\lastseen request last online timestamp from buddy

139
buddy.py
View file

@ -24,6 +24,12 @@ __email__ = "post@steffenvogel.de"
from Spectrum2 import protocol_pb2 from Spectrum2 import protocol_pb2
import logging import logging
import time
import utils
import base64
import deferred
from deferred import call
class Buddy(): class Buddy():
@ -33,11 +39,10 @@ class Buddy():
self.number = number self.number = number
self.groups = groups self.groups = groups
self.image_hash = image_hash if image_hash is not None else "" self.image_hash = image_hash if image_hash is not None else ""
self.statusMsg = "" self.statusMsg = u""
self.lastseen = 0 self.lastseen = 0
self.presence = 0 self.presence = 0
def update(self, nick, groups, image_hash): def update(self, nick, groups, image_hash):
self.nick = nick self.nick = nick
self.groups = groups self.groups = groups
@ -55,13 +60,12 @@ class BuddyList(dict):
self.session = session self.session = session
self.user = user self.user = user
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
self.synced = False
def _load(self, buddies): def _load(self, buddies):
for buddy in buddies: for buddy in buddies:
number = buddy.buddyName number = buddy.buddyName
nick = buddy.alias nick = buddy.alias
statusMsg = buddy.statusMessage statusMsg = buddy.statusMessage.decode('utf-8')
groups = [g for g in buddy.group] groups = [g for g in buddy.group]
image_hash = buddy.iconHash image_hash = buddy.iconHash
self[number] = Buddy(self.owner, number, nick, statusMsg, self[number] = Buddy(self.owner, number, nick, statusMsg,
@ -69,35 +73,45 @@ class BuddyList(dict):
self.logger.debug("Update roster") self.logger.debug("Update roster")
# old = self.buddies.keys()
# self.buddies.load()
# new = self.buddies.keys()
# contacts = new
contacts = self.keys() contacts = self.keys()
contacts.remove('bot')
if self.synced == False: self.session.sendSync(contacts, delta=False, interactive=True,
self.session.sendSync(contacts, delta = False, interactive = True) success=self.onSync)
self.synced = True
# 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))) 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: for number in contacts:
buddy = self[number] buddy = self[number]
if number != 'bot': self.updateSpectrum(buddy)
self.backend.handleBuddyChanged(self.user, number, buddy.nick,
buddy.groups, protocol_pb2.STATUS_NONE, def onSync(self, existing, nonexisting, invalid):
iconHash = buddy.image_hash if buddy.image_hash is not None else "") """We should only presence subscribe to existing numbers"""
self.session.subscribePresence(number)
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): def load(self, buddies):
@ -111,23 +125,38 @@ class BuddyList(dict):
buddy = self[number] buddy = self[number]
buddy.update(nick, groups, image_hash) buddy.update(nick, groups, image_hash)
else: else:
self.session.sendSync([number], delta = True, interactive = True)
self.session.subscribePresence(number)
buddy = Buddy(self.owner, number, nick, "", groups, image_hash) buddy = Buddy(self.owner, number, nick, "", groups, image_hash)
self[number] = buddy self[number] = buddy
self.logger.debug("Roster add: %s", 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: if buddy.presence == 0:
status = protocol_pb2.STATUS_NONE status = protocol_pb2.STATUS_NONE
elif buddy.presence == 'unavailable': elif buddy.presence == 'unavailable':
status = protocol_pb2.STATUS_AWAY status = protocol_pb2.STATUS_AWAY
else: else:
status = protocol_pb2.STATUS_ONLINE 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): def remove(self, number):
try: try:
@ -141,3 +170,49 @@ class BuddyList(dict):
return buddy return buddy
except KeyError: except KeyError:
return None 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)

139
deferred.py Normal file
View file

@ -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)

View file

@ -21,14 +21,83 @@ __email__ = "post@steffenvogel.de"
along with transWhat. If not, see <http://www.gnu.org/licenses/>. along with transWhat. If not, see <http://www.gnu.org/licenses/>.
""" """
from Spectrum2 import protocol_pb2
class Group(): class Group():
def __init__(self, id, owner, subject, subjectOwner): def __init__(self, id, owner, subject, subjectOwner, backend, user):
self.id = id self.id = id
self.subject = subject self.subject = subject
self.subjectOwner = subjectOwner self.subjectOwner = subjectOwner
self.owner = owner self.owner = owner
self.joined = False self.joined = False
self.backend = backend
self.user = user
self.nick = "me" 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)

View file

@ -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 <http://www.gnu.org/licenses/>.
"""
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")

147
registersession.py Normal file
View file

@ -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

View file

@ -30,7 +30,6 @@ from PIL import Image
import sys import sys
import os import os
from yowsup.common.tools import TimeTools
from yowsup.layers.protocol_media.mediauploader import MediaUploader from yowsup.layers.protocol_media.mediauploader import MediaUploader
from yowsup.layers.protocol_media.mediadownloader import MediaDownloader from yowsup.layers.protocol_media.mediadownloader import MediaDownloader
@ -40,6 +39,8 @@ from buddy import BuddyList
from threading import Timer from threading import Timer
from group import Group from group import Group
from bot import Bot from bot import Bot
import deferred
from deferred import call
from yowsupwrapper import YowsupApp from yowsupwrapper import YowsupApp
@ -53,6 +54,7 @@ class MsgIDs:
class Session(YowsupApp): class Session(YowsupApp):
broadcast_prefix = u'\U0001F4E2 '
def __init__(self, backend, user, legacyName, extra): def __init__(self, backend, user, legacyName, extra):
super(Session, self).__init__() super(Session, self).__init__()
@ -144,9 +146,10 @@ class Session(YowsupApp):
oroom.subjectOwner = subjectOwner oroom.subjectOwner = subjectOwner
oroom.subject = subject oroom.subject = subject
else: else:
self.groups[room] = Group(room, owner, subject, subjectOwner) self.groups[room] = Group(room, owner, subject, subjectOwner, self.backend, self.user)
# self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0]) # self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0])
self.groups[room].participants = group.getParticipants().keys() self.groups[room].addParticipants(group.getParticipants().keys(),
self.buddies, self.legacyName)
#self._addParticipantsToRoom(room, group.getParticipants()) #self._addParticipantsToRoom(room, group.getParticipants())
@ -173,13 +176,15 @@ class Session(YowsupApp):
self.legacyName, room, nick) self.legacyName, room, nick)
group = self.groups[room] group = self.groups[room]
group.joined = True
group.nick = nick group.nick = nick
group.participants[self.legacyName] = nick
try: try:
ownerNick = self.buddies[group.subjectOwner].nick ownerNick = group.participants[group.subjectOwner]
except KeyError: except KeyError:
ownerNick = group.subjectOwner ownerNick = group.subjectOwner
self._refreshParticipants(room) group.sendParticipantsToSpectrum(self.legacyName)
self.backend.handleSubject(self.user, self._shortenGroupId(room), self.backend.handleSubject(self.user, self._shortenGroupId(room),
group.subject, ownerNick) group.subject, ownerNick)
self.logger.debug("Room subject: room=%s, subject=%s", self.logger.debug("Room subject: room=%s, subject=%s",
@ -187,7 +192,6 @@ class Session(YowsupApp):
self.backend.handleRoomNicknameChanged( self.backend.handleRoomNicknameChanged(
self.user, self._shortenGroupId(room), group.subject self.user, self._shortenGroupId(room), group.subject
) )
group.joined = True
else: else:
self.logger.warn("Room doesn't exist: %s", room) self.logger.warn("Room doesn't exist: %s", room)
@ -199,29 +203,6 @@ class Session(YowsupApp):
else: else:
self.logger.warn("Room doesn't exist: %s. Unable to leave.", room) 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): def _lastSeen(self, number, seconds):
self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds))) self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds)))
if seconds < 60: if seconds < 60:
@ -237,16 +218,26 @@ class Session(YowsupApp):
self.backend.handleConnected(self.user) self.backend.handleConnected(self.user)
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, self.backend.handleBuddyChanged(self.user, "bot", self.bot.name,
["Admin"], protocol_pb2.STATUS_ONLINE) ["Admin"], protocol_pb2.STATUS_ONLINE)
if self.initialized == False: # Initialisation?
self.sendOfflineMessages() self.requestPrivacyList()
#self.bot.call("welcome") self.requestClientConfig()
self.initialized = True self.requestServerProperties()
# ?
self.logger.debug('Requesting groups list')
self.requestGroupsList(self._updateGroups)
# self.requestBroadcastList()
# This should handle, sync, statuses, and presence
self.sendPresence(True) self.sendPresence(True)
for func in self.loginQueue: for func in self.loginQueue:
func() func()
self.logger.debug('Requesting groups list') if self.initialized == False:
self.requestGroupsList(self._updateGroups) self.sendOfflineMessages()
#self.bot.call("welcome")
self.initialized = True
self.loggedIn = True self.loggedIn = True
# Called by superclass # Called by superclass
@ -268,16 +259,13 @@ class Session(YowsupApp):
type, participant, offline, items])) type, participant, offline, items]))
) )
try: try:
buddy = self.buddies[_from.split('@')[0]] number = _from.split('@')[0]
#self.backend.handleBuddyChanged(self.user, buddy.number.number, self.backend.handleMessageAck(self.user, number, self.msgIDs[_id].xmppId)
# buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE) self.msgIDs[_id].cnt = self.msgIDs[_id].cnt + 1
self.backend.handleMessageAck(self.user, buddy.number, self.msgIDs[_id].xmppId) if self.msgIDs[_id].cnt == 2:
self.msgIDs[_id].cnt = self.msgIDs[_id].cnt +1 del self.msgIDs[_id]
if self.msgIDs[_id].cnt == 2:
del self.msgIDs[_id]
except KeyError: except KeyError:
pass self.logger.error("Message %s not found. Unable to send ack", _id)
# Called by superclass # Called by superclass
def onAck(self, _id, _class, _from, timestamp): def onAck(self, _id, _class, _from, timestamp):
@ -299,29 +287,18 @@ class Session(YowsupApp):
self.sendReceipt(_id, _from, None, participant) self.sendReceipt(_id, _from, None, participant)
self.logger.info("Message received from %s to %s: %s (at ts=%s)", self.logger.info("Message received from %s to %s: %s (at ts=%s)",
buddy, self.legacyName, messageContent, timestamp) buddy, self.legacyName, messageContent, timestamp)
if participant is not None: # Group message if participant is not None: # Group message or broadcast
partname = participant.split('@')[0] partname = participant.split('@')[0]
try: if _from.split('@')[1] == 'broadcast': # Broadcast message
part = self.buddies[partname] message = self.broadcast_prefix + messageContent
if part.nick == "": self.sendMessageToXMPP(partname, message, timestamp)
part.nick = notify else: # Group message
self.backend.handleParticipantChanged( if notify is None:
self.user, partname, self._shortenGroupId(buddy), notify = ""
protocol_pb2.PARTICIPANT_FLAG_NONE, self.sendGroupMessageToXMPP(buddy, partname, messageContent,
protocol_pb2.STATUS_ONLINE, "", part.nick timestamp, notify)
) # TODO
except KeyError:
self.updateBuddy(partname, notify, [])
self.sendGroupMessageToXMPP(buddy, partname, messageContent,
timestamp)
else: else:
self.sendMessageToXMPP(buddy, messageContent, timestamp) 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 # Called by superclass
def onImage(self, image): def onImage(self, image):
@ -329,14 +306,19 @@ class Session(YowsupApp):
buddy = image._from.split('@')[0] buddy = image._from.split('@')[0]
participant = image.participant participant = image.participant
if image.caption is None: if image.caption is None:
image.caption = '' image.caption = ''
message = image.url + ' ' + image.caption
if participant is not None: # Group message if participant is not None: # Group message
partname = participant.split('@')[0] partname = participant.split('@')[0]
self.sendGroupMessageToXMPP(buddy, partname, message, image.timestamp) if image._from.split('@')[1] == 'broadcast': # Broadcast message
else: self.sendMessageToXMPP(partname, self.broadcast_prefix, image.timestamp)
self.sendMessageToXMPP(partname, image.url, image.timestamp)
self.sendMessageToXMPP(buddy, message, 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) self.sendReceipt(image._id, image._from, None, image.participant)
# Called by superclass # Called by superclass
@ -346,10 +328,13 @@ class Session(YowsupApp):
participant = audio.participant participant = audio.participant
message = audio.url message = audio.url
if participant is not None: # Group message if participant is not None: # Group message
partname = participant.split('@')[0] partname = participant.split('@')[0]
self.sendGroupMessageToXMPP(buddy, partname, message, audio.timestamp) if audio._from.split('@')[1] == 'broadcast': # Broadcast message
else: 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.sendMessageToXMPP(buddy, message, audio.timestamp)
self.sendReceipt(audio._id, audio._from, None, audio.participant) self.sendReceipt(audio._id, audio._from, None, audio.participant)
@ -361,10 +346,13 @@ class Session(YowsupApp):
message = video.url message = video.url
if participant is not None: # Group message if participant is not None: # Group message
partname = participant.split('@')[0] partname = participant.split('@')[0]
self.sendGroupMessageToXMPP(buddy, partname, message, video.timestamp) if video._from.split('@')[1] == 'broadcast': # Broadcast message
else: 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.sendMessageToXMPP(buddy, message, video.timestamp)
self.sendReceipt(video._id, video._from, None, video.participant) self.sendReceipt(video._id, video._from, None, video.participant)
@ -372,23 +360,30 @@ class Session(YowsupApp):
buddy = location._from.split('@')[0] buddy = location._from.split('@')[0]
latitude = location.getLatitude() latitude = location.getLatitude()
longitude = location.getLongitude() longitude = location.getLongitude()
url = location.getLocationUrl() url = location.getLocationURL()
participant = location.participant participant = location.participant
latlong = 'geo:' + latitude + ',' + longitude
self.logger.debug("Location received from %s: %s, %s", self.logger.debug("Location received from %s: %s, %s",
buddy, latitude, longitude) 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) if participant is not None: # Group message
self.sendMessageToXMPP(buddy, 'geo:' + latitude + ',' + longitude, partname = participant.split('@')[0]
location.timestamp) if location._from.split('@')[1] == 'broadcast': # Broadcast message
self.sendReceipt(location._id, location._from, None, location.participant, location.timestamp) 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 # Called by superclass
@ -398,13 +393,17 @@ class Session(YowsupApp):
_id, _from, name, card_data, to, notify, timestamp, participant _id, _from, name, card_data, to, notify, timestamp, participant
])) ]))
) )
message = "Received VCard (not implemented yet)"
buddy = _from.split("@")[0] buddy = _from.split("@")[0]
if participant is not None: # Group message if participant is not None: # Group message
partname = participant.split('@')[0] partname = participant.split('@')[0]
self.sendGroupMessageToXMPP(buddy, partname, "Received VCard (not implemented yet)", timestamp) if _from.split('@')[1] == 'broadcast': # Broadcast message
else: message = self.broadcast_prefix + message
self.sendMessageToXMPP(partname, message, timestamp)
self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)") else: # Group message
self.sendGroupMessageToXMPP(buddy, partname, message, timestamp)
else:
self.sendMessageToXMPP(buddy, message, timestamp)
# self.sendMessageToXMPP(buddy, card_data) # self.sendMessageToXMPP(buddy, card_data)
#self.transferFile(buddy, str(name), card_data) #self.transferFile(buddy, str(name), card_data)
self.sendReceipt(_id, _from, None, participant) self.sendReceipt(_id, _from, None, participant)
@ -442,11 +441,8 @@ class Session(YowsupApp):
subjectOwner = group.getSubjectOwnerJid(full = False) subjectOwner = group.getSubjectOwnerJid(full = False)
subject = utils.softToUni(group.getSubject()) subject = utils.softToUni(group.getSubject())
self.groups[room] = Group(room, owner, subject, subjectOwner) self.groups[room] = Group(room, owner, subject, subjectOwner, self.backend, self.user)
self.groups[room].participants = group.getParticipants().keys() self.groups[room].addParticipants(group.getParticipants, self.buddies, self.legacyName)
# self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0])
#self._addParticipantsToRoom(room, group.getParticipants())
self.bot.send("You have been added to group: %s@%s (%s)" self.bot.send("You have been added to group: %s@%s (%s)"
% (self._shortenGroupId(room), subject, self.backend.spectrum_jid)) % (self._shortenGroupId(room), subject, self.backend.spectrum_jid))
@ -454,68 +450,110 @@ class Session(YowsupApp):
def onParticipantsAddedToGroup(self, group): def onParticipantsAddedToGroup(self, group):
self.logger.debug("Participants added to group: %s", group) self.logger.debug("Participants added to group: %s", group)
room = group.getGroupId().split('@')[0] room = group.getGroupId().split('@')[0]
self.groups[room].participants.extend(group.getParticipants()) self.groups[room].addParticipants(group.getParticipants(), self.buddies, self.legacyName)
self._refreshParticipants(room) 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 # Called by superclass
def onParticipantsRemovedFromGroup(self, room, participants): def onParticipantsRemovedFromGroup(self, room, participants):
self.logger.debug("Participants removed from group: %s, %s", self.logger.debug("Participants removed from group: %s, %s",
room, participants) room, participants)
group = self.groups[room] self.groups[room].removeParticipants(participants)
for jid in participants:
group.participants.remove(jid) # Called by superclass
buddy = jid.split("@")[0] def onContactStatusChanged(self, number, status):
try: self.logger.debug("%s changed their status to %s", number, status)
nick = self.buddies[buddy].nick try:
except KeyError: buddy = self.buddies[number]
nick = buddy buddy.statusMsg = status
if nick == "": self.buddies.updateSpectrum(buddy)
nick = buddy except KeyError:
if buddy == self.legacyName: self.logger.debug("%s not in buddy list", number)
nick = group.nick
flags = protocol_pb2.PARTICIPANT_FLAG_NONE # Called by superclass
self.backend.handleParticipantChanged( def onContactPictureChanged(self, number):
self.user, nick, self._shortenGroupId(room), flags, self.logger.debug("%s changed their profile picture", number)
protocol_pb2.STATUS_NONE, buddy) 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): def onPresenceReceived(self, _type, name, jid, lastseen):
self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen) self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen)
buddy = jid.split("@")[0] buddy = jid.split("@")[0]
try: try:
buddy = self.buddies[buddy] buddy = self.buddies[buddy]
except KeyError: 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 return
if (lastseen == str(buddy.lastseen)) and (_type == buddy.presence): if (lastseen == str(buddy.lastseen)) and (_type == buddy.presence):
return return
if ((lastseen != "deny") and (lastseen != None) and (lastseen != "none")): if ((lastseen != "deny") and (lastseen != None) and (lastseen != "none")):
buddy.lastseen = int(lastseen) buddy.lastseen = int(lastseen)
if (_type == None): if (_type == None):
buddy.lastseen = time.time() buddy.lastseen = time.time()
buddy.presence = _type 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": if _type == "unavailable":
self.onPresenceUnavailable(buddy, statusmsg) self.onPresenceUnavailable(buddy)
else: 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.logger.info("Is available: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number, self.buddies.updateSpectrum(buddy)
buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE, statusmsg, buddy.image_hash)
def onPresenceUnavailable(self, buddy, statusmsg): def onPresenceUnavailable(self, buddy):
self.logger.info("Is unavailable: %s", buddy) self.logger.info("Is unavailable: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number, self.buddies.updateSpectrum(buddy)
buddy.nick, buddy.groups, protocol_pb2.STATUS_AWAY, statusmsg, buddy.image_hash)
# spectrum RequestMethods # spectrum RequestMethods
def sendTypingStarted(self, buddy): def sendTypingStarted(self, buddy):
@ -532,29 +570,65 @@ class Session(YowsupApp):
self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy) self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy)
self.sendTyping(buddy, False) self.sendTyping(buddy, False)
def sendMessageToWA(self, sender, message, ID): def sendImage(self, message, ID, to):
self.logger.info("Message sent from %s to %s: %s", if (".jpg" in message.lower()):
self.legacyName, sender, message) 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") 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": if sender == "bot":
self.bot.parse(message) self.bot.parse(message)
elif "-" in sender: # group msg elif "-" in sender: # group msg
if "/" in sender: # directed at single user if "/" in sender: # directed at single user
room, nick = sender.split("/") room, nick = sender.split("/")
for buddy, buddy3 in self.buddies.iteritems(): group = self.groups[room]
self.logger.info("Group buddy=%s nick=%s", buddy, number = None
buddy3.nick) for othernumber, othernick in group.participants.iteritems():
if buddy3.nick == nick: if othernick == nick:
nick = buddy number = othernumber
waId = self.sendTextMessage(nick + '@s.whatsapp.net', message) break
self.msgIDs[waId] = MsgIDs( ID, waId) 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: else:
room = sender room = sender
if message[0] == '\\' and message[:1] != '\\\\': if message[0] == '\\' and message[:1] != '\\\\':
@ -566,30 +640,17 @@ class Session(YowsupApp):
self.logger.debug("Group Message from %s to %s Groups: %s", self.logger.debug("Group Message from %s to %s Groups: %s",
group.nick , group , self.groups) group.nick , group , self.groups)
self.backend.handleMessage( 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: except KeyError:
self.logger.error('Group not found: %s', room) self.logger.error('Group not found: %s', room)
if (".jpg" in message.lower()) or (".webp" in message.lower()): if (".jpg" in message.lower()) or (".webp" in message.lower()):
if (".jpg" in message.lower()): self.sendImage(message, ID, room + '@g.us')
self.imgType = "jpg" elif "geo:" in message.lower():
if (".webp" in message.lower()): self._sendLocation(room + "@g.us", message, ID)
self.imgType = "webp" else:
self.imgMsgId = ID self.sendTextMessage(room + '@g.us', message)
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)
else: # private msg else: # private msg
buddy = sender buddy = sender
# if message == "\\lastseen": # if message == "\\lastseen":
@ -605,20 +666,8 @@ class Session(YowsupApp):
#self.call("contact_getProfilePicture", (buddy + "@s.whatsapp.net",)) #self.call("contact_getProfilePicture", (buddy + "@s.whatsapp.net",))
self.requestVCard(buddy) self.requestVCard(buddy)
else: else:
if (".jpg" in message.lower()) or (".webp" in message.lower()): if (".jpg" in message.lower()) or (".webp" in message.lower()):
#waId = self.call("message_imageSend", (buddy + "@s.whatsapp.net", message, None, 0, None)) self.sendImage(message, ID, buddy + "@s.whatsapp.net")
#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"
elif "geo:" in message.lower(): elif "geo:" in message.lower():
self._sendLocation(buddy + "@s.whatsapp.net", message, ID) self._sendLocation(buddy + "@s.whatsapp.net", message, ID)
else: else:
@ -635,20 +684,7 @@ class Session(YowsupApp):
self.leaveGroup(room) self.leaveGroup(room)
# Delete Room on spectrum side # Delete Room on spectrum side
group = self.groups[room] group = self.groups[room]
for jid in group.participants: group.leaveRoom()
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)
del self.groups[room] del self.groups[room]
def _requestLastSeen(self, buddy): def _requestLastSeen(self, buddy):
@ -686,35 +722,36 @@ class Session(YowsupApp):
self.backend.handleMessage(self.user, buddy, messageContent, "", self.backend.handleMessage(self.user, buddy, messageContent, "",
"", timestamp) "", timestamp)
def sendGroupMessageToXMPP(self, room, buddy, messageContent, timestamp = ""): def sendGroupMessageToXMPP(self, room, number, messageContent, timestamp = u"", defaultname = u""):
# self._refreshParticipants(room)
try:
nick = self.buddies[buddy].nick
except KeyError:
nick = buddy
if nick == "":
nick = buddy
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("Group message queued from %s to %s: %s", self.logger.debug("Group message queued from %s to %s: %s",
buddy, room, messageContent) number, room, messageContent)
if room not in self.groupOfflineQueue: if room not in self.groupOfflineQueue:
self.groupOfflineQueue[room] = [ ] self.groupOfflineQueue[room] = [ ]
self.groupOfflineQueue[room].append( self.groupOfflineQueue[room].append(
(buddy, messageContent, timestamp) (number, messageContent, timestamp)
) )
else: else:
self.logger.debug("Group message sent from %s (%s) to %s: %s", self.logger.debug("Group message sent from %s to %s: %s",
buddy, nick, room, messageContent) number, room, messageContent)
try: try:
group = self.groups[room] 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: if group.joined:
self.backend.handleMessage(self.user,room, messageContent, self.backend.handleMessage(self.user, room, messageContent,
nick, "", timestamp) nick, "", timestamp)
else: else:
self.bot.send("You have received a message in group: %s@%s" self.bot.send("You have received a message in group: %s@%s"
@ -724,7 +761,7 @@ class Session(YowsupApp):
except KeyError: except KeyError:
self.logger.warn("Group is not in group list") self.logger.warn("Group is not in group list")
self.backend.handleMessage(self.user, self._shortenGroupId(room), self.backend.handleMessage(self.user, self._shortenGroupId(room),
messageContent, nick, "", timestamp) messageContent, number, "", timestamp)
def changeStatus(self, status): def changeStatus(self, status):
@ -770,61 +807,8 @@ class Session(YowsupApp):
self.buddies.remove(buddy) self.buddies.remove(buddy)
def requestVCard(self, buddy, ID=None): def requestVCard(self, buddy, ID=None):
def onSuccess(response, request): self.buddies.requestVCard(buddy, ID)
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.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): def createThumb(self, size=100, raw=False):
img = Image.open(self.imgPath) img = Image.open(self.imgPath)
width, height = img.size width, height = img.size

19
threadutils.py Normal file
View file

@ -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()

View file

@ -29,8 +29,8 @@ import logging
import asyncore import asyncore
import sys, os import sys, os
import e4u import e4u
import threading
import Queue import Queue
import threadutils
sys.path.insert(0, os.getcwd()) sys.path.insert(0, os.getcwd())
@ -62,7 +62,13 @@ logging.basicConfig( \
# Handler # Handler
def handleTransportData(data): 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() e4u.load()
@ -76,7 +82,13 @@ io = IOChannel(args.host, args.port, handleTransportData, connectionClosed)
plugin = WhatsAppBackend(io, args.j) 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: while True:
try: try:
@ -90,6 +102,13 @@ while True:
break break
if closed: if closed:
break break
while True:
try:
callback = threadutils.eventQueue.get_nowait()
except Queue.Empty:
break
else:
callback()
except SystemExit: except SystemExit:
break break
except: except:

View file

@ -25,6 +25,7 @@ from Spectrum2.backend import SpectrumBackend
from Spectrum2 import protocol_pb2 from Spectrum2 import protocol_pb2
from session import Session from session import Session
from registersession import RegisterSession
import logging import logging
@ -36,18 +37,20 @@ class WhatsAppBackend(SpectrumBackend):
self.sessions = { } self.sessions = { }
self.spectrum_jid = spectrum_jid self.spectrum_jid = spectrum_jid
# Used to prevent duplicate messages # Used to prevent duplicate messages
self.lastMessage = {} self.lastMsgId = {}
self.logger.debug("Backend started") self.logger.debug("Backend started")
# RequestsHandlers # RequestsHandlers
def handleLoginRequest(self, user, legacyName, password, extra): def handleLoginRequest(self, user, legacyName, password, extra):
self.logger.debug("handleLoginRequest(user=%s, legacyName=%s)", user, legacyName) self.logger.debug("handleLoginRequest(user=%s, legacyName=%s)", user, legacyName)
if user not in self.sessions: # Key word means we should register a new password
self.sessions[user] = Session(self, user, legacyName, extra) if password == 'register':
if user not in self.sessions:
if user not in self.lastMessage: self.sessions[user] = RegisterSession(self, user, legacyName, extra)
self.lastMessage[user] = {} else:
if user not in self.sessions:
self.sessions[user] = Session(self, user, legacyName, extra)
self.sessions[user].login(password) self.sessions[user].login(password)
@ -57,20 +60,17 @@ class WhatsAppBackend(SpectrumBackend):
self.sessions[user].logout() self.sessions[user].logout()
del self.sessions[user] del self.sessions[user]
def handleMessageSendRequest(self, user, buddy, message, xhtml = "", ID = 0): def handleMessageSendRequest(self, user, buddy, message, xhtml="", ID=""):
self.logger.debug("handleMessageSendRequest(user=%s, buddy=%s, message=%s, xhtml = %s)", user, buddy, message, xhtml) 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 # For some reason spectrum occasionally sends to identical messages to
# a buddy, one to the bare jid and one to /bot. This causes duplicate # a buddy, one to the bare jid and one to the /bot resource. This
# messages. Since it is unlikely a user wants to send the same message # causes duplicate messages. Thus we should not send consecutive
# twice, we should just ignore the second message # messages with the same id
# if ID == '':
# TODO Proper fix, this work around drops all duplicate messages even self.sessions[user].sendMessageToWA(buddy, message, ID, xhtml)
# intentional ones. elif user not in self.lastMsgId or self.lastMsgId[user] != ID:
# IDEA there is an ID field in ConvMessage. If it is extracted it will work self.sessions[user].sendMessageToWA(buddy, message, ID, xhtml)
usersMessage = self.lastMessage[user] self.lastMsgId[user] = ID
if buddy not in usersMessage or usersMessage[buddy] != message:
self.sessions[user].sendMessageToWA(buddy, message, ID)
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)
@ -117,14 +117,29 @@ class WhatsAppBackend(SpectrumBackend):
self.logger.debug("handleVCardRequest(user=%s, buddy=%s, ID=%s)", user, buddy, ID) self.logger.debug("handleVCardRequest(user=%s, buddy=%s, ID=%s)", user, buddy, ID)
self.sessions[user].requestVCard(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 # TODO
def handleBuddyBlockToggled(self, user, buddy, blocked):
pass
def handleVCardUpdatedRequest(self, user, photo, nickname):
pass
def handleAttentionRequest(self, user, buddy, message): def handleAttentionRequest(self, user, buddy, message):
pass pass

View file

@ -35,11 +35,21 @@ from yowsup.layers.protocol_chatstate.protocolentities import *
from yowsup.layers.protocol_contacts.protocolentities import * from yowsup.layers.protocol_contacts.protocolentities import *
from yowsup.layers.protocol_groups.protocolentities import * from yowsup.layers.protocol_groups.protocolentities import *
from yowsup.layers.protocol_media.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_messages.protocolentities import *
from yowsup.layers.protocol_presence.protocolentities import * from yowsup.layers.protocol_presence.protocolentities import *
from yowsup.layers.protocol_profiles.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_receipts.protocolentities import *
from yowsup.layers.protocol_iq.protocolentities import *
from yowsup.layers.protocol_media.mediauploader import MediaUploader 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 from functools import partial
@ -127,6 +137,10 @@ class YowsupApp(object):
receipt = OutgoingReceiptProtocolEntity(_id, _from, read, participant) receipt = OutgoingReceiptProtocolEntity(_id, _from, read, participant)
self.sendEntity(receipt) self.sendEntity(receipt)
def downloadMedia(self, url, onSuccess = None, onFailure = None):
downloader = MediaDownloader(onSuccess, onFailure)
downloader.download(url)
def sendTextMessage(self, to, message): def sendTextMessage(self, to, message):
""" """
Sends a text message Sends a text message
@ -144,26 +158,27 @@ class YowsupApp(object):
self.sendEntity(messageEntity) self.sendEntity(messageEntity)
return messageEntity.getId() return messageEntity.getId()
def image_send(self, jid, path, caption = None): def sendImage(self, jid, path, caption = None, onSuccess = None, onFailure = None):
entity = RequestUploadIqProtocolEntity(RequestUploadIqProtocolEntity.MEDIA_TYPE_IMAGE, filePath=path) entity = RequestUploadIqProtocolEntity(RequestUploadIqProtocolEntity.MEDIA_TYPE_IMAGE, filePath=path)
successFn = lambda successEntity, originalEntity: self.onRequestUploadResult(jid, path, successEntity, originalEntity, caption) successFn = lambda successEntity, originalEntity: self.onRequestUploadResult(jid, path, successEntity, originalEntity, caption, onSuccess, onFailure)
errorFn = lambda errorEntity, originalEntity: self.onRequestUploadError(jid, path, errorEntity, originalEntity) errorFn = lambda errorEntity, originalEntity: self.onRequestUploadError(jid, path, errorEntity, originalEntity)
self.sendIq(entity, successFn, errorFn) 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: if requestUploadIqProtocolEntity.mediaType == RequestUploadIqProtocolEntity.MEDIA_TYPE_AUDIO:
doSendFn = self._doSendAudio doSendFn = self.doSendAudio
else: else:
doSendFn = self._doSendImage doSendFn = self.doSendImage
if resultRequestUploadIqProtocolEntity.isDuplicate(): if resultRequestUploadIqProtocolEntity.isDuplicate():
doSendFn(filePath, resultRequestUploadIqProtocolEntity.getUrl(), jid, doSendFn(filePath, resultRequestUploadIqProtocolEntity.getUrl(), jid,
resultRequestUploadIqProtocolEntity.getIp(), caption) resultRequestUploadIqProtocolEntity.getIp(), caption)
else: else:
successFn = lambda filePath, jid, url: doSendFn(filePath, url, jid, resultRequestUploadIqProtocolEntity.getIp(), caption) successFn = lambda filePath, jid, url: doSendFn(filePath, url, jid, resultRequestUploadIqProtocolEntity.getIp(), caption, onSuccess, onFailure)
mediaUploader = MediaUploader(jid, self.legacyName, filePath, ownNumber = self.stack.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(full=False)
mediaUploader = MediaUploader(jid, ownNumber, filePath,
resultRequestUploadIqProtocolEntity.getUrl(), resultRequestUploadIqProtocolEntity.getUrl(),
resultRequestUploadIqProtocolEntity.getResumeOffset(), resultRequestUploadIqProtocolEntity.getResumeOffset(),
successFn, self.onUploadError, self.onUploadProgress, async=False) successFn, self.onUploadError, self.onUploadProgress, async=False)
@ -181,17 +196,21 @@ class YowsupApp(object):
#sys.stdout.flush() #sys.stdout.flush()
pass 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) entity = ImageDownloadableMediaMessageProtocolEntity.fromFilePath(filePath, url, ip, to, caption = caption)
self.sendEntity(entity) 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() 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) entity = AudioDownloadableMediaMessageProtocolEntity.fromFilePath(filePath, url, ip, to)
self.sendEntity(entity) 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() return entity.getId()
@ -252,7 +271,20 @@ class YowsupApp(object):
""" """
iq = SetStatusIqProtocolEntity(statusText) iq = SetStatusIqProtocolEntity(statusText)
self.sendIq(iq) 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): def sendTyping(self, phoneNumber, typing):
""" """
Notify buddy using phoneNumber that you are typing to him Notify buddy using phoneNumber that you are typing to him
@ -271,12 +303,12 @@ class YowsupApp(object):
ChatstateProtocolEntity.STATE_PAUSED, jid ChatstateProtocolEntity.STATE_PAUSED, jid
) )
self.sendEntity(state) 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 You need to sync new contacts before you interact with
them, failure to do so could result in a temporary ban. them, failure to do so could result in a temporary ban.
Args: Args:
- contacts: ([str]) a list of phone numbers of the - contacts: ([str]) a list of phone numbers of the
contacts you wish to sync contacts you wish to sync
@ -285,12 +317,62 @@ class YowsupApp(object):
contact list. contact list.
- interactive: (bool; default: True) Set to false if you are - interactive: (bool; default: True) Set to false if you are
sure this is the first time registering 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 mode = GetSyncIqProtocolEntity.MODE_DELTA if delta else GetSyncIqProtocolEntity.MODE_FULL
context = GetSyncIqProtocolEntity.CONTEXT_INTERACTIVE if interactive else GetSyncIqProtocolEntity.CONTEXT_REGISTRATION 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) 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): def requestLastSeen(self, phoneNumber, success = None, failure = None):
""" """
@ -336,6 +418,34 @@ class YowsupApp(object):
iq = InfoGroupsIqProtocolEntity(group + '@g.us') iq = InfoGroupsIqProtocolEntity(group + '@g.us')
self.sendIq(iq, onSuccess = onSuccess, onError = onFailure) 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): def onAuthSuccess(self, status, kind, creation, expiration, props, nonce, t):
""" """
Called when login is successful. Called when login is successful.
@ -504,13 +614,66 @@ class YowsupApp(object):
def onParticipantsRemovedFromGroup(self, group, participants): def onParticipantsRemovedFromGroup(self, group, participants):
"""Called when participants have been removed from a group """Called when participants have been removed from a group
Args: Args:
- group: (str) id of the group (e.g. 27831788123-144024456) - group: (str) id of the group (e.g. 27831788123-144024456)
- participants: (list) jids of participants that are removed - participants: (list) jids of participants that are removed
""" """
pass 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): def sendEntity(self, entity):
"""Sends an entity down the stack (as if YowsupAppLayer called toLower)""" """Sends an entity down the stack (as if YowsupAppLayer called toLower)"""
self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT, self.stack.broadcastEvent(YowLayerEvent(YowsupAppLayer.TO_LOWER_EVENT,
@ -617,6 +780,34 @@ class YowsupAppLayer(YowInterfaceLayer):
entity.getGroupId().split('@')[0], entity.getGroupId().split('@')[0],
entity.getParticipants().keys() 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') @ProtocolEntityCallback('message')
def onMessageReceived(self, entity): def onMessageReceived(self, entity):