Skip to content

Commit 7fdb5f2

Browse files
authored
Merge pull request #60 from jitendra-ky/56-add-web-socket-for-real-time-chat
56-add-web-socket-for-real-time-chat
2 parents 9d808d3 + 3ce9636 commit 7fdb5f2

File tree

9 files changed

+212
-20
lines changed

9 files changed

+212
-20
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Deploy Tornado WebSocket Server
2+
3+
on:
4+
push:
5+
branches:
6+
- 'main'
7+
workflow_dispatch:
8+
9+
jobs:
10+
build-and-deploy:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
# Step 1: Checkout the repository
15+
- name: Checkout repository
16+
uses: actions/checkout@v3
17+
18+
# Step 2: Set up Docker Buildx for building images
19+
- name: Set up Docker Buildx
20+
uses: docker/setup-buildx-action@v2
21+
22+
# Step 3: Log in to Docker Hub using credentials stored in GitHub secrets
23+
- name: Log in to Docker Hub
24+
uses: docker/login-action@v2
25+
with:
26+
username: ${{ secrets.DOCKER_HUB_USERNAME }}
27+
password: ${{ secrets.DOCKER_HUB_TOKEN }}
28+
29+
# Step 4: Build and tag the Docker image
30+
- name: Build and tag Docker image
31+
run: |
32+
docker build -f ./tools/dockerfiles/Dockerfile.tornado -t jitenky/dragonfly-tornado:latest .
33+
34+
# Step 5: Push the Docker image to Docker Hub
35+
- name: Push Docker image to Docker Hub
36+
run: |
37+
docker push jitenky/dragonfly-tornado:latest
38+
39+
- name: Log in to Azure
40+
uses: azure/login@v1
41+
with:
42+
creds: ${{ secrets.AZURE_CREDENTIALS }}
43+
44+
- name: Restart Azure App Service
45+
run: |
46+
az webapp restart --name dragonfly-tornado-2 --resource-group dragonfly

requirements/development.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ ruff==0.9.6
88
selenium==4.29.0
99
geckodriver-autoinstaller==0.1.0
1010
psycopg2-binary==2.9.10
11+
tornado==6.4.2

requirements/production.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ google-auth-httplib2==0.2.0
77
psycopg2==2.9.10
88
gunicorn==21.2.0
99
whitenoise==6.9.0
10+
tornado==6.4.2

static/js/home.js

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,75 @@
11
import { getCookie } from './assets.js'
22
import * as app_states from './app_states.js'
33

4+
let ws
5+
6+
function connectWebSocket() {
7+
// Create a new WebSocket connection to the Tornado server with user ID
8+
const userId = app_states.userId
9+
10+
// Create WebSocket connection
11+
const wsHost = env_var.TORNADO_HOSTNAME
12+
ws = new WebSocket(`${wsHost}/ws/chat?user_id=${userId}`)
13+
14+
ws.onopen = function () {
15+
console.log('WebSocket connection opened')
16+
}
17+
18+
ws.onmessage = function (event) {
19+
// Handle incoming messages from the WebSocket server
20+
const message = JSON.parse(event.data)
21+
console.log('Received message:', message)
22+
// Update the chat UI with the new message
23+
if (
24+
message.sender.toString() === app_states.selectedContactId.toString() &&
25+
message.receiver.toString() === app_states.userId.toString()
26+
) {
27+
console.log('Received message:', message)
28+
app_states.messages.push(message)
29+
rerender_msg_view()
30+
}
31+
}
32+
33+
ws.onclose = function () {
34+
console.log('WebSocket connection closed')
35+
}
36+
37+
ws.onerror = function (error) {
38+
console.log('WebSocket error:', error)
39+
}
40+
}
41+
42+
function sendMessageToWebSocket(message) {
43+
// Send a message to the WebSocket server
44+
if (ws && ws.readyState === WebSocket.OPEN) {
45+
ws.send(JSON.stringify(message))
46+
} else {
47+
console.log('WebSocket is not open')
48+
}
49+
}
50+
451
function rerender_msg_view() {
52+
// rerender the mes_view using the app_state message list
53+
const msgList = app_states.messages
54+
const chatBody = $('.chat-body')
55+
chatBody.empty()
56+
msgList.forEach((message) => {
57+
const messageElement = $('<div>').addClass(
58+
message.sender === app_states.userId
59+
? 'chat-message chat-message-sent'
60+
: 'chat-message chat-message-received'
61+
)
62+
const profile = $('<div>').addClass('chat-message-profile')
63+
const content = $('<div>').addClass('chat-message-content')
64+
content.append($('<p>').text(message.content))
65+
messageElement.append(profile, content)
66+
chatBody.append(messageElement)
67+
})
68+
// scroll to bottom
69+
$('.chat-body').scrollTop($('.chat-body')[0].scrollHeight)
70+
}
71+
72+
function render_msg_view() {
573
// reads the app_state and rerenders the message view
674

775
// check if user is logged in
@@ -41,22 +109,7 @@ function rerender_msg_view() {
41109
console.log('Messages:', response)
42110
app_states.setMessages(response)
43111
// render the messages
44-
const chatBody = $('.chat-body')
45-
chatBody.empty()
46-
response.forEach((message) => {
47-
const messageElement = $('<div>').addClass(
48-
message.sender === app_states.userId
49-
? 'chat-message chat-message-sent'
50-
: 'chat-message chat-message-received'
51-
)
52-
const profile = $('<div>').addClass('chat-message-profile')
53-
const content = $('<div>').addClass('chat-message-content')
54-
content.append($('<p>').text(message.content))
55-
messageElement.append(profile, content)
56-
chatBody.append(messageElement)
57-
// scroll to bottom
58-
chatBody.scrollTop(chatBody[0].scrollHeight)
59-
})
112+
rerender_msg_view()
60113
},
61114
error: function (response) {
62115
console.log('Error:', response)
@@ -115,7 +168,7 @@ function onClickContact() {
115168
// on click of a contact, set the selectedContactId and rerender the message view
116169
const contactId = $(this).attr('id')
117170
app_states.setSelectedContactId(contactId)
118-
rerender_msg_view()
171+
render_msg_view()
119172
}
120173

121174
function onClickSend() {
@@ -135,11 +188,16 @@ function onClickSend() {
135188
},
136189
success: function (response) {
137190
console.log('Message sent:', response)
191+
// Create a message object and send it to the WebSocket server
192+
const wsMessage = {
193+
sender: app_states.userId,
194+
receiver: app_states.selectedContactId,
195+
content: message,
196+
}
197+
sendMessageToWebSocket(wsMessage)
138198
app_states.messages.push(response)
139-
rerender_msg_view()
199+
render_msg_view()
140200
$('#message-box').val('')
141-
// scroll to bottom
142-
$('.chat-body').scrollTop($('.chat-body')[0].scrollHeight)
143201
},
144202
error: function (response) {
145203
console.log('Error:', response)
@@ -271,6 +329,8 @@ $(function () {
271329

272330
rerender_contacts_view()
273331

332+
connectWebSocket()
333+
274334
$('#chat-msg-send-btn').on('click', onClickSend)
275335

276336
$('#new-message-btn').click(function () {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Use official Python image as a base
2+
FROM python:3.10
3+
4+
# Set environment variables
5+
ENV PYTHONUNBUFFERED=1
6+
7+
# Set the working directory inside the container
8+
WORKDIR /app
9+
10+
# Copy requirements and install dependencies
11+
COPY requirements/production.txt requirements.txt
12+
RUN pip install --no-cache-dir -r requirements.txt
13+
14+
# Copy the rest of the application code
15+
COPY . .
16+
17+
# Expose port 8888 for Tornado WebSocket server
18+
EXPOSE 8888
19+
20+
# Run the Tornado server
21+
CMD ["python", "zserver/tornado/server.py"]

zserver/tornado/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file is intentionally left blank to make this directory a package.

zserver/tornado/handlers.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import json
2+
3+
import tornado.websocket
4+
5+
6+
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
7+
# Dictionary to keep track of user connections
8+
connections = {}
9+
10+
def open(self) -> None:
11+
"""Open a new WebSocket connection."""
12+
user_id = self.get_argument("user_id")
13+
self.connections[user_id] = self
14+
print(f"WebSocket connection opened for user {user_id}")
15+
16+
def on_message(self, message: str) -> None:
17+
"""Receive a message from the client."""
18+
data = json.loads(message)
19+
sender = data.get("sender")
20+
receiver = data.get("receiver")
21+
content = data.get("content")
22+
23+
# Send the message to the intended recipient
24+
if receiver in self.connections:
25+
self.connections[receiver].write_message(json.dumps({
26+
"sender": sender,
27+
"receiver": receiver,
28+
"content": content,
29+
}))
30+
31+
def on_close(self) -> None:
32+
"""Close the WebSocket connection."""
33+
user_id = self.get_argument("user_id")
34+
if user_id in self.connections:
35+
del self.connections[user_id]
36+
print(f"WebSocket connection closed for user {user_id}")
37+
38+
def check_origin(self, origin: str) -> bool:
39+
"""Allow connections from any origin."""
40+
print(f"Origin: {origin}")
41+
return True

zserver/tornado/server.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import tornado.ioloop
2+
import tornado.web
3+
import tornado.websocket
4+
from handlers import ChatWebSocketHandler
5+
6+
7+
def make_app():
8+
"""Create a Tornado web application with WebSocket handler."""
9+
return tornado.web.Application([
10+
(r"/ws/chat", ChatWebSocketHandler), # Route for WebSocket connections
11+
])
12+
13+
if __name__ == "__main__":
14+
# Create the Tornado application
15+
app = make_app()
16+
# Listen on port 8888 for incoming connections
17+
app.listen(8888)
18+
print("Tornado server started on port 8888")
19+
# Start the Tornado I/O loop to handle events
20+
tornado.ioloop.IOLoop.current().start()

zserver/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ def get_env_var() -> dict[str, str]:
77
env_var = {}
88
env_var["GOOGLE_CLIENT_ID"] = os.getenv("GOOGLE_CLIENT_ID")
99
env_var["GOOGLE_REDIRECT_URI"] = os.getenv("GOOGLE_REDIRECT_URI")
10+
env_var["TORNADO_HOSTNAME"] = os.getenv("TORNADO_HOSTNAME")
1011
return env_var

0 commit comments

Comments
 (0)