" + 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 |
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 |
--------------------------------------------------------------------------------