├── client ├── chat │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── admin.py │ ├── apps.py │ ├── urls.py │ ├── views.py │ ├── static │ │ └── chat │ │ │ ├── custom.js │ │ │ └── style.css │ └── templates │ │ └── chat │ │ └── index.html ├── project │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── requirements.txt └── manage.py ├── backend ├── requirements.txt ├── package.json ├── serverless.yml ├── handler.py └── package-lock.json ├── README.md └── .gitignore /client/chat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/chat/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.9.121 2 | botocore==1.12.121 3 | PyJWT==1.7.1 4 | -------------------------------------------------------------------------------- /client/chat/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /client/chat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /client/chat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /client/chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | name = 'chat' 6 | -------------------------------------------------------------------------------- /client/chat/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path('', views.index, name="index") 6 | ] 7 | -------------------------------------------------------------------------------- /client/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.3.9 2 | chardet==3.0.4 3 | Django==2.1.7 4 | django-uniauth==1.0.4 5 | docutils==0.14 6 | idna==2.8 7 | jmespath==0.9.4 8 | lxml==4.3.2 9 | PyJWT==1.7.1 10 | python-cas==1.4.0 11 | python-dateutil==2.8.0 12 | pytz==2018.9 13 | requests==2.21.0 14 | s3transfer==0.2.0 15 | six==1.12.0 16 | urllib3==1.24.1 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-chat 2 | 3 | An example chat application that uses the Serverless Framework on top of AWS Lambda and API Gateway with WebSockets for the backend, along with a simple Django client. 4 | 5 | You can view a tutorial explaining how to create an app like this [here](https://medium.com/@ldgoodridge95/creating-a-chat-app-with-serverless-websockets-and-python-a-tutorial-54cbc432e4f). 6 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "NPM packages for the serverless backend app", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "serverless-python-requirements": "^4.3.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/chat/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from uniauth.decorators import login_required 3 | import jwt 4 | import os 5 | 6 | 7 | @login_required 8 | def index(request): 9 | token = jwt.encode({"username": request.user.username}, "FAKE_SECRET", 10 | algorithm="HS256").decode("utf-8") 11 | return render(request, "chat/index.html", 12 | {"endpoint": os.environ["WEBSOCKET_ENDPOINT"], "token": token}) 13 | 14 | -------------------------------------------------------------------------------- /client/project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /client/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Django stuff 26 | *.log 27 | 28 | # Node package 29 | node_modules 30 | 31 | # Serverless directories 32 | .serverless 33 | .requirements 34 | 35 | # Ignore local database file 36 | db.sqlite3 37 | 38 | # Ignore vagrant build files 39 | .vagrant/ 40 | 41 | # PyBuilder 42 | target/ 43 | 44 | # Testing Tools 45 | .coverage 46 | .tox/ 47 | 48 | # Temp Files 49 | .*.sw* 50 | 51 | # Other Files 52 | .DS_Store 53 | .idea 54 | todo.txt 55 | notes/ 56 | -------------------------------------------------------------------------------- /client/project/urls.py: -------------------------------------------------------------------------------- 1 | """project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('accounts/', include('uniauth.urls', namespace='uniauth')), 22 | path('', include("chat.urls")), 23 | ] 24 | -------------------------------------------------------------------------------- /client/chat/static/chat/custom.js: -------------------------------------------------------------------------------- 1 | /* custom.js */ 2 | 3 | var socket; 4 | 5 | // Connect to the WebSocket and setup listeners 6 | function setupWebSocket(endpoint, username, token) { 7 | socket = new ReconnectingWebSocket(endpoint + "?token=" + token); 8 | 9 | socket.onopen = function(event) { 10 | console.log("Socket is open!"); 11 | data = {"action": "getRecentMessages"}; 12 | socket.send(JSON.stringify(data)); 13 | }; 14 | 15 | socket.onmessage = function(message) { 16 | var data = JSON.parse(message.data); 17 | data["messages"].forEach(function(message) { 18 | if ($("#message-container").children(0).attr("id") == "empty-message") { 19 | $("#message-container").empty(); 20 | } 21 | if (message["username"] === username) { 22 | $("#message-container").append("
(You) " + message["content"]); 23 | } else { 24 | $("#message-container").append("
(" + message["username"] + ") " + message["content"]); 25 | } 26 | $("#message-container").children().last()[0].scrollIntoView(); 27 | }); 28 | }; 29 | } 30 | 31 | // Sends a message to the websocket using the text in the post bar 32 | function postMessage(token) { 33 | var content = $("#post-bar").val(); 34 | if (content !== "") { 35 | data = {"action": "sendMessage", "token": token, "content": content}; 36 | socket.send(JSON.stringify(data)); 37 | $("#post-bar").val(""); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/chat/static/chat/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #4c5667; 3 | height: 100%; 4 | } 5 | 6 | html { 7 | height: 100%; 8 | } 9 | 10 | .btn-primary { 11 | background-color: #039cfd; 12 | border-color: #039cfd; 13 | } 14 | 15 | #comments-icon { 16 | padding-right: 16px; 17 | } 18 | 19 | #footer-container { 20 | bottom: 0px; 21 | left: 50%; 22 | margin-left: -35%; 23 | position: absolute; 24 | width: 70% 25 | } 26 | 27 | #header-bar { 28 | background: #dcdfe2; 29 | padding: 2rem 1rem; 30 | } 31 | 32 | #header-bar h1 { 33 | display: inline-block; 34 | } 35 | 36 | #header-text-first { 37 | padding-right: 10px; 38 | } 39 | 40 | #main-container { 41 | height: calc(100% - 262px); 42 | padding-left: 15%; 43 | padding-right: 15%; 44 | width: 100%; 45 | } 46 | 47 | #main-wrapper { 48 | background: #f4f6fa; 49 | height: 100%; 50 | overflow: auto; 51 | } 52 | 53 | #main-wrapper .container { 54 | padding-bottom: 30px; 55 | } 56 | 57 | #message-container { 58 | display: block; 59 | height: 100%; 60 | overflow-y: auto; 61 | } 62 | 63 | #post-bar { 64 | flex: 1; 65 | width: 100%; 66 | } 67 | 68 | #post-btn { 69 | margin-left: 15px; 70 | } 71 | 72 | #post-container { 73 | background-color: #dcdfe2 74 | } 75 | 76 | #results p b { 77 | padding-right: 8px; 78 | } 79 | 80 | @media (max-width: 768px) { 81 | #search-actions { 82 | padding-top: 10px; 83 | padding-left: 46px; 84 | } 85 | } 86 | 87 | #search-actions button { 88 | margin-right: 5px; 89 | width: 80px; 90 | } 91 | -------------------------------------------------------------------------------- /backend/serverless.yml: -------------------------------------------------------------------------------- 1 | # Serverless configuration 2 | 3 | service: serverless-chat 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.7 8 | stage: dev 9 | websocketApiName: serverless-chat-api 10 | websocketApiRouteSelectionExpression: $request.body.action 11 | iamRoleStatements: 12 | - Effect: Allow 13 | Action: 14 | - "execute-api:ManageConnections" 15 | Resource: 16 | - "arn:aws:execute-api:*:*:**/@connections/*" 17 | - Effect: Allow 18 | Action: 19 | - "dynamodb:PutItem" 20 | - "dynamodb:GetItem" 21 | - "dynamodb:UpdateItem" 22 | - "dynamodb:DeleteItem" 23 | - "dynamodb:BatchGetItem" 24 | - "dynamodb:BatchWriteItem" 25 | - "dynamodb:Scan" 26 | - "dynamodb:Query" 27 | Resource: 28 | - "arn:aws:dynamodb:us-east-1:*:*" 29 | 30 | plugins: 31 | - serverless-python-requirements 32 | 33 | custom: 34 | pythonRequirements: 35 | dockerizePip: true 36 | noDeploy: [] 37 | 38 | functions: 39 | connectionManager: 40 | handler: handler.connection_manager 41 | events: 42 | - websocket: 43 | route: $connect 44 | - websocket: 45 | route: $disconnect 46 | defaultMessage: 47 | handler: handler.default_message 48 | events: 49 | - websocket: 50 | route: $default 51 | getRecentMessages: 52 | handler: handler.get_recent_messages 53 | events: 54 | - websocket: 55 | route: getRecentMessages 56 | sendMessage: 57 | handler: handler.send_message 58 | events: 59 | - websocket: 60 | route: sendMessage 61 | ping: 62 | handler: handler.ping 63 | events: 64 | - http: 65 | path: ping 66 | method: get 67 | 68 | -------------------------------------------------------------------------------- /client/chat/templates/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% load static %} 5 | 6 | 7 | Serverless Chat 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

Welcome to the

Serverless Chatroom

17 |

Catch up on the conversation below!

18 |
19 |
20 |
21 |
-- No messages --
22 |
23 |
24 | 31 |
32 | 33 | 34 | 35 | 36 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /client/project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 21 | 22 | # Definitely change these for real applications 23 | DEBUG = True 24 | SECRET_KEY = 'FAKE_SECRET' 25 | ALLOWED_HOSTS = ['*'] 26 | 27 | # Uniauth Settings 28 | LOGIN_URL = '/accounts/login/' 29 | UNIAUTH_FROM_EMAIL = 'uniauth@serverless-chat.com' 30 | UNIAUTH_LOGIN_REDIRECT_URL = '/' 31 | 32 | # Log emails to the console 33 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | 'django.contrib.admin', 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | 'chat', 45 | 'uniauth', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | AUTHENTICATION_BACKENDS = [ 59 | 'uniauth.backends.LinkedEmailBackend', 60 | 'uniauth.backends.CASBackend', 61 | ] 62 | 63 | ROOT_URLCONF = 'project.urls' 64 | 65 | TEMPLATES = [ 66 | { 67 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 68 | 'DIRS': [], 69 | 'APP_DIRS': True, 70 | 'OPTIONS': { 71 | 'context_processors': [ 72 | 'django.template.context_processors.debug', 73 | 'django.template.context_processors.request', 74 | 'django.contrib.auth.context_processors.auth', 75 | 'django.contrib.messages.context_processors.messages', 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | WSGI_APPLICATION = 'project.wsgi.application' 82 | 83 | 84 | # Database 85 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 86 | 87 | DATABASES = { 88 | 'default': { 89 | 'ENGINE': 'django.db.backends.sqlite3', 90 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 91 | } 92 | } 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 110 | }, 111 | ] 112 | 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 116 | 117 | LANGUAGE_CODE = 'en-us' 118 | 119 | TIME_ZONE = 'UTC' 120 | 121 | USE_I18N = True 122 | 123 | USE_L10N = True 124 | 125 | USE_TZ = True 126 | 127 | 128 | # Static files (CSS, JavaScript, Images) 129 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 130 | 131 | STATIC_URL = '/static/' 132 | -------------------------------------------------------------------------------- /backend/handler.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import jwt 4 | import logging 5 | import time 6 | 7 | logger = logging.getLogger("handler_logger") 8 | logger.setLevel(logging.DEBUG) 9 | 10 | dynamodb = boto3.resource("dynamodb") 11 | 12 | 13 | def _get_body(event): 14 | try: 15 | return json.loads(event.get("body", "")) 16 | except: 17 | logger.debug("event body could not be JSON decoded.") 18 | return {} 19 | 20 | 21 | def _get_response(status_code, body): 22 | if not isinstance(body, str): 23 | body = json.dumps(body) 24 | return {"statusCode": status_code, "body": body} 25 | 26 | 27 | def _send_to_connection(connection_id, data, event): 28 | gatewayapi = boto3.client("apigatewaymanagementapi", 29 | endpoint_url = "https://" + event["requestContext"]["domainName"] + 30 | "/" + event["requestContext"]["stage"]) 31 | return gatewayapi.post_to_connection(ConnectionId=connection_id, 32 | Data=json.dumps(data).encode('utf-8')) 33 | 34 | 35 | def connection_manager(event, context): 36 | """ 37 | Handles connecting and disconnecting for the Websocket. 38 | 39 | Connect verifes the passed in token, and if successful, 40 | adds the connectionID to the database. 41 | 42 | Disconnect removes the connectionID from the database. 43 | """ 44 | connectionID = event["requestContext"].get("connectionId") 45 | token = event.get("queryStringParameters", {}).get("token") 46 | 47 | if event["requestContext"]["eventType"] == "CONNECT": 48 | logger.info("Connect requested (CID: {}, Token: {})"\ 49 | .format(connectionID, token)) 50 | 51 | # Ensure connectionID and token are set 52 | if not connectionID: 53 | logger.error("Failed: connectionId value not set.") 54 | return _get_response(500, "connectionId value not set.") 55 | if not token: 56 | logger.debug("Failed: token query parameter not provided.") 57 | return _get_response(400, "token query parameter not provided.") 58 | 59 | # Verify the token 60 | try: 61 | payload = jwt.decode(token, "FAKE_SECRET", algorithms="HS256") 62 | logger.info("Verified JWT for '{}'".format(payload.get("username"))) 63 | except: 64 | logger.debug("Failed: Token verification failed.") 65 | return _get_response(400, "Token verification failed.") 66 | 67 | # Add connectionID to the database 68 | table = dynamodb.Table("serverless-chat_Connections") 69 | table.put_item(Item={"ConnectionID": connectionID}) 70 | return _get_response(200, "Connect successful.") 71 | 72 | elif event["requestContext"]["eventType"] == "DISCONNECT": 73 | logger.info("Disconnect requested (CID: {})".format(connectionID)) 74 | 75 | # Ensure connectionID is set 76 | if not connectionID: 77 | logger.error("Failed: connectionId value not set.") 78 | return _get_response(500, "connectionId value not set.") 79 | 80 | # Remove the connectionID from the database 81 | table = dynamodb.Table("serverless-chat_Connections") 82 | table.delete_item(Key={"ConnectionID": connectionID}) 83 | return _get_response(200, "Disconnect successful.") 84 | 85 | else: 86 | logger.error("Connection manager received unrecognized eventType '{}'"\ 87 | .format(event["requestContext"]["eventType"])) 88 | return _get_response(500, "Unrecognized eventType.") 89 | 90 | 91 | def default_message(event, context): 92 | """ 93 | Send back error when unrecognized WebSocket action is received. 94 | """ 95 | logger.info("Unrecognized WebSocket action received.") 96 | return _get_response(400, "Unrecognized WebSocket action.") 97 | 98 | 99 | def get_recent_messages(event, context): 100 | """ 101 | Return the 10 most recent chat messages. 102 | """ 103 | connectionID = event["requestContext"].get("connectionId") 104 | logger.info("Retrieving most recent messages for CID '{}'"\ 105 | .format(connectionID)) 106 | 107 | # Ensure connectionID is set 108 | if not connectionID: 109 | logger.error("Failed: connectionId value not set.") 110 | return _get_response(500, "connectionId value not set.") 111 | 112 | # Get the 10 most recent chat messages 113 | table = dynamodb.Table("serverless-chat_Messages") 114 | response = table.query(KeyConditionExpression="Room = :room", 115 | ExpressionAttributeValues={":room": "general"}, 116 | Limit=10, ScanIndexForward=False) 117 | items = response.get("Items", []) 118 | 119 | # Extract the relevant data and order chronologically 120 | messages = [{"username": x["Username"], "content": x["Content"]} 121 | for x in items] 122 | messages.reverse() 123 | 124 | # Send them to the client who asked for it 125 | data = {"messages": messages} 126 | _send_to_connection(connectionID, data, event) 127 | 128 | return _get_response(200, "Sent recent messages to '{}'."\ 129 | .format(connectionID)) 130 | 131 | 132 | def send_message(event, context): 133 | """ 134 | When a message is sent on the socket, verify the passed in token, 135 | and forward it to all connections if successful. 136 | """ 137 | logger.info("Message sent on WebSocket.") 138 | 139 | # Ensure all required fields were provided 140 | body = _get_body(event) 141 | if not isinstance(body, dict): 142 | logger.debug("Failed: message body not in dict format.") 143 | return _get_response(400, "Message body not in dict format.") 144 | for attribute in ["token", "content"]: 145 | if attribute not in body: 146 | logger.debug("Failed: '{}' not in message dict."\ 147 | .format(attribute)) 148 | return _get_response(400, "'{}' not in message dict"\ 149 | .format(attribute)) 150 | 151 | # Verify the token 152 | try: 153 | payload = jwt.decode(body["token"], "FAKE_SECRET", algorithms="HS256") 154 | username = payload.get("username") 155 | logger.info("Verified JWT for '{}'".format(username)) 156 | except: 157 | logger.debug("Failed: Token verification failed.") 158 | return _get_response(400, "Token verification failed.") 159 | 160 | # Get the next message index 161 | # (Note: there is technically a race condition where two 162 | # users post at the same time and use the same index, but 163 | # accounting for that is outside the scope of this project) 164 | table = dynamodb.Table("serverless-chat_Messages") 165 | response = table.query(KeyConditionExpression="Room = :room", 166 | ExpressionAttributeValues={":room": "general"}, 167 | Limit=1, ScanIndexForward=False) 168 | items = response.get("Items", []) 169 | index = items[0]["Index"] + 1 if len(items) > 0 else 0 170 | 171 | # Add the new message to the database 172 | timestamp = int(time.time()) 173 | content = body["content"] 174 | table.put_item(Item={"Room": "general", "Index": index, 175 | "Timestamp": timestamp, "Username": username, 176 | "Content": content}) 177 | 178 | # Get all current connections 179 | table = dynamodb.Table("serverless-chat_Connections") 180 | response = table.scan(ProjectionExpression="ConnectionID") 181 | items = response.get("Items", []) 182 | connections = [x["ConnectionID"] for x in items if "ConnectionID" in x] 183 | 184 | # Send the message data to all connections 185 | message = {"username": username, "content": content} 186 | logger.debug("Broadcasting message: {}".format(message)) 187 | data = {"messages": [message]} 188 | for connectionID in connections: 189 | _send_to_connection(connectionID, data, event) 190 | return _get_response(200, "Message sent to {} connections."\ 191 | .format(len(connections))) 192 | 193 | def ping(event, context): 194 | """ 195 | Sanity check endpoint that echoes back 'PONG' to the sender. 196 | """ 197 | logger.info("Ping requested.") 198 | return _get_response(200, "PONG!") 199 | 200 | -------------------------------------------------------------------------------- /backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "appdirectory": { 8 | "version": "0.1.0", 9 | "resolved": "https://registry.npmjs.org/appdirectory/-/appdirectory-0.1.0.tgz", 10 | "integrity": "sha1-62yBYyDnsqsW9e2ZfyjYIF31Y3U=" 11 | }, 12 | "array-filter": { 13 | "version": "0.0.1", 14 | "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", 15 | "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=" 16 | }, 17 | "array-map": { 18 | "version": "0.0.0", 19 | "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", 20 | "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=" 21 | }, 22 | "array-reduce": { 23 | "version": "0.0.0", 24 | "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", 25 | "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=" 26 | }, 27 | "balanced-match": { 28 | "version": "1.0.0", 29 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 30 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 31 | }, 32 | "bluebird": { 33 | "version": "3.5.3", 34 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", 35 | "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" 36 | }, 37 | "brace-expansion": { 38 | "version": "1.1.11", 39 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 40 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 41 | "requires": { 42 | "balanced-match": "1.0.0", 43 | "concat-map": "0.0.1" 44 | } 45 | }, 46 | "concat-map": { 47 | "version": "0.0.1", 48 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 49 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 50 | }, 51 | "core-util-is": { 52 | "version": "1.0.2", 53 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 54 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 55 | }, 56 | "fs-extra": { 57 | "version": "7.0.1", 58 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", 59 | "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", 60 | "requires": { 61 | "graceful-fs": "4.1.15", 62 | "jsonfile": "4.0.0", 63 | "universalify": "0.1.2" 64 | } 65 | }, 66 | "fs.realpath": { 67 | "version": "1.0.0", 68 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 69 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 70 | }, 71 | "glob": { 72 | "version": "7.1.3", 73 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 74 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 75 | "requires": { 76 | "fs.realpath": "1.0.0", 77 | "inflight": "1.0.6", 78 | "inherits": "2.0.3", 79 | "minimatch": "3.0.4", 80 | "once": "1.4.0", 81 | "path-is-absolute": "1.0.1" 82 | } 83 | }, 84 | "glob-all": { 85 | "version": "3.1.0", 86 | "resolved": "https://registry.npmjs.org/glob-all/-/glob-all-3.1.0.tgz", 87 | "integrity": "sha1-iRPd+17hrHgSZWJBsD1SF8ZLAqs=", 88 | "requires": { 89 | "glob": "7.1.3", 90 | "yargs": "1.2.6" 91 | } 92 | }, 93 | "graceful-fs": { 94 | "version": "4.1.15", 95 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", 96 | "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" 97 | }, 98 | "immediate": { 99 | "version": "3.0.6", 100 | "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", 101 | "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" 102 | }, 103 | "inflight": { 104 | "version": "1.0.6", 105 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 106 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 107 | "requires": { 108 | "once": "1.4.0", 109 | "wrappy": "1.0.2" 110 | } 111 | }, 112 | "inherits": { 113 | "version": "2.0.3", 114 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 115 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 116 | }, 117 | "is-wsl": { 118 | "version": "1.1.0", 119 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", 120 | "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" 121 | }, 122 | "isarray": { 123 | "version": "1.0.0", 124 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 125 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 126 | }, 127 | "jsonfile": { 128 | "version": "4.0.0", 129 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 130 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 131 | "requires": { 132 | "graceful-fs": "4.1.15" 133 | } 134 | }, 135 | "jsonify": { 136 | "version": "0.0.0", 137 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", 138 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" 139 | }, 140 | "jszip": { 141 | "version": "3.2.1", 142 | "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz", 143 | "integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==", 144 | "requires": { 145 | "lie": "3.3.0", 146 | "pako": "1.0.10", 147 | "readable-stream": "2.3.6", 148 | "set-immediate-shim": "1.0.1" 149 | } 150 | }, 151 | "lie": { 152 | "version": "3.3.0", 153 | "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", 154 | "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", 155 | "requires": { 156 | "immediate": "3.0.6" 157 | } 158 | }, 159 | "lodash.get": { 160 | "version": "4.4.2", 161 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 162 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" 163 | }, 164 | "lodash.set": { 165 | "version": "4.3.2", 166 | "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", 167 | "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" 168 | }, 169 | "lodash.uniqby": { 170 | "version": "4.7.0", 171 | "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", 172 | "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=" 173 | }, 174 | "lodash.values": { 175 | "version": "4.3.0", 176 | "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz", 177 | "integrity": "sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=" 178 | }, 179 | "md5-file": { 180 | "version": "4.0.0", 181 | "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-4.0.0.tgz", 182 | "integrity": "sha512-UC0qFwyAjn4YdPpKaDNw6gNxRf7Mcx7jC1UGCY4boCzgvU2Aoc1mOGzTtrjjLKhM5ivsnhoKpQVxKPp+1j1qwg==" 183 | }, 184 | "minimatch": { 185 | "version": "3.0.4", 186 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 187 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 188 | "requires": { 189 | "brace-expansion": "1.1.11" 190 | } 191 | }, 192 | "minimist": { 193 | "version": "0.1.0", 194 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz", 195 | "integrity": "sha1-md9lelJXTCHJBXSX33QnkLK0wN4=" 196 | }, 197 | "once": { 198 | "version": "1.4.0", 199 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 200 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 201 | "requires": { 202 | "wrappy": "1.0.2" 203 | } 204 | }, 205 | "pako": { 206 | "version": "1.0.10", 207 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", 208 | "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" 209 | }, 210 | "path-is-absolute": { 211 | "version": "1.0.1", 212 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 213 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 214 | }, 215 | "process-nextick-args": { 216 | "version": "2.0.0", 217 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 218 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 219 | }, 220 | "readable-stream": { 221 | "version": "2.3.6", 222 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 223 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 224 | "requires": { 225 | "core-util-is": "1.0.2", 226 | "inherits": "2.0.3", 227 | "isarray": "1.0.0", 228 | "process-nextick-args": "2.0.0", 229 | "safe-buffer": "5.1.2", 230 | "string_decoder": "1.1.1", 231 | "util-deprecate": "1.0.2" 232 | } 233 | }, 234 | "rimraf": { 235 | "version": "2.6.3", 236 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 237 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 238 | "requires": { 239 | "glob": "7.1.3" 240 | } 241 | }, 242 | "safe-buffer": { 243 | "version": "5.1.2", 244 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 245 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 246 | }, 247 | "serverless-python-requirements": { 248 | "version": "4.3.0", 249 | "resolved": "https://registry.npmjs.org/serverless-python-requirements/-/serverless-python-requirements-4.3.0.tgz", 250 | "integrity": "sha512-VyXdEKNxUWoQDbssWZeR5YMaTDf1U4CO3yJH2953Y2Rt8zD6hG+vpTkVR490/Ws1PQsBopWuFfgDcLyvAppaRg==", 251 | "requires": { 252 | "appdirectory": "0.1.0", 253 | "bluebird": "3.5.3", 254 | "fs-extra": "7.0.1", 255 | "glob-all": "3.1.0", 256 | "is-wsl": "1.1.0", 257 | "jszip": "3.2.1", 258 | "lodash.get": "4.4.2", 259 | "lodash.set": "4.3.2", 260 | "lodash.uniqby": "4.7.0", 261 | "lodash.values": "4.3.0", 262 | "md5-file": "4.0.0", 263 | "rimraf": "2.6.3", 264 | "shell-quote": "1.6.1" 265 | } 266 | }, 267 | "set-immediate-shim": { 268 | "version": "1.0.1", 269 | "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", 270 | "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" 271 | }, 272 | "shell-quote": { 273 | "version": "1.6.1", 274 | "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", 275 | "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", 276 | "requires": { 277 | "array-filter": "0.0.1", 278 | "array-map": "0.0.0", 279 | "array-reduce": "0.0.0", 280 | "jsonify": "0.0.0" 281 | } 282 | }, 283 | "string_decoder": { 284 | "version": "1.1.1", 285 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 286 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 287 | "requires": { 288 | "safe-buffer": "5.1.2" 289 | } 290 | }, 291 | "universalify": { 292 | "version": "0.1.2", 293 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 294 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" 295 | }, 296 | "util-deprecate": { 297 | "version": "1.0.2", 298 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 299 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 300 | }, 301 | "wrappy": { 302 | "version": "1.0.2", 303 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 304 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 305 | }, 306 | "yargs": { 307 | "version": "1.2.6", 308 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.2.6.tgz", 309 | "integrity": "sha1-nHtKgv1dWVsr8Xq23MQxNUMv40s=", 310 | "requires": { 311 | "minimist": "0.1.0" 312 | } 313 | } 314 | } 315 | } 316 | --------------------------------------------------------------------------------