Merge pull request #1 from moyamo/yowsup-2

update
This commit is contained in:
dazzzl 2015-11-05 14:30:43 +01:00
commit 1b16e55f73
20 changed files with 1459 additions and 1311 deletions

View file

@ -1,44 +1,33 @@
# transWhat # transWhat
transWhat is a WhatsApp XMPP Gateway based on Spectrum2 transWhat is a WhatsApp XMPP Gateway based on [Spectrum 2](http://www.spectrum.im) and [Yowsup 2](https://github.com/tgalal/yowsup).
## Dependencies ## Dependencies
#### Python packages
pip install e4u protobuf mysql dateutil
- **e4u**: is a simple emoji4unicode python bindings
- **yowsup**:
- **mysqldb**: MySQL client python bindings
#### Spectrum 2 #### Spectrum 2
is a XMPP transport is a XMPP transport
Manual compile latest version from https://github.com/hanzz/libtransport Manual compile latest version from https://github.com/hanzz/libtransport.
#### e4u ## Contributors
is a simple emoji4unicode python wrapper library
Install with `pip install e4u` Pull requests, bug reports etc. are welcome. Help us to provide a open implementation of the WhatsApp protocol.
#### Yowsup The following persons have contributed major parts of this code:
is a Implementation of the WhatsApp protocol in python
Use my patched version at https://github.com/stv0g/yowsup - **Steffen Vogel** (@stv0g): Idea and initial implementation based on Yowsup 1
- **Mohammed Yaseen Mowzer** (@moyamo): Port to Yowsup 2
#### Google Atom and GData Python wrappers
required for Google contacts import
#### MySQLdb
required
#### Google protobuf
required
#### date.util
required
## Contribute
Pull requests, bug reports etc. are welcome.
Help us to provide a open implementation of the WhatsApp protocol.
## Documentation ## Documentation
A project wiki is available [here](http://dev.0l.de/projects/transwhat/start). A project wiki is available [here](http://dev.0l.de/projects/transwhat/start).
A mailinglist for discussion is available [here](http://lists.0l.de/listinfo/whatsapp).
A writeup of this project is also availabe at my [blog](http://www.steffenvogel.de/2013/06/29/transwhat/). An *outdated* writeup of this project is also availabe at my [blog](http://www.steffenvogel.de/2013/06/29/transwhat/).

View file

@ -4,6 +4,7 @@ import struct
import sys import sys
import os import os
import logging
import google.protobuf import google.protobuf
def WRAP(MESSAGE, TYPE): def WRAP(MESSAGE, TYPE):
@ -24,6 +25,7 @@ class SpectrumBackend:
self.m_pingReceived = False self.m_pingReceived = False
self.m_data = "" self.m_data = ""
self.m_init_res = 0 self.m_init_res = 0
self.logger = logging.getLogger(self.__class__.__name__)
def handleMessage(self, user, legacyName, msg, nickname = "", xhtml = "", timestamp = ""): def handleMessage(self, user, legacyName, msg, nickname = "", xhtml = "", timestamp = ""):
m = protocol_pb2.ConversationMessage() m = protocol_pb2.ConversationMessage()
@ -204,7 +206,7 @@ class SpectrumBackend:
def handleFTData(self, ftID, data): def handleFTData(self, ftID, data):
d = protocol_pb2.FileTransferData() d = protocol_pb2.FileTransferData()
d.ftid = ftID d.ftID = ftID
d.data = data d.data = data
message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA); message = WRAP(d.SerializeToString(), protocol_pb2.WrapperMessage.TYPE_FT_DATA);
@ -349,8 +351,15 @@ class SpectrumBackend:
wrapper = protocol_pb2.WrapperMessage() wrapper = protocol_pb2.WrapperMessage()
if (wrapper.ParseFromString(self.m_data[4:]) == False): try:
parseFromString = wrapper.ParseFromString(self.m_data[4:])
except:
parseFromString = True
self.logger.error("Parse from String exception")
if parseFromString == False:
self.m_data = self.m_data[expected_size+4:] self.m_data = self.m_data[expected_size+4:]
self.logger.error("Parse from String exception")
return return
self.m_data = self.m_data[4+expected_size:] self.m_data = self.m_data[4+expected_size:]

View file

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

File diff suppressed because one or more lines are too long

59
bot.py
View file

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

View file

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

View file

@ -1,73 +0,0 @@
#!/usr/bin/python
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2013, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
__status__ = "Prototype"
"""
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 os
import sys
import cgi
import cgitb
import time
sys.path.insert(1, os.path.join(sys.path[0], '..'))
from constants import *
def cookies(str):
return dict(c.split('=') for c in str.split(";"))
def save_token(timestamp, number, token, filename="tokens"):
file = open(filename, 'a')
file.write("%s\t%s\t%s\n" % (str(timestamp), number, token))
file.close()
def main():
form = cgi.FieldStorage()
number = form.getfirst("number")
auth_url = form.getfirst("auth_url")
token = form.getfirst("code")
if auth_url:
print "Status: 301 Moved"
print "Location: %s" % auth_url
print "Content-type: text/html"
print "Set-Cookie: number=%s" % number
print "\n\n";
elif token and os.environ.has_key('HTTP_COOKIE'):
print "Status: 301 Moved"
print "Content-type: text/html"
print "Location: http://whatsapp.0l.de"
print
c = cookies(os.environ['HTTP_COOKIE'])
save_token(time.time(), c['number'], token, TOKEN_FILE)
else:
print "Content-type: text/html"
print "\n"
print "something strange happened :("
if __name__ == "__main__":
main()

View file

@ -1,11 +0,0 @@
<?xml version="1.0" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="refresh" content="0;url=http://dev.0l.de/projects/transwhat/start">
</head>
<body>
</body>
</html>

View file

@ -1,237 +0,0 @@
#!/usr/bin/env python
# -*- encoding: UTF8 -*-
# author: Philipp Klaus, philipp.klaus →AT→ gmail.com
# This file is part of python-sipgate-xmlrpc.
#
# python-sipgate-xmlrpc 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
# (at your option) any later version.
#
# python-sipgate-xmlrpc 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 python-sipgate-xmlrpc. If not, see <http://www.gnu.org/licenses/>.
#####################################################################
###### This the most important file of the project: #######
###### It contains the classe api, which #######
###### implements the XML-RPC communication with the #######
###### Sipgate API. #######
#from time import time
from sys import stderr
from xmlrpclib import ServerProxy, Fault, ProtocolError, ResponseError
from exceptions import TypeError
from socket import error as socket_error
import re
VERSION = "0.9.2"
NAME = "%s - python-sipgate-xmlrpc/sipgate.py"
VENDOR = "https://github.com/pklaus/python-sipgate-xmlrpc"
### ------- Here comes the most important piece of code: the api class with magic methods -----
class api (ServerProxy):
def __init__ (self, username=False, password=False, prog_name=False, verbose=False):
if not (username and password and prog_name):
raise SipgateAPIException('To use the class sipgate.api you must provide, username, password and a program name.')
address = SIPGATE_API_URL % {'username':username, 'password':password}
### The super() call would be more modern but it doesn't work with the current Python version yet.
#super(api, self).__init__(address, verbose=debug)
ServerProxy.__init__(self, address,verbose=verbose)
### It is considered good practice to Identify the client talking to the server:
self.ClientIdentify({ "ClientName" : NAME % prog_name, "ClientVersion" : VERSION, "ClientVendor" : VENDOR })
def __getattr__(self,name):
return _Method(self.__request, name)
def __request (self, methodname, params):
if methodname.replace(API_PREFIX,'') not in VALID_METHODS:
stderr.write( UNKNOWN_METHOD_MESSAGE % {
'method': methodname.replace(API_PREFIX,''), 'api_prefix': API_PREFIX,
'api_version': SIPGATE_API_DOC_V, 'api_date': SIPGATE_API_DOC_D } )
if len(params)>0 and not type(params[0]) is dict:
raise TypeError(DICT_AS_PARAM_MESSAGE % methodname.replace(API_PREFIX,''))
method_function = ServerProxy.__getattr__(self,methodname)
try:
result = method_function(params[0] if len(params)>0 and type(params[0]) is dict else dict())
# cast the result dictionary to a SipgateResponse (custom dictionary):
result = SipgateResponse(result)
except Fault, e:
raise SipgateAPIFault(e.faultCode, e.faultString)
except ProtocolError, e:
raise SipgateAPIProtocolError(e.url, e.errcode, e.errmsg, e.headers)
except socket_error, (value,message):
raise SipgateAPISocketError(value, message)
return result
## <http://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict-and-override-get-set>
class SipgateResponse(dict):
def __init__(self, response_dict):
try:
self.StatusCode, self.StatusString = int(response_dict['StatusCode']), response_dict['StatusString']
self.success = self.StatusCode == 200
except:
raise TypeError(RESPONSE_NOT_A_DICTIONARY % response_dict)
dict.__init__(self, response_dict)
class _Method:
# With the help of this class the api class does not
# need to state explicitly the possible XML-RPC calls.
def __init__(self, send, name):
self.__send = send
self.__name = API_PREFIX+name
def __call__(self, *args):
return self.__send(self.__name, args)
### ------ now we define the exceptions that could occur ------
class SipgateAPIException(Exception):
pass
class SipgateAPIFault(Fault, SipgateAPIException):
# As this inherits from xmlrpclib.Fault it also has the
# attributes faultCode and faultString.
pass
class SipgateAPIProtocolError(ProtocolError, SipgateAPIException):
# As this inherits from xmlrpclib.ProtocolError it also has the
# attributes errcode and errmsg.
pass
class SipgateAPISocketError(socket_error, SipgateAPIException):
# As this inherits from socket.error it also has the
# attributes .
pass
### ------ This section contains message strings -------
UNKNOWN_METHOD_MESSAGE = "The method '%(method)s' for the API prefix '%(api_prefix)s' " + \
"was called. This method, however, is currently not documented for the Sipgate API " + \
"v%(api_version)s (%(api_date)s). Let's try but I've warned you.\n"
DICT_AS_PARAM_MESSAGE = 'Please specify a dictionary as function call parameter for api.%s().'
RESPONSE_NOT_A_DICTIONARY = 'The response "%s" does not seem to be a response from the ' + \
'Sipgate XML-RPC API.'
### ------ This section contains constants of the Sipgate XML-RPC API -------
# This constant represents the version of the currently implemented Sipgate API
# ans is taken from the API description PDF:
SIPGATE_API_DOC_V = '1.06'
SIPGATE_API_DOC_D = 'August 21, 2007'
# Sipgate basic and plus accounts must use this API URL:
SIPGATE_API_URL = "https://%(username)s:%(password)s@samurai.sipgate.net/RPC2"
# Sipgate one and team have a different URL: api.sipgate.net.
# see <http://groups.google.com/group/sipgate-api/msg/51a3535b6d61241f>
API_PREFIX = 'samurai.'
VALID_METHODS = [
'AccountStatementGet',
'BalanceGet',
'ClientIdentify',
'HistoryGetByDate',
'ItemizedEntriesGet',
'OwnUriListGet',
'PhonebookEntryGet',
'PhonebookListGet',
'RecommendedIntervalGet',
'ServerdataGet',
'SessionClose',
'SessionInitiate',
'SessionInitiateMulti',
'SessionStatusGet',
'TosListGet',
'TosListGet',
'UmSummaryGet',
'UserdataGreetingGet',
'UserdataSipGet',
]
SERVER_STATUS_CODES = {
### From Table A.1 and A.2 of the API docu: general server status codes
200: 'Method success',
400: 'Method not supported',
401: 'Request denied (no reason specified)',
402: 'Internal error',
403: 'Invalid arguments',
404: 'Resources exceeded (this MUST not be used to indicate parameters in error)',
405: 'Invalid parameter name',
406: 'Invalid parameter type',
407: 'Invalid parameter value',
408: 'Attempt to set a non-writable parameter',
409: 'Notification request rejected.',
410: 'Parameter exceeds maximum size.',
411: 'Missing parameter.',
412: 'Too many requests.',
500: 'Date out of range.',
501: 'Uri does not belong to user.',
502: 'Unknown type of service.',
503: 'Selected payment method failed.',
504: 'Selected currency not supported.',
505: 'Amount exceeds limit.',
506: 'Malformed SIP URI.',
507: 'URI not in list.',
508: 'Format is not valid E.164.',
509: 'Unknown status.',
510: 'Unknown ID.',
511: 'Invalid timevalue.',
512: 'Referenced session not found.',
513: 'Only single default per TOS allowed.',
514: 'Malformed VCARD format.',
515: 'Malformed PID format.',
516: 'Presence information not available.',
517: 'Invalid label name.',
518: 'Label not assigned.',
519: 'Label doesnt exist.',
520: 'Parameter includes invalid characters.',
521: 'Bad password. (Rejected due to security concerns.)',
522: 'Malformed timezone format.',
523: 'Delay exceeds limit.',
524: 'Requested VPN type not available.',
525: 'Requested TOS not available.',
526: 'Unified messaging not available.',
527: 'URI not available for registration.',
}
TYPE_OF_SERVICE = {
'fax': 'pages', # fax transmission
'text': 'characters', # text message (e.g. "SMS")
'video': 'seconds', # video communication
'voice': 'seconds', # voice communication
}
class helpers (object):
@staticmethod
def FQTN(phone_number, default_country_code):
"""
Assures phone numbers are in the form of a E164 Fully Qualified Telephone Number
without the leading + sign.
The alternative would be the Python port of Google's libphonenumber:
https://github.com/daviddrysdale/python-phonenumbers
"""
phone_number = phone_number.replace(' ','').replace('-','').replace('+','').replace('/','')
## number starting with 00 (so it's an international format)
if re.compile("^00[1-9][0-9]*$").match(phone_number):
return phone_number[2:]
## number starting with your country code (so it was already a FQTN):
if re.compile("^"+default_country_code+"[1-9][0-9]*$").match(phone_number):
return phone_number
if re.compile("^0[1-9]*$").match(phone_number):
return default_country_code+phone_number[1:]
if re.compile("^[1-9]*$").match(phone_number):
return phone_number
raise TypeError("Couldn't parse this phone number: "+phone_number)

View file

@ -1,99 +0,0 @@
#!/usr/bin/python
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2013, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
__status__ = "Prototype"
"""
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 os
import sys
import cgi
import cgitb
import time
import pycurl
import StringIO
import json
import sipgate
sys.path.insert(1, os.path.join(sys.path[0], '..'))
from constants import *
def send_sms(recipient, content):
sg = sipgate.api(SIPGATE_USERNAME, SIPGATE_PASSWORD, 'transwhat')
default_uri = 'sip:NULL@sipgate.net'
for own_uri in sg.OwnUriListGet()['OwnUriList']:
if own_uri['DefaultUri']:
default_uri = own_uri['SipUri']
# SessionInitiate may return the following server status codes in case of errors: 501, 502, 506, 520, 525
return sg.SessionInitiate({'LocalUri': default_uri, 'RemoteUri': 'sip:%s@sipgate.de' % recipient, 'TOS': 'text', 'Content': content })
def main():
url = os.environ['SCRIPT_URI'] + '?' + os.environ['QUERY_STRING']
writer = StringIO.StringIO()
ch = pycurl.Curl()
ch.setopt(pycurl.URL, url)
ch.setopt(pycurl.USERAGENT, os.environ['HTTP_USER_AGENT'])
ch.setopt(pycurl.WRITEFUNCTION, writer.write)
ch.setopt(pycurl.SSL_VERIFYPEER, False)
ch.setopt(pycurl.HEADER, True)
ch.perform()
response = writer.getvalue()
headers, body = response.split("\r\n\r\n", 1)
headers = headers.split("\n")
preamble = headers.pop(0)
code = preamble.split(" ", 2)[1]
status = preamble.split(" ", 2)[2]
print "Status: %s %s" % (code, status)
for header in headers:
print header
print
print body
file = open(REQUESTS_FILE, "a")
file.write("\n--- Time: %s\n>>> Request: %s\n<<< Reponse Headers:\n%s\nResponse Body:\n%s\n" % (time.strftime("%a, %d %b %Y %H:%M:%S"), url, "\n".join(headers), body))
file.close()
# send password via sms to requester
if code == "200":
parsed = json.loads(body)
form = cgi.FieldStorage()
cc = form.getfirst("cc")
number = form.getfirst("in")
if parsed.has_key('pw') and parsed.has_key('login'):
send_sms(parsed['login'], parsed['pw'])
ch.close()
if __name__ == "__main__":
main()

View file

@ -1,63 +0,0 @@
<VirtualHost *:80>
ServerAdmin webmaster@0l.de
ServerName whatsapp.0l.de
DocumentRoot /home/stv0g/files/whatsapp/transwhat/cgi
<Directory />
Options FollowSymLinks
AllowOverride None
</Directory>
<Directory /home/stv0g/files/whatsapp/transwhat/cgi/>
Options Indexes FollowSymLinks MultiViews +ExecCGI
AllowOverride All
Order allow,deny
allow from all
AddHandler cgi-script .py
</Directory>
ErrorLog /home/stv0g/files/whatsapp/htdocs/error.log
CustomLog /home/stv0g/files/whatsapp/htdocs/access.log combined
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerAdmin webmaster@0l.de
ServerName whatsapp.0l.de
ServerAlias v.whatsapp.net
DocumentRoot /home/stv0g/files/whatsapp/transwhat/cgi
<Directory />
Options FollowSymLinks
AllowOverride None
</Directory>
<Directory /home/stv0g/files/whatsapp/transwhat/cgi/>
Options Indexes FollowSymLinks MultiViews +ExecCGI
AllowOverride None
Order allow,deny
allow from all
AddHandler cgi-script .py
</Directory>
ErrorLog /home/stv0g/files/whatsapp/htdocs/error.log
CustomLog /home/stv0g/files/whatsapp/htdocs/access.log combined
LogLevel info
# debug, info, notice, warn, error, crit, alert, emerg.
# Rewrite
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*) /sniff.py/$1
# SSL
SSLEngine on
SSLCertificateFile /home/stv0g/files/whatsapp/htdocs/whatsapp.crt
SSLCertificateKeyFile /home/stv0g/files/whatsapp/htdocs/whatsapp.key
<FilesMatch "\.(cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
</VirtualHost>
</IfModule>

View file

@ -1,9 +1,11 @@
Welcome to transWhat! Welcome to transWhat!
===== NEWS ==== ===== NEWS ====
- 03.06.13 transWhat service is born - 03.06.13 transWhat service is born
- 10.06.13 added bot user to import contacts and adjust settings, see http://2p.0l.de
- 14.06.13 finally enable password sniffing, see http://2o.0l.de
- 18.06.13 major deployment of development version - 18.06.13 major deployment of development version
- 07.09.15 transWhat is alive again. Now running with new Yowsup 2 library
Type "\help" for a list of available commands. Type "\help" for a list of available commands.
Visit http://whatsapp.0l.de for the full documentation.
Visit http://github.com/stv0g/transwhat/ for more details.
Join xmpp://transwhat@conference.jabber.ccc.de to hangout and discuss.

View file

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

View file

@ -31,10 +31,4 @@ BASE_PATH = "/opt/transwhat"
TOKEN_FILE = BASE_PATH + "/logs/tokens" TOKEN_FILE = BASE_PATH + "/logs/tokens"
MOTD_FILE = BASE_PATH + "/conf/motd" MOTD_FILE = BASE_PATH + "/conf/motd"
REQUESTS_FILE = BASE_PATH + "/logs/requests" REQUESTS_FILE = BASE_PATH + "/logs/requests"
GOOGLE_CLIENT_ID = ""
GOOGLE_CLIENT_SECRET = ""
SIPGATE_USERNAME=""
SIPGATE_PASSWORD=""

View file

@ -1,69 +0,0 @@
__author__ = "Steffen Vogel"
__copyright__ = "Copyright 2013, Steffen Vogel"
__license__ = "GPLv3"
__maintainer__ = "Steffen Vogel"
__email__ = "post@steffenvogel.de"
__status__ = "Prototype"
"""
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 sys
import gdata.gauth
import gdata.contacts.client
import gdata.contacts.data
import atom.data
from constants import *
gdata.contacts.REL_MOBILE='http://schemas.google.com/g/2005#mobile'
class GoogleClient():
def __init__(self):
self.client = gdata.contacts.client.ContactsClient()
# self.token = gdata.gauth.OAuth2Token(
# client_id = GOOGLE_CLIENT_ID,
# client_secret = GOOGLE_CLIENT_SECRET,
# scope = 'https://www.google.com/m8/feeds/contacts',
# user_agent = 'whatTrans'
# )
def getTokenUrl(self, uri = 'urn:ietf:wg:oauth:2.0:oob'):
return self.token.generate_authorize_url(redirect_uri=uri)
def getContacts(self, request_token):
access_token = self.token.get_access_token(request_token)
self.token.authorize(self.client)
numbers = { }
feed = self.client.GetContacts()
while feed:
for i, entry in enumerate(feed.entry):
for number in entry.phone_number:
numbers[number.text] = entry.title.text
next = feed.GetNextLink()
if next:
feed = self.client.GetContacts(next.href)
else:
break
return numbers

View file

@ -27,27 +27,6 @@ import logging
import urllib import urllib
import time import time
from yowsup.stacks import YowStack
from yowsup.layers import YowLayerEvent, YowParallelLayer
from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback
from yowsup.layers.auth import (YowCryptLayer, YowAuthenticationProtocolLayer,
AuthError)
from yowsup.layers.protocol_iq import YowIqProtocolLayer
from yowsup.layers.protocol_groups import YowGroupsProtocolLayer
from yowsup.layers.coder import YowCoderLayer
from yowsup.layers.network import YowNetworkLayer
from yowsup.layers.protocol_messages import YowMessagesProtocolLayer
from yowsup.layers.protocol_media import YowMediaProtocolLayer
from yowsup.layers.stanzaregulator import YowStanzaRegulator
from yowsup.layers.protocol_receipts import YowReceiptProtocolLayer
from yowsup.layers.protocol_acks import YowAckProtocolLayer
from yowsup.layers.logger import YowLoggerLayer
from yowsup.common import YowConstants
from yowsup import env
from yowsup.layers.protocol_presence import *
from yowsup.layers.protocol_presence.protocolentities import *
from yowsup.layers.protocol_messages.protocolentities import TextMessageProtocolEntity
from yowsup.layers.protocol_chatstate.protocolentities import *
from Spectrum2 import protocol_pb2 from Spectrum2 import protocol_pb2
from buddy import BuddyList from buddy import BuddyList
@ -55,10 +34,12 @@ from threading import Timer
from group import Group from group import Group
from bot import Bot from bot import Bot
from constants import * from constants import *
from yowsupwrapper import YowsupApp
class Session(): class Session(YowsupApp):
def __init__(self, backend, user, legacyName, extra, db): def __init__(self, backend, user, legacyName, extra, db):
super(Session, self).__init__()
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
self.logger.info("Created: %s", legacyName) self.logger.info("Created: %s", legacyName)
@ -73,121 +54,428 @@ class Session():
self.groups = {} self.groups = {}
self.presenceRequested = [] self.presenceRequested = []
self.offlineQueue = [] self.offlineQueue = []
self.msgIDs = { }
self.groupOfflineQueue = { } self.groupOfflineQueue = { }
self.shouldBeConnected = False
self.timer = None self.timer = None
self.password = None self.password = None
self.initialized = False self.initialized = False
self.loggedin = False self.synced = False
self.buddies = BuddyList(self.legacyName, self.db)
self.bot = Bot(self) self.bot = Bot(self)
env.CURRENT_ENV = env.S40YowsupEnv()
layers = (SpectrumLayer,
YowParallelLayer((YowAuthenticationProtocolLayer,
YowMessagesProtocolLayer,
YowReceiptProtocolLayer,
YowAckProtocolLayer,
YowMediaProtocolLayer,
YowIqProtocolLayer,
YowGroupsProtocolLayer,
YowPresenceProtocolLayer)),
YowCoderLayer,
YowCryptLayer,
YowStanzaRegulator,
YowNetworkLayer
)
self.stack = YowStack(layers)
self.stack.broadcastEvent(
YowLayerEvent(SpectrumLayer.EVENT_START,
backend = self.backend,
user = self.user,
db = self.db,
legacyName = self.legacyName,
session = self
)
)
def __del__(self): # handleLogoutRequest def __del__(self): # handleLogoutRequest
self.logout() self.logout()
def call(self, method, **kwargs):
self.logger.debug("%s(%s)", method,
", ".join(str(k) + ': ' + str(v) for k, v in kwargs.items()))
self.stack.broadcastEvent(YowLayerEvent(method, **kwargs))
def logout(self): def logout(self):
self.loggedin = False self.logger.info("%s logged out", self.user)
self.stack.broadcastEvent(YowLayerEvent(YowNetworkLayer.EVENT_STATE_DISCONNECT)) super(Session, self).logout()
def login(self, password): def login(self, password):
self.stack.setProp(YowAuthenticationProtocolLayer.PROP_CREDENTIALS, self.logger.info("%s attempting login", self.user)
(self.legacyName, password))
self.stack.setProp(YowNetworkLayer.PROP_ENDPOINT,
YowConstants.ENDPOINTS[0])
self.stack.setProp(YowCoderLayer.PROP_DOMAIN,
YowConstants.DOMAIN)
self.stack.setProp(YowCoderLayer.PROP_RESOURCE,
env.CURRENT_ENV.getResource())
self.stack.setProp(YowIqProtocolLayer.PROP_PING_INTERVAL, 5)
self.loggedin = True
self.password = password self.password = password
try: self.shouldBeConncted = True
self.stack.broadcastEvent( super(Session, self).login(self.legacyName, self.password)
YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT))
except TypeError as e: # Occurs when password is not correctly formated def _shortenGroupId(self, gid):
self.logger.debug("Auth error -> user: %s; details: %s;", # FIXME: will have problems if number begins with 0
self.user, e) return '-'.join(hex(int(s))[2:] for s in gid.split('-'))
try:
self.stack.loop(timeout=0.5, discrete=0.5) def _lengthenGroupId(self, gid):
except AuthError as e: # For some reason Yowsup throws an exception # FIXME: will have problems if number begins with 0
self.logger.debug("Auth error -> user: %s; details: %s;", return '-'.join(str(int(s, 16)) for s in gid.split('-'))
self.user, e)
def updateRoomList(self): def updateRoomList(self):
rooms = [] rooms = []
for room, group in self.groups.iteritems(): for room, group in self.groups.iteritems():
rooms.append([room, group.subject]) rooms.append([self._shortenGroupId(room), group.subject])
self.logger.debug("Got rooms: %s", rooms)
self.backend.handleRoomList(rooms) self.backend.handleRoomList(rooms)
def updateRoster(self):
self.logger.debug("Update roster")
old = self.buddies.keys()
self.buddies.load()
new = self.buddies.keys()
contacts = new
if self.synced == False:
self.sendSync(contacts, delta = False, interactive = True)
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(add)))
for number in remove:
self.backend.handleBuddyChanged(self.user, number, "", [],
protocol_pb2.STATUS_NONE)
self.backend.handleBuddyRemoved(self.user, number)
self.unsubscribePresence(number)
for number in add:
buddy = self.buddies[number]
self.subscribePresence(number)
self.backend.handleBuddyChanged(self.user, number, buddy.nick,
buddy.groups, protocol_pb2.STATUS_NONE,
iconHash = buddy.image_hash if buddy.image_hash is not None else "")
#self.requestLastSeen(number, self._lastSeen)
def _updateGroups(self, response, request):
self.logger.debug('Received groups list %s', response)
groups = response.getGroups()
for group in groups:
room = group.getId()
owner = group.getOwner().split('@')[0]
subjectOwner = group.getSubjectOwner().split('@')[0]
subject = utils.softToUni(group.getSubject())
if room in self.groups:
oroom = self.groups[room]
oroom.owner = owner
oroom.subjectOwner = subjectOwner
oroom.subject = subject
else:
self.groups[room] = Group(room, owner, subject, subjectOwner)
self.joinRoom(self._shortenGroupId(room), self.user.split("@")[0])
self._addParticipantsToRoom(room, group.getParticipants())
if room in self.groupOfflineQueue:
while self.groupOfflineQueue[room]:
msg = self.groupOfflineQueue[room].pop(0)
self.backend.handleMessage(self.user, room, msg[1],
msg[0], "", msg[2])
self.logger.debug("Send queued group message to: %s %s %s",
msg[0],msg[1], msg[2])
self.updateRoomList()
def joinRoom(self, room, nick):
room = self._lengthenGroupId(room)
if room in self.groups:
self.logger.info("Joining room: %s room=%s, nick=%s",
self.legacyName, room, nick)
group = self.groups[room]
group.nick = nick
try:
ownerNick = self.buddies[group.subjectOwner].nick
except KeyError:
ownerNick = group.subjectOwner
self.backend.handleSubject(self.user, room, group.subject,
ownerNick)
self.backend.handleRoomNicknameChanged(self.user, room,
group.subject)
self._refreshParticipants(room)
else:
self.logger.warn("Room doesn't exist: %s", 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
buddyFull = 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
buddyFull = self.user
self.backend.handleParticipantChanged(
self.user, buddyFull, self._shortenGroupId(room), flags,
protocol_pb2.STATUS_ONLINE, buddy, nick)
def _addParticipantsToRoom(self, room, participants):
group = self.groups[room]
group.participants = participants
for jid, _type in participants.iteritems():
buddy = jid.split("@")[0]
buddyFull = buddy
self.logger.info("Added %s to room %s", buddy, room)
try:
nick = self.buddies[buddy].nick
except KeyError:
nick = buddy
buddyFull = buddy
if _type == 'admin':
flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
else:
flags = protocol_pb2.PARTICIPANT_FLAG_NONE
if buddy == self.legacyName:
nick = group.nick
flags = protocol_pb2.PARTICIPANT_FLAG_ME
buddyFull = self.user
self.backend.handleParticipantChanged(self.user, buddyFull,
self._shortenGroupId(room), flags, protocol_pb2.STATUS_ONLINE, buddy, nick)
def _lastSeen(self, number, seconds):
self.logger.debug("Last seen %s at %s seconds" % (number, str(seconds)))
if seconds < 60:
self.onPresenceAvailable(number)
else:
self.onPresenceUnavailable(number)
# Called by superclass
def onAuthSuccess(self, status, kind, creation,
expiration, props, nonce, t):
self.logger.info("Auth success: %s", self.user)
self.backend.handleConnected(self.user)
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name,
["Admin"], protocol_pb2.STATUS_ONLINE)
if self.initialized == False:
self.sendOfflineMessages()
self.bot.call("welcome")
self.initialized = True
self.sendPresence(True)
self.updateRoster()
self.logger.debug('Requesting groups list')
self.requestGroupsList(self._updateGroups)
# Called by superclass
def onAuthFailed(self, reason):
self.logger.info("Auth failed: %s (%s)", self.user, reason)
self.backend.handleDisconnected(self.user, 0, reason)
self.password = None
self.shouldBeConnected = False
# Called by superclass
def onDisconnect(self):
self.logger.debug('Disconnected')
self.backend.handleDisconnected(self.user, 0, 'Disconnected for unknown reasons')
# Called by superclass
def onReceipt(self, _id, _from, timestamp, type, participant, offline, items):
self.logger.debug("received receipt, sending ack: " +
' '.join(map(str, [_id, _from, timestamp,
type, participant, offline, items]))
)
try:
buddy = self.buddies[_from.split('@')[0]]
self.backend.handleBuddyChanged(self.user, buddy.number.number,
buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE)
except KeyError:
pass
# Called by superclass
def onAck(self, _id, _class, _from, timestamp):
self.logger.debug('received ack ' +
' '.join(map(str, [_id, _class, _from,timestamp,]))
)
# Called by superclass
def onTextMessage(self, _id, _from, to, notify, timestamp, participant,
offline, retry, body):
self.logger.debug('received TextMessage' +
' '.join(map(str, [
_id, _from, to, notify, timestamp,
participant, offline, retry, body
]))
)
buddy = _from.split('@')[0]
messageContent = utils.softToUni(body)
self.sendReceipt(_id, _from, None, participant)
self.logger.info("Message received from %s to %s: %s (at ts=%s)",
buddy, self.legacyName, messageContent, timestamp)
if participant is not None: # Group message
partname = participant.split('@')[0]
self.sendGroupMessageToXMPP(buddy, partname, messageContent,
timestamp)
else:
self.sendMessageToXMPP(buddy, messageContent, timestamp)
# isBroadcast always returns false, I'm not sure how to get a broadcast
# message.
#if messageEntity.isBroadcast():
# self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\
# buddy, self.legacyName, messageContent, timestamp)
# messageContent = "[Broadcast] " + messageContent
# Called by superclass
def onImage(self, image):
self.logger.debug('Received image message %s', str(image))
buddy = image._from.split('@')[0]
message = image.url + ' ' + image.caption
self.sendMessageToXMPP(buddy, message, image.timestamp)
self.sendReceipt(image._id, image._from, None, image.participant)
# Called by superclass
def onAudio(self, audio):
self.logger.debug('Received audio message %s', str(audio))
buddy = audio._from.split('@')[0]
message = audio.url
self.sendMessageToXMPP(buddy, message, audio.timestamp)
self.sendReceipt(audio._id, audio._from, None, audio.participant)
# Called by superclass
def onVideo(self, video):
self.logger.debug('Received video message %s', str(video))
buddy = video._from.split('@')[0]
message = video.url
self.sendMessageToXMPP(buddy, message, video.timestamp)
self.sendReceipt(video._id, video._from, None, video.participant)
def onLocation(self, location):
buddy = location._from.split('@')[0]
latitude = location.getLatitude()
longitude = location.getLongitude()
url = location.getLocationUrl()
self.logger.debug("Location received from %s: %s, %s",
buddy, latitude, longitude)
self.sendMessageToXMPP(buddy, url, location.timestamp)
self.sendMessageToXMPP(buddy, 'geo:' + latitude + ',' + longitude,
location.timestamp)
# Called by superclass
def onVCard(self, _id, _from, name, card_data, to, notify, timestamp, participant):
self.logger.debug('received VCard' +
' '.join(map(str, [
_id, _from, name, card_data, to, notify, timestamp, participant
]))
)
buddy = _from.split("@")[0]
self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)")
# self.sendMessageToXMPP(buddy, card_data)
self.transferFile(buddy, str(name), card_data)
self.sendReceipt(_id, _from, None, participant)
def transferFile(self, buddy, name, data):
# Not working
self.logger.debug('transfering file %s', name)
self.backend.handleFTStart(self.user, buddy, name, len(data))
self.backend.handleFTData(0, data)
self.backend.handleFTFinish(self.user, buddy, name, len(data), 0)
# Called by superclass
def onContactTyping(self, buddy):
self.logger.info("Started typing: %s", buddy)
if buddy != 'bot':
self.sendPresence(True)
self.backend.handleBuddyTyping(self.user, buddy)
if self.timer != None:
self.timer.cancel()
# Called by superclass
def onContactPaused(self, buddy):
self.logger.info("Paused typing: %s", buddy)
if buddy != 'bot':
self.backend.handleBuddyTyped(self.user, buddy)
self.timer = Timer(3, self.backend.handleBuddyStoppedTyping,
(self.user, buddy)).start()
def onPresenceReceived(self, _type, name, jid, lastseen):
self.logger.info("Presence received: %s %s %s %s", _type, name, jid, lastseen)
buddy = jid.split("@")[0]
# seems to be causing an error
# self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen))
if buddy in self.presenceRequested:
timestamp = time.localtime(time.time() - lastseen)
timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp)
self.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen)))
self.presenceRequested.remove(buddy)
if lastseen < 60:
self.onPresenceAvailable(buddy)
else:
self.onPresenceUnavailable(buddy)
def onPresenceAvailable(self, buddy):
try:
buddy = self.buddies[buddy]
self.logger.info("Is available: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number.number,
buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE)
except KeyError:
self.logger.error("Buddy not found: %s", buddy)
def onPresenceUnavailable(self, buddy):
try:
buddy = self.buddies[buddy]
self.logger.info("Is unavailable: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number.number,
buddy.nick, buddy.groups, protocol_pb2.STATUS_XA)
except KeyError:
self.logger.error("Buddy not found: %s", buddy)
# spectrum RequestMethods # spectrum RequestMethods
def sendTypingStarted(self, buddy): def sendTypingStarted(self, buddy):
if buddy != "bot": if buddy != "bot":
self.logger.info("Started typing: %s to %s", self.legacyName, buddy) self.logger.info("Started typing: %s to %s", self.legacyName, buddy)
self.call("typing_send", buddy = (buddy + "@s.whatsapp.net",)) self.sendTyping(buddy, True)
# If he is typing he is present
# I really don't know where else to put this.
# Ideally, this should be sent if the user is looking at his client
self.sendPresence(True)
def sendTypingStopped(self, buddy): def sendTypingStopped(self, buddy):
if buddy != "bot": if buddy != "bot":
self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy) self.logger.info("Stopped typing: %s to %s", self.legacyName, buddy)
self.call("typing_paused", buddy = (buddy + "@s.whatsapp.net",)) self.sendTyping(buddy, False)
def sendMessageToWA(self, sender, message): def sendMessageToWA(self, sender, message):
self.logger.info("Message sent from %s to %s: %s", self.legacyName, sender, message) self.logger.info("Message sent from %s to %s: %s",
self.legacyName, sender, message)
message = message.encode("utf-8") message = message.encode("utf-8")
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: if "/" in sender: # directed at single user
room, buddy = sender.split("/") room, nick = sender.split("/")
self.call("message_send", to = buddy + "@s.whatsapp.net", for buddy, buddy3 in self.buddies.iteritems():
message = message) self.logger.info("Group buddy=%s nick=%s", buddy,
buddy3.nick)
if buddy3.nick == nick:
nick = buddy
self.sendTextMessage(nick + '@s.whatsapp.net', message)
else: else:
room = sender room = sender
group = self.groups[room] try:
group = self.groups[self._lengthenGroupId(room)]
self.logger.info("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
)
except KeyError:
self.logger.error('Group not found: %s', room)
self.sendTextMessage(self._lengthenGroupId(room) + '@g.us', message)
self.backend.handleMessage(self.user, room, message, group.nick)
self.call("message_send", to = room + "@g.us", message = message)
else: # private msg else: # private msg
buddy = sender buddy = sender
if message == "\\lastseen": # if message == "\\lastseen":
self.presenceRequested.append(buddy) # self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",))
self.call("presence_request", buddy = (buddy + "@s.whatsapp.net",)) # else:
else: self.sendTextMessage(sender + '@s.whatsapp.net', message)
self.call("message_send", to=buddy + "@s.whatsapp.net", message=message)
def sendMessageToXMPP(self, buddy, messageContent, timestamp = ""): def sendMessageToXMPP(self, buddy, messageContent, timestamp = "", nickname = ""):
if timestamp: if timestamp:
timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp)) timestamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime(timestamp))
@ -202,34 +490,48 @@ class Session():
"", timestamp) "", timestamp)
def sendGroupMessageToXMPP(self, room, 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
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", buddy, room, messageContent) self.logger.debug("Group message queued from %s to %s: %s",
buddy, room, messageContent)
if room not in self.groupOfflineQueue: if room not in self.groupOfflineQueue:
self.groupOfflineQueue[room] = [ ] self.groupOfflineQueue[room] = [ ]
self.groupOfflineQueue[room].append((buddy, messageContent, timestamp)) self.groupOfflineQueue[room].append(
(buddy, messageContent, timestamp)
)
else: else:
self.logger.debug("Group message sent from %s to %s: %s", buddy, room, messageContent) self.logger.debug("Group message sent from %s (%s) to %s: %s",
self.backend.handleMessage(self.user, room, messageContent, buddy, "", timestamp) buddy, nick, room, messageContent)
self.backend.handleMessage(self.user, self._shortenGroupId(room),
messageContent, nick, "", timestamp)
def changeStatus(self, status): def changeStatus(self, status):
if status != self.status: if status != self.status:
self.logger.info("Status changed: %s", status) self.logger.info("Status changed: %s", status)
self.status = status self.status = status
if status == protocol_pb2.STATUS_ONLINE or status == protocol_pb2.STATUS_FFC: if status == protocol_pb2.STATUS_ONLINE \
self.call("presence_sendAvailable") or status == protocol_pb2.STATUS_FFC:
self.sendPresence(True)
else: else:
self.call("presence_sendUnavailable") self.sendPresence(False)
def changeStatusMessage(self, statusMessage): def changeStatusMessage(self, statusMessage):
if (statusMessage != self.statusMessage) or (self.initialized == False): if (statusMessage != self.statusMessage) or (self.initialized == False):
self.statusMessage = statusMessage self.statusMessage = statusMessage
self.call("profile_setStatus", message = statusMessage.encode("utf-8")) self.setStatus(statusMessage.encode('utf-8'))
self.logger.info("Status message changed: %s", statusMessage) self.logger.info("Status message changed: %s", statusMessage)
if self.initialized == False: if self.initialized == False:
@ -244,9 +546,9 @@ class Session():
self.backend.handleMessage(self.user, msg[0], msg[1], "", "", msg[2]) self.backend.handleMessage(self.user, msg[0], msg[1], "", "", msg[2])
# also for adding a new buddy # also for adding a new buddy
def updateBuddy(self, buddy, nick, groups): def updateBuddy(self, buddy, nick, groups, image_hash = None):
if buddy != "bot": if buddy != "bot":
self.buddies.update(buddy, nick, groups) self.buddies.update(buddy, nick, groups, image_hash)
self.updateRoster() self.updateRoster()
def removeBuddy(self, buddy): def removeBuddy(self, buddy):
@ -255,26 +557,21 @@ class Session():
self.buddies.remove(buddy) self.buddies.remove(buddy)
self.updateRoster() self.updateRoster()
def joinRoom(self, room, nick):
if room in self.groups:
group = self.groups[room]
self.logger.info("Joining room: %s room=%s, nick=%s", self.legacyName, room, nick) def requestVCard(self, buddy, ID):
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)
self.backend.handleVCard(self.user, ID, buddy, "", "", response.pictureData)
obuddy = self.buddies[buddy]
self.updateBuddy(buddy, obuddy.nick, obuddy.groups, image_hash)
group.nick = nick self.logger.debug('Requesting profile picture of %s', buddy)
self.requestProfilePicture(buddy, onSuccess = onSuccess)
self.call("group_getParticipants", (room + "@g.us",))
self.backend.handleSubject(self.user, room, group.subject, group.subjectOwner)
else:
self.logger.warn("Room doesn't exist: %s", room)
def onMediaReceived(self, messageId, jid, preview, url, size, receiptRequested, isBroadcast):
buddy = jid.split("@")[0]
self.logger.info("Media received from %s: %s", buddy, url)
self.sendMessageToXMPP(buddy, utils.shorten(url))
if receiptRequested: self.call("message_ack", (jid, messageId))
# Not used
def onLocationReceived(self, messageId, jid, name, preview, latitude, longitude, receiptRequested, isBroadcast): def onLocationReceived(self, messageId, jid, name, preview, latitude, longitude, receiptRequested, isBroadcast):
buddy = jid.split("@")[0] buddy = jid.split("@")[0]
self.logger.info("Location received from %s: %s, %s", buddy, latitude, longitude) self.logger.info("Location received from %s: %s, %s", buddy, latitude, longitude)
@ -283,70 +580,6 @@ class Session():
self.sendMessageToXMPP(buddy, utils.shorten(url)) self.sendMessageToXMPP(buddy, utils.shorten(url))
if receiptRequested: self.call("message_ack", (jid, messageId)) if receiptRequested: self.call("message_ack", (jid, messageId))
def onVcardReceived(self, messageId, jid, name, data, receiptRequested, isBroadcast): # TODO
buddy = jid.split("@")[0]
self.logger.info("VCard received from %s", buddy)
self.sendMessageToXMPP(buddy, "Received VCard (not implemented yet)")
if receiptRequested: self.call("message_ack", (jid, messageId))
def onContactTyping(self, jid):
buddy = jid.split("@")[0]
self.logger.info("Started typing: %s", buddy)
self.backend.handleBuddyTyping(self.user, buddy)
if self.timer != None:
self.timer.cancel()
def onContactPaused(self, jid):
buddy = jid.split("@")[0]
self.logger.info("Paused typing: %s", buddy)
self.backend.handleBuddyTyped(self.user, jid.split("@")[0])
self.timer = Timer(3, self.backend.handleBuddyStoppedTyping, (self.user, buddy)).start()
def onGroupGotInfo(self, gjid, owner, subject, subjectOwner, subjectTimestamp, creationTimestamp):
room = gjid.split("@")[0]
owner = owner.split("@")[0]
subjectOwner = subjectOwner.split("@")[0]
if room in self.groups:
room = self.groups[room]
room.owner = owner
room.subjectOwner = subjectOwner
room.subject = subject
else:
self.groups[room] = Group(room, owner, subject, subjectOwner)
self.updateRoomList()
def onGroupGotParticipants(self, gjid, jids):
room = gjid.split("@")[0]
group = self.groups[room]
for jid in jids:
buddy = jid.split("@")[0]
self.logger.info("Added %s to room %s", buddy, room)
if buddy == group.owner:
flags = protocol_pb2.PARTICIPANT_FLAG_MODERATOR
else:
flags = protocol_pb2.PARTICIPANT_FLAG_NONE
self.backend.handleParticipantChanged(self.user, buddy, room, flags, protocol_pb2.STATUS_ONLINE) # TODO check status
if room in self.groupOfflineQueue:
while self.groupOfflineQueue[room]:
msg = self.groupOfflineQueue[room].pop(0)
self.backend.handleMessage(self.user, room, msg[1], msg[0], "", msg[2])
self.logger.debug("Send queued group message to: %s %s %s", msg[0],msg[1], msg[2])
def onGroupMessageReceived(self, messageId, gjid, jid, messageContent, timestamp, receiptRequested, pushName):
buddy = jid.split("@")[0]
room = gjid.split("@")[0]
self.logger.info("Group message received in %s from %s: %s", room, buddy, messageContent)
self.sendGroupMessageToXMPP(room, buddy, utils.softToUni(messageContent), timestamp)
if receiptRequested: self.call("message_ack", (gjid, messageId))
def onGroupSubjectReceived(self, messageId, gjid, jid, subject, timestamp, receiptRequested): def onGroupSubjectReceived(self, messageId, gjid, jid, subject, timestamp, receiptRequested):
room = gjid.split("@")[0] room = gjid.split("@")[0]
@ -356,15 +589,6 @@ class Session():
if receiptRequested: self.call("subject_ack", (gjid, messageId)) if receiptRequested: self.call("subject_ack", (gjid, messageId))
# Yowsup Notifications # Yowsup Notifications
def onGroupParticipantAdded(self, gjid, jid, author, timestamp, messageId, receiptRequested):
room = gjid.split("@")[0]
buddy = jid.split("@")[0]
loggin.info("Added % to room %s", buddy, room)
self.backend.handleParticipantChanged(self.user, buddy, room, protocol_pb2.PARTICIPANT_FLAG_NONE, protocol_pb2.STATUS_ONLINE)
if receiptRequested: self.call("notification_ack", (gjid, messageId))
def onGroupParticipantRemoved(self, gjid, jid, author, timestamp, messageId, receiptRequested): def onGroupParticipantRemoved(self, gjid, jid, author, timestamp, messageId, receiptRequested):
room = gjid.split("@")[0] room = gjid.split("@")[0]
buddy = jid.split("@")[0] buddy = jid.split("@")[0]
@ -381,163 +605,3 @@ class Session():
def onGroupPictureUpdated(self, jid, author, timestamp, messageId, pictureId, receiptRequested): def onGroupPictureUpdated(self, jid, author, timestamp, messageId, pictureId, receiptRequested):
# TODO # TODO
if receiptRequested: self.call("notification_ack", (jid, messageId)) if receiptRequested: self.call("notification_ack", (jid, messageId))
class SpectrumLayer(YowInterfaceLayer):
EVENT_START = "transwhat.event.SpectrumLayer.start"
def onEvent(self, layerEvent):
# We cannot use __init__, since it can take no arguments
retval = False
if layerEvent.getName() == SpectrumLayer.EVENT_START:
self.logger = logging.getLogger(self.__class__.__name__)
self.backend = layerEvent.getArg("backend")
self.user = layerEvent.getArg("user")
self.legacyName = layerEvent.getArg("legacyName")
self.db = layerEvent.getArg("db")
self.session = layerEvent.getArg("session")
self.buddies = BuddyList(self.legacyName, self.db)
self.bot = Bot(self)
retval = True
elif layerEvent.getName() == YowNetworkLayer.EVENT_STATE_DISCONNECTED:
reason = layerEvent.getArg("reason")
self.logger.info("Disconnected: %s (%s)", self.user, reason)
if not self.session.loggedin or self.session.password is None:
self.backend.handleDisconnected(self.user, 0, reason)
else:
self.session.login(self.session.password)
elif layerEvent.getName() == 'presence_sendAvailable':
entity = AvailablePresenceProtocolEntity()
self.toLower(entity)
retval = True
elif layerEvent.getName() == 'presence_sendUnavailable':
entity = UnavailablePresenceProtocolEntity()
self.toLower(entity)
retval = True
elif layerEvent.getName() == 'profile_setStatus':
entity = PresenceProtocolEntity(name = layerEvent.getArg('message'))
self.toLower(entity)
retval = True
elif layerEvent.getName() == 'message_send':
to = layerEvent.getArg('to')
message = layerEvent.getArg('message')
messageEntity = TextMessageProtocolEntity(message, to = to)
self.toLower(messageEntity)
retval = True
elif layerEvent.getName() == 'typing_send':
buddy = layerEvent.getArg('buddy')
state = OutgoingChatstateProtocolEntity(
ChatstateProtocolEntity.STATE_TYPING, buddy
)
self.toLower(state)
retval = True
elif layerEvent.getName() == 'typing_paused':
buddy = layerEvent.getArg('buddy')
state = OutgoingChatstateProtocolEntity(
ChatstateProtocolEntity.STATE_PAUSE, buddy
)
self.toLower(state)
retval = True
elif layerEvent.getName() == 'presence_request':
buddy = layerEvent.getArg('buddy')
sub = SubscribePresenceProtocolEntity(buddy)
self.toLower(sub)
self.logger.debug("EVENT %s", layerEvent.getName())
return retval
@ProtocolEntityCallback("success")
def onAuthSuccess(self, entity):
self.logger.info("Auth success: %s", self.user)
self.backend.handleConnected(self.user)
self.backend.handleBuddyChanged(self.user, "bot", self.bot.name, ["Admin"], protocol_pb2.STATUS_ONLINE)
self.updateRoster()
@ProtocolEntityCallback("failure")
def onAuthFailed(self, entity):
self.logger.info("Auth failed: %s (%s)", self.user, entity.getReason())
self.backend.handleDisconnected(self.user, 0, entity.getReason())
self.session.password = None
def updateRoster(self):
self.logger.debug("Update roster")
old = self.buddies.keys()
self.buddies.load()
new = self.buddies.keys()
add = set(new) - set(old)
remove = set(old) - set(new)
self.logger.debug("Roster remove: %s", str(list(remove)))
self.logger.debug("Roster add: %s", str(list(add)))
for number in remove:
self.backend.handleBuddyChanged(self.user, number, "", [], protocol_pb2.STATUS_NONE)
self.backend.handleBuddyRemoved(self.user, number)
entity = UnsubscribePresenceProtocolEntity(number + "@s.whatsapp.net")
self.toLower(entity)
for number in add:
buddy = self.buddies[number]
entity = SubscribePresenceProtocolEntity(number + "@s.whatsapp.net")
self.toLower(entity)
@ProtocolEntityCallback("message")
def onMessageReceived(self, messageEntity):
buddy = messageEntity.getFrom().split('@')[0]
messageContent = utils.softToUni(messageEntity.getBody())
timestamp = messageEntity.getTimestamp()
if messageEntity.isBroadcast():
self.logger.info("Broadcast received from %s to %s: %s (at ts=%s)",\
buddy, self.legacyName, messageContent, timestamp)
messageContent = "[Broadcast] " + messageContent
else:
self.logger.info("Message received from %s to %s: %s (at ts=%s)",
buddy, self.legacyName, messageContent, timestamp)
self.session.sendMessageToXMPP(buddy, messageContent, timestamp)
# if receiptRequested: self.call("message_ack", (jid, messageId))
@ProtocolEntityCallback("presence")
def onPrecenceUpdated(self, presence):
jid = presence.getFrom()
lastseen = presence.getLast()
buddy = jid.split("@")[0]
self.logger.info("Lastseen: %s %s", buddy, utils.ago(lastseen))
if buddy in self.session.presenceRequested:
timestamp = time.localtime(time.time() - lastseen)
timestring = time.strftime("%a, %d %b %Y %H:%M:%S", timestamp)
self.session.sendMessageToXMPP(buddy, "%s (%s)" % (timestring, utils.ago(lastseen)))
self.session.presenceRequested.remove(buddy)
if lastseen < 60:
self.onPrecenceAvailable(jid)
else:
self.onPrecenceUnavailable(jid)
def onPrecenceAvailable(self, jid):
buddy = jid.split("@")[0]
try:
buddy = self.buddies[buddy]
self.logger.info("Is available: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_ONLINE)
except KeyError:
self.logger.error("Buddy not found: %s", buddy)
def onPrecenceUnavailable(self, jid):
buddy = jid.split("@")[0]
try:
buddy = self.buddies[buddy]
self.logger.info("Is unavailable: %s", buddy)
self.backend.handleBuddyChanged(self.user, buddy.number.number, buddy.nick, buddy.groups, protocol_pb2.STATUS_XA)
except KeyError:
self.logger.error("Buddy not found: %s", buddy)

View file

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

View file

@ -22,21 +22,9 @@ __status__ = "Prototype"
along with transWhat. If not, see <http://www.gnu.org/licenses/>. along with transWhat. If not, see <http://www.gnu.org/licenses/>.
""" """
import urllib
import json
import e4u import e4u
import base64 import base64
import hashlib
def shorten(url):
url = urllib.urlopen("http://d.0l.de/add.json?type=URL&rdata=%s" % urllib.quote(url))
response = url.read()
response = json.loads(response)
for entry in response:
if entry['type'] == 'success':
host = entry['data'][0]['host']
return "http://s.%s/%s" % (host['zone']['name'], host['punycode'])
def ago(secs): def ago(secs):
periods = ["second", "minute", "hour", "day", "week", "month", "year", "decade"] periods = ["second", "minute", "hour", "day", "week", "month", "year", "decade"]
@ -61,3 +49,6 @@ def softToUni(message):
def decodePassword(password): def decodePassword(password):
return base64.b64decode(bytes(password.encode("utf-8"))) return base64.b64decode(bytes(password.encode("utf-8")))
def sha1hash(data):
return hashlib.sha1(data).hexdigest()

View file

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

570
yowsupwrapper.py Normal file
View file

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