Blog available for sell
This blog is available for sale. Please 'contact us' if interested.
Advertise with us
Django application to automate the WhatsApp messaging

In part 1 on this article, we tried automating the messaging sending via WhatsApp web using selenium. Drawback of the approach was that, a new instance of browser was created everytime we need to send the message. And we need to scan the QR code everytime new browser instance is created.

In this article we will fix the above issue. We will also develop a Django application to expose few  API endpoints. Then we will use those endpoints to send the WhatsApp message.


(1) Creating Django application

If this is the first time you are working with Django, please follow this article to setup and run simplest Django application. 

https://pythoncircle.com/post/26/hello-word-in-django-how-to-start-with-django/

Please note that for this article, I used Python 3.10 and Django 3.2. 


(2) Virtual Environment

Create a virtual environment using python 3.10 and install below dependencies in it. Save the below content in a file requirements.txt and run the command pip install -r requirements.txt after activating the virtual environment.

asgiref==3.6.0
async-generator==1.10
attrs==22.2.0
certifi==2022.12.7
charset-normalizer==2.1.1
Django==3.2.16
exceptiongroup==1.1.0
h11==0.14.0
idna==3.4
outcome==1.2.0
packaging==22.0
PySocks==1.7.1
python-dotenv==0.21.0
pytz==2022.7
requests==2.28.1
selenium==4.7.2
sniffio==1.3.0
sortedcontainers==2.4.0
sqlparse==0.4.3
tqdm==4.64.1
trio==0.22.0
trio-websocket==0.9.2
urllib3==1.26.13
webdriver-manager==3.8.5
wsproto==1.2.0


(2) URLs

Create a urls.py file in your app and add the below content to it.

Replace app1 with your app name.

from django.urls import path

from app1 import views
from django.urls import path

app_name = "app1"

urlpatterns = [
    path(r'send/', views.send, name='send'),
    path(r'instance/', views.create_instance, name='create_instance'),
    path(r'instance/<str:instance_id>/', views.delete_instance, name='delete_instance'),
]

Here first URL i.e. send/ is to send the WhatsApp messages using GET and POST methods both. Both methods are supported. Curl to send messages is below.
Second URL i.e. instance/ is used to create a new browser instance. You have to scan the QR code only once and you can re-use that instance everytime you need to send the WhatsApp message.
Third URL is to destroy/delete the instance.


(3) Project URLs

Link you app's URLs with Project URLs. Copy the below content in project's urls.py file

from django.urls import path, include

urlpatterns = [
    path(r'', include('app1.urls', namespace="app1")),
]


(4) Views

In views.py file, paste the below content. I will explain the code shortly.

import json
import logging
import os
import random
import string
import sys
import time
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import User
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from selenium import webdriver
from selenium.common.exceptions import (
    UnexpectedAlertPresentException,
    NoSuchElementException,
)
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait
from urllib.parse import quote
from webdriver_manager.chrome import ChromeDriverManager

BASE_URL = "https://web.whatsapp.com/"
CHAT_URL = "https://web.whatsapp.com/send?phone={phone}&text&type=phone_number&app_absent=1"
headers = {'content_type': 'application/json'}

# keep session_id and object reference of all open browsers
# implement LRU later
active_sessions = {

}


def create_instance(request):
    chrome_options = Options()
    chrome_options.add_argument("start-maximized")
    user_data_dir = ''.join(random.choices(string.ascii_letters, k=8))
    chrome_options.add_argument("--user-data-dir=/tmp/chrome-data/" + user_data_dir)
    chrome_options.add_argument("--incognito")

    browser = webdriver.Chrome(
        ChromeDriverManager().install(),
        options=chrome_options,
    )

    handles = browser.window_handles
    for _, handle in enumerate(handles):
        if handle != browser.current_window_handle:
            browser.switch_to.window(handle)
            browser.close()

    browser.get(BASE_URL)
    browser.maximize_window()
    # print(browser.session_id)
    # print(browser.__dict__)
    """
    Output:
    {'port': 0, 'vendor_prefix': 'goog', 'service': <selenium.webdriver.chrome.service.Service object at 0x7f909886db40>, 'command_executor': <selenium.webdriver.chromium.remote_connection.ChromiumRemoteConnection object at 0x7f90989d7c70>, '_is_remote': False, 'session_id': '3519b8069ac6cd1c15fa126c47659201', 'caps': {'acceptInsecureCerts': False, 'browserName': 'chrome', 'browserVersion': '107.0.5304.121', 'chrome': {'chromedriverVersion': '107.0.5304.62 (1eec40d3a5764881c92085aaee66d25075c159aa-refs/branch-heads/5304@{#942})', 'userDataDir': './User_Data'}, 'goog:chromeOptions': {'debuggerAddress': 'localhost:33499'}, 'networkConnectionEnabled': False, 'pageLoadStrategy': 'normal', 'platformName': 'linux', 'proxy': {}, 'setWindowRect': True, 'strictFileInteractability': False, 'timeouts': {'implicit': 0, 'pageLoad': 300000, 'script': 30000}, 'unhandledPromptBehavior': 'dismiss and notify', 'webauthn:extension:credBlob': True, 'webauthn:extension:largeBlob': True, 'webauthn:virtualAuthenticators': True}, 'pinned_scripts': {}, 'error_handler': <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7f90989d7640>, '_switch_to': <selenium.webdriver.remote.switch_to.SwitchTo object at 0x7f90989d7b80>, '_mobile': <selenium.webdriver.remote.mobile.Mobile object at 0x7f90989d7940>, '_file_detector': <selenium.webdriver.remote.file_detector.LocalFileDetector object at 0x7f90989d7550>, '_authenticator_id': None}
    """

    # this make sure we have a list of all active session.
    # also keeping a global reference of this session/browser stops it from closing.
    # otherwise browser will close automatically after this function exists
    session_id = browser.__dict__.get('session_id')
    print(session_id)
    active_sessions[session_id] = browser

    # browser is now ready with QR code.
    # let's wait someone to scan this QR in WA application and start a new session
    return JsonResponse({
        'instance': session_id
    })


@csrf_exempt
def delete_instance(request, instance_id):
    if request.method == 'DELETE':
        instance = active_sessions.pop(instance_id, None)
        logging.getLogger("info").info("instance {instance} deleted".format(instance=instance))
        response = {
            'status': 'success',
            'message': 'instance {instance} deleted successfully'.format(instance=instance_id)
        }
        return HttpResponse(json.dumps(response), headers=headers, status=200)
    else:
        return HttpResponse(status=405)


def send_message(data):
    instance = data.get('instance', '')
    instance = instance.strip()
    if not instance:
        print('no instance found')
        return HttpResponse(
            json.dumps({
                'message': 'no instance found'
            }), headers=headers, status=400
        )

    phone = data.get('phone', '')
    phone = phone.strip('+')
    if not phone:
        print('no phone found')
        return HttpResponse(
            json.dumps({
                'message': 'no phone found'
            }), headers=headers, status=400
        )

    message = data.get('message', '')
    message = message.strip()
    if not message:
        print('no message found')
        return HttpResponse(
            json.dumps({
                'message': 'no message found'
            }), headers=headers, status=400
        )

    browser = active_sessions.get(instance, None)
    if not browser:
        print('no active instance found')
        return HttpResponse(
            json.dumps({
                'message': f'no active instance with instance id {instance} found'.format(instance=instance)
            }), headers=headers, status=400
        )
    # open chat URL to this number and wait for window/chats to load
    browser.get(CHAT_URL.format(phone=phone))
    time.sleep(3)

    # find text input box
    inp_xpath = (
        '//*[@id="main"]/footer/div[1]/div/span[2]/div/div[2]/div[1]/div/div[1]'
    )
    input_box = WebDriverWait(browser, 60).until(
        expected_conditions.presence_of_element_located((By.XPATH, inp_xpath))
    )
    for line in message.split("\n"):
        input_box.send_keys(line)
        ActionChains(browser).key_down(Keys.SHIFT).key_down(
            Keys.ENTER
        ).key_up(Keys.ENTER).key_up(Keys.SHIFT).perform()
    input_box.send_keys(Keys.ENTER)

    logging.getLogger('info').info(f"Message sent successfully to {phone}")
    return HttpResponse(
        json.dumps({
            'status': 'success',
            'message': 'Message sent successfully to {phone}'.format(phone=phone)
        }),
        headers=headers,
        status=200
    )


@csrf_exempt
def send(request):
    if request.method == 'GET':
        print(request.GET)
        data = request.GET
        return send_message(data)

    if request.method == 'POST':
        data = request.body  # byte
        data = json.loads(data.decode('utf-8'))  # json/dict
        return send_message(data)

    else:
        return HttpResponse(status=405)


(5) Logging

We are logging the info and errors in log files. For this to work, we need to define the logging configuration. 

Create a file logger_settings.py parallel to settings.py file.

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'large': {
            'format': '%(asctime)s  %(levelname)s  %(process)d  %(pathname)s  ' +
                      '%(funcName)s  %(lineno)d  %(message)s  '
        },
        'small': {
            'format': '%(asctime)s  %(levelname)s  %(pathname)s  %(lineno)d  %(message)s  '
        },
        'tiny': {
            'format': '%(asctime)s  %(message)s  '
        }
    },
    'handlers': {
        'errors_file': {
            'level': 'ERROR',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'when': 'midnight',
            'interval': 1,
            'filename': os.path.join(BASE_DIR, 'logs/errors.log'),
            'formatter': 'large',
        },
        'warning_file': {
            'level': 'WARNING',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'when': 'midnight',
            'interval': 1,
            'filename': os.path.join(BASE_DIR, 'logs/warnings.log'),
            'formatter': 'large',
        },
        'info_file': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'when': 'midnight',
            'interval': 1,
            'filename': os.path.join(BASE_DIR, 'logs/info.log'),
            'formatter': 'large',
        },
    },
    'loggers': {
        'error': {
            'handlers': ['errors_file'],
            'level': 'WARNING',
            'propagate': False,
        },
        'warning': {
            'handlers': ['warning_file'],
            'level': 'WARNING',
            'propagate': False,
        },
        'info': {
            'handlers': ['info_file'],
            'level': 'INFO',
            'propagate': False,
        },

    },

}

Import logger_settings.py file in settings.py file.

try:
    from .logger_settings import *
except Exception as e:
    print("Unable to load logger settings")
    pass


(6) Running application

Django code is ready. Run the application using command python manage.py runserver. If everything is correct, you will see below messages on terminal.

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
January 03, 2023 - 12:49:05
Django version 3.2.16, using settings 'wa.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.


(7) Testing

You can check if application is working as expected or not by using below CURL requests. 
Open a new terminal and run this curl to create a new browser instance.

curl localhost:8000/instance/

This will start a new browser session in incognito mode and open the base URL of whatsapp web. A QR code will be shown. Scan this QR code in your WhatsApp app and link the devices. 

Once device is linked, you can send the messages.

This API will return the JSON response containing the instance Id. We will use this instance Id in next CURL.


(8) Sending message

Use below curl to send the Whatsapp message.

curl --location --request POST 'localhost:8000/send/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "phone": "Phone number with country code (without +)",
    "message": "Hello Https://PythonCirle.com",
    "instance": "Instance Id here"
}'


Replace the values in above curl with your phone number and instance Id.

You will see that the message is sent.


Note :-  Message is sent using your account. Abusing this process may result in blocking your account. Please use wisely.


(9) Understanding the code.

I have tried to write a self explanatory code with proper comments and messages. 

Create Instance method is trying to create a new browser instance. To keep the track of all active sessions so that we can re-use the once created session everytime a user wants to send the message with scanning QR code again and again, we will keep the reference of active session in a dictionary. Since the browser instance reference is still alive due to reference in dictionary, browser won't close automatically as it was happening in previous article.


Send method fetches the existing session from dictionary and uses it send the message.


Once you are done and no longer wants to send more messages, you can destroy the instance by calling destroy API as below.

curl -X DELETE localhost:8000/instance/<instance-id>/


These are REST APIs. APIs are returning JSON response. 

You can enhance this project as per your requirements.


In the next article we will host this application on a remote server like digitalocean or pythonanywhere so that application keep running 24 hours. 



Host your Django Application for free on PythonAnyWhere.
If you want complete control of your application and server, you should consider 
DigitalOcean. Create an account with this link and get $200 credits.



1 comment on 'Django Application To Automate The Whatsapp Messaging'
Login to comment

Mohammed Baashar Sept. 28, 2023, 2:39 p.m.
Hello, thanks tons for the perfect explanation and article; Is part III coming ? You don't have dates in the articles. Thanks again.

Related Articles:
Automating PDF generation using Python reportlab module
Generating PDF using python reportlab module, Adding table to PDF using Python, Adding Pie Chart to PDF using Python, Generating PDF invoice using Python code, Automating PDF generation using Python reportlab module...
Accessing Gmail Inbox using Python imaplib module
In this article, we are accessing Gmail inbox using IMAP library of python, We covered how to generate the password of an App to access the gmail inbox, how to read inbox and different category emails, how to read promotional email, forum email and updates, How to search in email in Spam folder, how to search an email by subject line, how to get header values of an email, how to check DKIM, SPF, and DMARC headers of an email...
Sending email with attachments using Python built-in email module
Sending email with attachments using Python built-in email module, adding image as attachment in email while sending using Python, Automating email sending process using Python, Automating email attachment using python...
Automating Facebook page posts using python script
posting on facebook page using python script, automating the facebook page posts, python code to post on facebook page, create facebook page post using python script...
DigitalOcean Referral Badge

© 2022-2023 Python Circle   Contact   Sponsor   Archive   Sitemap