├── startapi.sh ├── requirements.txt ├── startservice.sh ├── startall.sh ├── env.list ├── serviceconfig.yml.sample ├── Dockerfile ├── serviceconfig.yml ├── docs └── send.yml ├── service.py ├── api.py ├── .gitignore ├── src ├── yowsupextension.py └── layer.py └── README.md /startapi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | python3 ./api.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nameko 2 | flask 3 | flasgger 4 | git+https://github.com/tgalal/yowsup@master 5 | pexpect -------------------------------------------------------------------------------- /startservice.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | nameko run --config ./serviceconfig.yml service 3 | #nameko run service -------------------------------------------------------------------------------- /startall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | /etc/init.d/rabbitmq-server start 3 | sleep 30 4 | sh ./startservice.sh & sh ./startapi.sh 5 | -------------------------------------------------------------------------------- /env.list: -------------------------------------------------------------------------------- 1 | USERNAME= 2 | PASSWORD= 3 | TOKEN_RESEND_MESSAGES= 4 | ENDPOINT_RESEND_MESSAGES= 5 | -------------------------------------------------------------------------------- /serviceconfig.yml.sample: -------------------------------------------------------------------------------- 1 | AMQP_URI: 'pyamqp://guest:guest@localhost' 2 | WEB_SERVER_ADDRESS: '0.0.0.0:8000' 3 | YOWSUP_USERNAME: '49XXXX' 4 | YOWSUP_PASSWORD: 'XXXX' 5 | TOKEN_RESEND_MESSAGES: 'quiero_que_me_token' 6 | ENDPOINT_RESEND_MESSAGES: 'http://localhost/loquesea' 7 | rpc_exchange: 'nameko-rpc' 8 | max_workers: 1 9 | parent_calls_tracked: 10 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | LABEL maintainer="gabriel.tandil@gmail.com" 3 | 4 | WORKDIR /app 5 | 6 | ADD . /app 7 | 8 | ENV DEBIAN_FRONTEND noninteractive 9 | 10 | RUN apt-get update 11 | RUN apt-get install -y apt-utils 12 | RUN apt-get install -y python3-pip python3-dev rabbitmq-server 13 | RUN pip3 install -r requirements.txt 14 | 15 | EXPOSE 80 16 | 17 | CMD ["sh","startall.sh"] 18 | -------------------------------------------------------------------------------- /serviceconfig.yml: -------------------------------------------------------------------------------- 1 | AMQP_URI: 'pyamqp://guest:guest@localhost' 2 | WEB_SERVER_ADDRESS: '0.0.0.0:8000' 3 | YOWSUP_USERNAME: !env_var '${USERNAME}' 4 | YOWSUP_PASSWORD: !env_var '${PASSWORD}' 5 | TOKEN_RESEND_MESSAGES: !env_var '${TOKEN_RESEND_MESSAGES}' 6 | ENDPOINT_RESEND_MESSAGES: !env_var '${ENDPOINT_RESEND_MESSAGES}' 7 | rpc_exchange: 'nameko-rpc' 8 | max_workers: 1 9 | parent_calls_tracked: 10 10 | -------------------------------------------------------------------------------- /docs/send.yml: -------------------------------------------------------------------------------- 1 | Micro Service Based Yowsup API 2 | This API is made with Flask, Flasgger and Nameko 3 | --- 4 | parameters: 5 | - name: body 6 | in: body 7 | required: true 8 | schema: 9 | id: data 10 | properties: 11 | type: 12 | type: string 13 | enum: 14 | - simple 15 | - image 16 | default: 'simple' 17 | body: 18 | type: string 19 | default: 'This is a test Message' 20 | address: 21 | type: string 22 | default: '490176xxxxxx' 23 | 24 | responses: 25 | 200: 26 | description: It will send the message trough Whatsapp -------------------------------------------------------------------------------- /service.py: -------------------------------------------------------------------------------- 1 | from nameko.rpc import rpc 2 | import logging 3 | 4 | from pprint import pprint 5 | from src.yowsupextension import YowsupExtension 6 | from nameko.timer import timer 7 | 8 | class yowsup(object): 9 | name = "yowsup" 10 | 11 | y = YowsupExtension() 12 | 13 | @rpc 14 | def send(self, type, body, address): 15 | logging.info('Get message: %s,%s,%s' % (type, body, address)) 16 | output = self.y.sendTextMessage(address, body) 17 | 18 | 19 | return True 20 | #pprint(self) 21 | #logging.info(self.y) 22 | #output = self.y.sendCommand('Test') 23 | #logging.info(output) 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from flasgger import Swagger 3 | from nameko.standalone.rpc import ClusterRpcProxy 4 | from flasgger.utils import swag_from 5 | import logging 6 | import os 7 | 8 | app = Flask(__name__) 9 | Swagger(app) 10 | 11 | CONFIG = {'AMQP_URI': "pyamqp://guest:guest@localhost"} 12 | 13 | @app.route('/send', methods=['POST']) 14 | @swag_from('docs/send.yml') 15 | def send(): 16 | logger = app.logger 17 | type = request.json.get('type') 18 | body = request.json.get('body') 19 | address = request.json.get('address') 20 | logger.info('Get message: %s,%s,%s' % (type,body,address)) 21 | 22 | with ClusterRpcProxy(CONFIG) as rpc: 23 | # asynchronously spawning and email notification 24 | rpc.yowsup.send(type,body,address) 25 | 26 | msg = "The message was sucessfully sended to the queue" 27 | return msg, 200 28 | 29 | if __name__ == "__main__": 30 | app.run(host='0.0.0.0', port=80) 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/workspace.xml 8 | .idea/tasks.xml 9 | .idea/dictionaries 10 | .idea/vcs.xml 11 | .idea/jsLibraryMappings.xml 12 | 13 | # Sensitive or high-churn files: 14 | .idea/dataSources.ids 15 | .idea/dataSources.xml 16 | .idea/dataSources.local.xml 17 | .idea/sqlDataSources.xml 18 | .idea/dynamic.xml 19 | .idea/uiDesigner.xml 20 | 21 | # Gradle: 22 | .idea/gradle.xml 23 | .idea/libraries 24 | 25 | # Mongo Explorer plugin: 26 | .idea/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.iws 30 | 31 | ## Plugin-specific files: 32 | 33 | # IntelliJ 34 | /out/ 35 | 36 | # mpeltonen/sbt-idea plugin 37 | .idea_modules/ 38 | 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | fabric.properties 47 | 48 | .project 49 | .pydevproject 50 | __pycache__/ 51 | src/__pycache__/ 52 | -------------------------------------------------------------------------------- /src/yowsupextension.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | import pexpect 4 | import logging 5 | 6 | from nameko.extensions import DependencyProvider 7 | from yowsup.layers.network import YowNetworkLayer 8 | from yowsup.layers.protocol_media import YowMediaProtocolLayer 9 | from yowsup.layers import YowLayerEvent 10 | from yowsup.stacks import YowStackBuilder 11 | from yowsup.layers.auth import AuthError 12 | 13 | # from axolotl.duplicatemessagexception import DuplicateMessageException 14 | 15 | from src.layer import SendReciveLayer 16 | from yowsup.layers.axolotl.props import PROP_IDENTITY_AUTOTRUST 17 | 18 | class YowsupExtension(DependencyProvider): 19 | def setup(self): 20 | number = str(self.container.config['YOWSUP_USERNAME']) 21 | password = self.container.config['YOWSUP_PASSWORD'] 22 | self.output('Starting YowsUP...' + number + '.') 23 | 24 | tokenReSendMessage = self.container.config['TOKEN_RESEND_MESSAGES'] 25 | urlReSendMessage = self.container.config['ENDPOINT_RESEND_MESSAGES'] 26 | 27 | credentials = (number, password) # replace with your phone and password 28 | 29 | stackBuilder = YowStackBuilder() 30 | self.stack = stackBuilder \ 31 | .pushDefaultLayers(True) \ 32 | .push(SendReciveLayer(tokenReSendMessage,urlReSendMessage,number)) \ 33 | .build() 34 | 35 | 36 | self.stack.setCredentials(credentials) 37 | self.stack.setProp(PROP_IDENTITY_AUTOTRUST, True) 38 | #self.stack.broadcastEvent(YowLayerEvent(YowsupCliLayer.EVENT_START)) 39 | 40 | 41 | 42 | connectEvent = YowLayerEvent(YowNetworkLayer.EVENT_STATE_CONNECT) 43 | self.stack.broadcastEvent(connectEvent) 44 | 45 | 46 | def startThread(): 47 | try: 48 | self.stack.loop(timeout=0.5, discrete=0.5) 49 | except AuthError as e: 50 | self.output("Auth Error, reason %s" % e) 51 | except ValueError as e: 52 | self.output(e); 53 | except KeyboardInterrupt: 54 | self.output("\nYowsdown KeyboardInterrupt") 55 | exit(0) 56 | except Exception as e: 57 | self.output(e) 58 | self.output("Whatsapp exited") 59 | exit(0) 60 | 61 | t1 = threading.Thread(target=startThread) 62 | t1.daemon = True 63 | t1.start() 64 | 65 | 66 | def sendTextMessage(self, address,message): 67 | self.output('Trying to send Message to %s:%s' % (address, message)) 68 | 69 | self.stack.broadcastEvent(YowLayerEvent(name=SendReciveLayer.EVENT_SEND_MESSAGE, msg=message, number=address)) 70 | return True 71 | 72 | def get_dependency(self, worker_ctx): 73 | return self 74 | 75 | def output(self, str): 76 | logging.info(str) 77 | pass 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEWS 20180327 2 | The **media** branch has image recibe (and some other media) capabilities. Also reconnect and minor imporovements. Thanks to all colaborators. 3 | 4 | ## Media version docker image 5 | gabrieltandil/yowsup-microservice:media 6 | 7 | # yowsup-microservice 8 | This Project provides a microservice which implements an interface to yowsup2. You can Send/Receive Whatsapp-Messages with any language of your choice. 9 | 10 | ### Prerequisites 11 | 12 | Install & Configure the yowsup2 CLI Demo. 13 | 14 | Use yowsup-cli to register a Number. 15 | 16 | ## Without Docker 17 | 18 | ### Installation (General) 19 | 20 | 1. Install rabbitmq 21 | 2. Install Flask,Nameko,Flasgger,pexpect 22 | 3. Install yowsup2 23 | 24 | ### Installation (on Ubuntu) 25 | 26 | ```bash 27 | # Install Python Stuff: 28 | sudo apt-get install python3-pip python3-dev 29 | pip3 install nameko 30 | pip3 install flask 31 | pip3 install flasgger 32 | pip3 install pexpect 33 | # git+https://github.com/tgalal/yowsup@master works fine 34 | pip3 install git+https://github.com/tgalal/yowsup@master 35 | 36 | # Install RabbitMQ 37 | apt-get install rabbitmq-server 38 | 39 | ``` 40 | 41 | 42 | ### Configuration 43 | 44 | rename *service.yml.sample* to *service.yml* and put your credentials into it. 45 | 46 | 47 | ### Usage 48 | 49 | Run the the Service with: 50 | ``` 51 | startservice.sh 52 | ``` 53 | 54 | Run the the Api with: 55 | ``` 56 | startapi.sh 57 | ``` 58 | #### Send 59 | Go to: 60 | http://127.0.0.1:5000/apidocs/index.html 61 | 62 | #### Recive 63 | Set the parameter ENDPOINT_RESEND_MESSAGES to the url of your application where you want the messages to be dispatched. 64 | The received messages will be sent to that url in JSON format. 65 | ``` 66 | {{"from":"{FROM}","to":"{TO}","time":"{TIME}","id":"{MESSAGE_ID}","message":"{MESSAGE}","type":"{TYPE}"}} 67 | ``` 68 | 69 | ### Example Messages for other Integrations: 70 | 71 | Have a look at swagger documentation. 72 | 73 | ### Debugging 74 | 75 | Run 76 | ``` 77 | nameko shell 78 | n.rpc.yowsup.send(type="simple", body="This is a test Message!", address="49XXXX") 79 | ``` 80 | 81 | ## Using Docker to run in a container 82 | 83 | This will automatically setup and run the main service and the API 84 | The service is exposed in port 80 85 | 86 | Change the following environment variables with your credentials (after registering on yowsup-cli) in *env.list* file. 87 | Or create a new env.list file 88 | 89 | ``` 90 | USERNAME= 91 | PASSWORD= 92 | TOKEN_RESEND_MESSAGES= 93 | ENDPOINT_RESEND_MESSAGES= 94 | ``` 95 | - *TOKEN_RESEND_MESSAGES: any value established by you that will be sent in the request to validate the identity.* 96 | - *ENDPOINT_RESEND_MESSAGES: the url where messages received from the whatsapp network will be sent in the specified json format.* 97 | 98 | Then run: 99 | 100 | ``` 101 | docker run --name --env-file env.list -p :80 gabrieltandil/yowsup-microservice:latest 102 | ``` 103 | 104 | And you're all set!! :D 105 | 106 | ### Build docker image 107 | 108 | ``` 109 | docker build -t yowsup-microservice:latest . 110 | ``` 111 | -------------------------------------------------------------------------------- /src/layer.py: -------------------------------------------------------------------------------- 1 | from yowsup.layers.interface import YowInterfaceLayer, ProtocolEntityCallback 2 | from yowsup.layers.auth import YowAuthenticationProtocolLayer 3 | from yowsup.layers import YowLayerEvent, EventCallback 4 | from yowsup.layers.network import YowNetworkLayer 5 | import sys 6 | from yowsup.common import YowConstants 7 | import datetime 8 | import os 9 | import logging 10 | from yowsup.layers.protocol_groups.protocolentities import * 11 | from yowsup.layers.protocol_presence.protocolentities import * 12 | from yowsup.layers.protocol_messages.protocolentities import * 13 | from yowsup.layers.protocol_ib.protocolentities import * 14 | from yowsup.layers.protocol_iq.protocolentities import * 15 | from yowsup.layers.protocol_contacts.protocolentities import * 16 | from yowsup.layers.protocol_chatstate.protocolentities import * 17 | from yowsup.layers.protocol_privacy.protocolentities import * 18 | from yowsup.layers.protocol_media.protocolentities import * 19 | from yowsup.layers.protocol_media.mediauploader import MediaUploader 20 | from yowsup.layers.protocol_profiles.protocolentities import * 21 | from yowsup.common.tools import Jid 22 | from yowsup.common.optionalmodules import PILOptionalModule, AxolotlOptionalModule 23 | import urllib.request 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | class SendReciveLayer(YowInterfaceLayer): 29 | 30 | 31 | MESSAGE_FORMAT = "{{\"from\":\"{FROM}\",\"to\":\"{TO}\",\"time\":\"{TIME}\",\"id\":\"{MESSAGE_ID}\",\"message\":\"{MESSAGE}\",\"type\":\"{TYPE}\"}}" 32 | 33 | DISCONNECT_ACTION_PROMPT = 0 34 | 35 | EVENT_SEND_MESSAGE = "org.openwhatsapp.yowsup.prop.queue.sendmessage" 36 | 37 | def __init__(self,tokenReSendMessage,urlReSendMessage,myNumber): 38 | super(SendReciveLayer, self).__init__() 39 | YowInterfaceLayer.__init__(self) 40 | self.accountDelWarnings = 0 41 | self.connected = False 42 | self.username = None 43 | self.sendReceipts = True 44 | self.sendRead = True 45 | self.disconnectAction = self.__class__.DISCONNECT_ACTION_PROMPT 46 | self.myNumber=myNumber 47 | self.credentials = None 48 | 49 | self.tokenReSendMessage=tokenReSendMessage 50 | self.urlReSendMessage=urlReSendMessage 51 | 52 | # add aliases to make it user to use commands. for example you can then do: 53 | # /message send foobar "HI" 54 | # and then it will get automaticlaly mapped to foobar's jid 55 | self.jidAliases = { 56 | # "NAME": "PHONE@s.whatsapp.net" 57 | } 58 | 59 | def aliasToJid(self, calias): 60 | 61 | jid = "%s@s.whatsapp.net" % calias 62 | return jid 63 | 64 | def jidToAlias(self, jid): 65 | for alias, ajid in self.jidAliases.items(): 66 | if ajid == jid: 67 | return alias 68 | return jid 69 | 70 | def setCredentials(self, username, password): 71 | self.getLayerInterface(YowAuthenticationProtocolLayer).setCredentials(username, password) 72 | 73 | return "%s@s.whatsapp.net" % username 74 | 75 | @EventCallback(YowNetworkLayer.EVENT_STATE_DISCONNECTED) 76 | def onStateDisconnected(self, layerEvent): 77 | self.output("Disconnected: %s" % layerEvent.getArg("reason")) 78 | if self.disconnectAction == self.__class__.DISCONNECT_ACTION_PROMPT: 79 | self.connected = False 80 | # self.notifyInputThread() 81 | else: 82 | os._exit(os.EX_OK) 83 | 84 | def assertConnected(self): 85 | if self.connected: 86 | return True 87 | else: 88 | self.output("Not connected", tag="Error", prompt=False) 89 | return False 90 | 91 | 92 | @ProtocolEntityCallback("chatstate") 93 | def onChatstate(self, entity): 94 | print(entity) 95 | 96 | @ProtocolEntityCallback("iq") 97 | def onIq(self, entity): 98 | print(entity) 99 | 100 | @ProtocolEntityCallback("receipt") 101 | def onReceipt(self, entity): 102 | self.toLower(entity.ack()) 103 | 104 | @ProtocolEntityCallback("ack") 105 | def onAck(self, entity): 106 | # formattedDate = datetime.datetime.fromtimestamp(self.sentCache[entity.getId()][0]).strftime('%d-%m-%Y %H:%M') 107 | # print("%s [%s]:%s"%(self.username, formattedDate, self.sentCache[entity.getId()][1])) 108 | if entity.getClass() == "message": 109 | self.output(entity.getId(), tag="Sent") 110 | # self.notifyInputThread() 111 | 112 | @ProtocolEntityCallback("success") 113 | def onSuccess(self, entity): 114 | self.connected = True 115 | self.output("Logged in!", "Auth", prompt=False) 116 | # self.notifyInputThread() 117 | 118 | @ProtocolEntityCallback("failure") 119 | def onFailure(self, entity): 120 | self.connected = False 121 | self.output("Login Failed, reason: %s" % entity.getReason(), prompt=False) 122 | 123 | @ProtocolEntityCallback("notification") 124 | def onNotification(self, notification): 125 | notificationData = notification.__str__() 126 | if notificationData: 127 | self.output(notificationData, tag="Notification") 128 | else: 129 | self.output("From :%s, Type: %s" % (self.jidToAlias(notification.getFrom()), notification.getType()), 130 | tag="Notification") 131 | if self.sendReceipts: 132 | self.toLower(notification.ack()) 133 | 134 | @ProtocolEntityCallback("message") 135 | def onMessage(self, message): 136 | 137 | messageOut = "" 138 | if message.getType() == "text": 139 | messageOut = self.getTextMessageBody(message) 140 | elif message.getType() == "media": 141 | messageOut = self.getMediaMessageBody(message) 142 | else: 143 | messageOut = "Unknown message type %s " % message.getType() 144 | 145 | formattedDate = datetime.datetime.fromtimestamp(message.getTimestamp()).strftime('%Y-%m-%d %H:%M:%S') 146 | sender = message.getFrom() if not message.isGroupMessage() else "%s/%s" % ( 147 | message.getParticipant(False), message.getFrom()) 148 | 149 | # convert message to json 150 | output = self.__class__.MESSAGE_FORMAT.format( 151 | FROM=sender, 152 | TO=self.myNumber, 153 | TIME=formattedDate, 154 | MESSAGE=messageOut.encode('utf8').decode() if sys.version_info >= (3, 0) else messageOut, 155 | MESSAGE_ID=message.getId(), 156 | TYPE=message.getType() 157 | ) 158 | 159 | req = urllib.request.Request(self.urlReSendMessage) 160 | req.add_header('Content-Type', 'application/json; charset=utf-8') 161 | 162 | jsondataasbytes = output.encode('utf-8') # needs to be bytes 163 | req.add_header('Content-Length', len(jsondataasbytes)) 164 | req.add_header('TOKEN', self.tokenReSendMessage ) 165 | 166 | # resend message to url from configuration 167 | try: 168 | response = urllib.request.urlopen(req, jsondataasbytes) 169 | self.output(response.info()) 170 | except Exception as e: 171 | self.output(e) 172 | 173 | self.output(output, tag=None, prompt=not self.sendReceipts) 174 | 175 | if self.sendReceipts: 176 | self.toLower(message.ack(self.sendRead)) 177 | self.output("Sent delivered receipt" + " and Read" if self.sendRead else "", 178 | tag="Message %s" % message.getId()) 179 | 180 | 181 | @EventCallback(EVENT_SEND_MESSAGE) 182 | def doSendMesage(self, layerEvent): 183 | content = layerEvent.getArg("msg") 184 | number = layerEvent.getArg("number") 185 | self.output("Send Message to %s : %s" % (number, content)) 186 | jid = number 187 | 188 | if self.assertConnected(): 189 | outgoingMessage = TextMessageProtocolEntity( 190 | content.encode("utf-8") if sys.version_info >= (3, 0) else content, to=self.aliasToJid(number)) 191 | self.toLower(outgoingMessage) 192 | 193 | def getTextMessageBody(self, message): 194 | return message.getBody() 195 | 196 | def getMediaMessageBody(self, message): 197 | if message.getMediaType() in ("image", "audio", "video"): 198 | return self.getDownloadableMediaMessageBody(message) 199 | else: 200 | return "[Media Type: %s]" % message.getMediaType() 201 | 202 | def getDownloadableMediaMessageBody(self, message): 203 | return "[Media Type:{media_type}, Size:{media_size}, URL:{media_url}]".format( 204 | media_type=message.getMediaType(), 205 | media_size=message.getMediaSize(), 206 | media_url=message.getMediaUrl() 207 | ) 208 | 209 | ########### callbacks ############ 210 | 211 | def __str__(self): 212 | return "Send Recive Interface Layer" 213 | 214 | def output(self, str, tag="", prompt=""): 215 | logging.info(str) 216 | pass 217 | --------------------------------------------------------------------------------