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

@ -64,6 +64,9 @@ Create a new file `/etc/spectrum2/transports/whatsapp.cfg` with the following co
config = /etc/spectrum2/logging.cfg
backend_config = /etc/spectrum2/backend-logging.cfg
[database]
type = sqlite3
## transWhat
### Installation
@ -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.

View file

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

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 |
| `\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 |

18
bot.py
View file

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

137
buddy.py
View file

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

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/>.
"""
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)

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 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)
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, [])
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)
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):
@ -330,13 +307,18 @@ class Session(YowsupApp):
participant = image.participant
if image.caption is None:
image.caption = ''
message = image.url + ' ' + image.caption
if participant is not None: # Group message
partname = participant.split('@')[0]
self.sendGroupMessageToXMPP(buddy, partname, message, image.timestamp)
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, message, image.timestamp)
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
@ -347,9 +329,12 @@ class Session(YowsupApp):
message = audio.url
if participant is not None: # Group message
partname = participant.split('@')[0]
if audio._from.split('@')[1] == 'broadcast': # Broadcast message
self.sendMessageToXMPP(partname, self.broadcast_prefix, audio.timestamp)
self.sendMessageToXMPP(partname, message, audio.timestamp)
else: # Group message
self.sendGroupMessageToXMPP(buddy, partname, message, audio.timestamp)
else:
self.sendMessageToXMPP(buddy, message, audio.timestamp)
self.sendReceipt(audio._id, audio._from, None, audio.participant)
@ -362,9 +347,12 @@ class Session(YowsupApp):
message = video.url
if participant is not None: # Group message
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]
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, 'geo:' + latitude + ',' + longitude,
location.timestamp)
self.sendGroupMessageToXMPP(buddy, partname, latlong, location.timestamp)
else:
if url is not None:
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)
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)
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, "Received VCard (not implemented yet)")
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,29 +450,74 @@ 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]
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:
nick = self.buddies[buddy].nick
buddy = self.buddies[number]
buddy.statusMsg = status
self.buddies.updateSpectrum(buddy)
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.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)
@ -484,6 +525,8 @@ class Session(YowsupApp):
try:
buddy = self.buddies[buddy]
except KeyError:
# Sometimes whatsapp send our own presence
if buddy != self.legacyName:
self.logger.error("Buddy not found: %s", buddy)
return
@ -497,25 +540,20 @@ class Session(YowsupApp):
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)
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"
self.sendImage(message, ID, 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.sendTextMessage(room + '@g.us', message)
else: # private msg
buddy = sender
# if message == "\\lastseen":
@ -606,19 +667,7 @@ class Session(YowsupApp):
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"
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,33 +722,34 @@ 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,
nick, "", timestamp)
@ -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,60 +807,7 @@ 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.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)
self.buddies.requestVCard(buddy, ID)
def createThumb(self, size=100, raw=False):
img = Image.open(self.imgPath)

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

View file

@ -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,19 +37,21 @@ 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)
# 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)
if user not in self.lastMessage:
self.lastMessage[user] = {}
self.sessions[user].login(password)
def handleLogoutRequest(self, user, legacyName):
@ -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

View file

@ -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):
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)
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())
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()
@ -253,6 +272,19 @@ 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
@ -272,7 +304,7 @@ class YowsupApp(object):
)
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.
@ -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.
@ -511,6 +621,59 @@ class YowsupApp(object):
"""
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):