├── .gitignore
├── Makefile
├── README.md
├── angular
├── .gitignore
├── Gruntfile.js
├── README.md
├── config.example.js
├── index.html
├── js
│ ├── app.js
│ ├── cameraListView.js
│ ├── cameraView.js
│ ├── dashboardView.js
│ └── index.js
├── package.json
├── scripts
│ ├── launch-chrome.sh
│ ├── package.json
│ └── upload-image.sh
├── scss
│ ├── _bootstrap.scss
│ ├── _reset.scss
│ └── main.scss
├── test.data
├── views
│ ├── camera_list.html
│ ├── camera_view.html
│ └── dashboard.html
└── webpack.config.js
├── assets
├── .gitattributes
├── README.md
├── index.html
├── index.js
├── index.js.map
├── styles.js
├── styles.js.map
├── vendor.js
└── vendor.js.map
├── models
├── README.md
├── security-camera-body.stl
├── security-camera-cover-plate.dxf
└── security-camera-lens-cover-plate.dxf
├── pi
├── README.md
├── package.json
└── security-camera.js
└── runtime
├── .gitignore
├── AlertGenerator
├── AlertGenerator.js
├── descriptor.yaml
└── testdata
│ ├── ignoreevent.yaml
│ └── newsnapshot.yaml
├── CameraAuthenticator
├── CameraAuthenticator.js
├── descriptor.yaml
└── testdata
│ └── simple.yaml
├── README.md
├── UserAuthenticator
├── UserAuthenticator.js
├── descriptor.yaml
└── testdata
│ └── simple.yaml
└── example-context.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
27 | node_modules
28 |
29 | # ignore sqlite database
30 | *.sqlite
31 | *.sqlite3
32 |
33 | # ignore dump
34 | dump.rdb
35 |
36 | # Ignore sftp info
37 | sftp-config.json
38 |
39 | .DS_Store
40 | .sass-cache
41 |
42 | # Project Specific
43 | # generated css
44 | bean-counter/node/public/css/main.css
45 | connected-cars/node/public/css/main.css
46 | remote-monitor/node/public/css/main.css
47 | # remote monitor images
48 | remote-monitor/node/public/imgs/camera_photos/*
49 | # compiled electron code
50 | bean-counter/electron/compiled
51 |
52 | package-lock.json
53 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | default: build
2 |
3 | prepare-angular:
4 | (cd angular; npm install)
5 |
6 | build-angular:
7 | (cd angular; ./node_modules/.bin/grunt build)
8 |
9 | assets/index.html: angular/build/assets/index.html
10 | cp $< $@
11 | assets/vendor.js: angular/build/assets/vendor.js
12 | cp $< $@
13 | assets/vendor.js.map: angular/build/assets/vendor.js.map
14 | cp $< $@
15 | assets/styles.js: angular/build/assets/styles.js
16 | cp $< $@
17 | assets/styles.js.map: angular/build/assets/styles.js.map
18 | cp $< $@
19 | assets/index.js: angular/build/assets/index.js
20 | cp $< $@
21 | assets/index.js.map: angular/build/assets/index.js.map
22 | cp $< $@
23 |
24 | build-angular-assets: build-angular assets/index.html \
25 | assets/index.js assets/index.js.map \
26 | assets/styles.js assets/styles.js.map \
27 | assets/vendor.js assets/vendor.js.map
28 |
29 | .PHONY: prepare-angular build-angular build-angular-assets
30 |
31 | assets/UserAuthenticator-deployment-instructions.txt: runtime/UserAuthenticator/descriptor.yaml runtime/UserAuthenticator/UserAuthenticator.js runtime/UserAuthenticator/descriptor.yaml runtime/context-local.yaml
32 | twilio-runtime-utils -c runtime/context-local.yaml deploy runtime/UserAuthenticator/descriptor.yaml > $@
33 |
34 | assets/CameraAuthenticator-deployment-instructions.txt: runtime/CameraAuthenticator/descriptor.yaml runtime/CameraAuthenticator/CameraAuthenticator.js runtime/CameraAuthenticator/descriptor.yaml runtime/context-local.yaml
35 | twilio-runtime-utils -c runtime/context-local.yaml deploy runtime/CameraAuthenticator/descriptor.yaml > $@
36 |
37 | assets/AlertGenerator-deployment-instructions.txt: runtime/AlertGenerator/descriptor.yaml runtime/AlertGenerator/AlertGenerator.js runtime/AlertGenerator/descriptor.yaml runtime/context-local.yaml
38 | twilio-runtime-utils -c runtime/context-local.yaml deploy runtime/AlertGenerator/descriptor.yaml > $@
39 |
40 | build-runtime: assets/UserAuthenticator-deployment-instructions.txt assets/CameraAuthenticator-deployment-instructions.txt assets/AlertGenerator-deployment-instructions.txt
41 |
42 | .PHONY: build-runtime
43 |
44 | prepare: prepare-angular
45 |
46 | build: build-angular-assets # build-runtime
47 |
48 | dev:
49 | (cd angular; ./node_modules/.bin/grunt dev)
50 |
51 | clean:
52 | rm -rf angular/build/assets
53 |
54 | full-clean: clean
55 | rm -f angular/package-lock.json
56 | rm -rf angular/node_modules
57 | rm -rf angular/build
58 |
59 | rebuild: clean build
60 |
61 | full-rebuild: full-clean prepare build
62 |
63 | .PHONY: default prepare build dev clean full-clean rebuild full-rebuild
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Security Camera Blueprint
2 | ### Build a remote security monitoring device.
3 |
4 | **Disclaimer: The blueprints and information made available on this page are examples only and are not to be used for production purposes. Twilio disclaims any warranties and liabilities under any legal theory (including, without limitation, breach of contract, tort, and indemnification) in connection with your use of or reliance on the blueprints. Any liabilities that arise in connection with your use of these blueprints shall solely be borne by you. By accessing and downloading these blueprints, you agree to the foregoing terms.**
5 |
6 | Full instructions for this tutorial can be found in the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/). This repository includes all files neccessary to build the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/).
7 |
8 | **Problem** All businesses desire safety for property, assets, and employees, but most cannot afford or rely on a hired security team. A low-cost, reliable, simple remote security solution would provide peace of mind around the protection of enterprise resources.
9 |
10 |
11 | **Solution** Remove the hassle from remote monitoring with the best practices discussed in this Blueprint, designed to accelerate the development of new security solutions and products. We will create a Twilio-powered device that keeps watch over remote locations and alerts administrators of intrusions or safety concerns. Constantly monitoring, the device notifies stakeholders of events that threaten assets or safety.
12 |
13 |
14 | **Degree of Difficulty (1-5): 3** This device requires some knowledge of Raspberry Pi, software installation, and some Linux features including the command line.
15 |
16 | ### What You’ll Need
17 |
18 | Before we get started, here's a quick overview of what you'll need to build the Security Camera
19 |
20 | **Materias lists**
21 | Electronic Components
22 | * [Twilio Programmable Wireless Starter Pack](https://www.twilio.com/console/wireless/sims/orders/new)
23 | * [Wireless Security Camera shopping list](http://bit.ly/2A51Nvk)
24 |
25 | The Wireless Starter Kit in the shopping list comes with an 8GB micro SD card. You will need a [microSD adapter](https://www.amazon.com/SanDisk-microSD-Memory-Adapter-MICROSD-ADAPTER/dp/B0047WZOOO/ref=sr_1_4?s=electronics&ie=UTF8&qid=1501698065&sr=1-4&keywords=micro+sd+card+to) if your computer has an SD port, or you can use a [USB adapter](https://www.amazon.com/Adapter-Standard-Connector-Smartphones-Function/dp/B01BXSKPES/ref=sr_1_6?ie=UTF8&qid=1501697821&sr=8-6&keywords=micro+sd+to+sd+adapter) if necessary. This Blueprint assumes you have access to a USB modem, mouse, and HDMI connected monitor. Need a USB keyboard and mouse? Try [Amazon’s keyboard and mouse bundle pack](https://www.amazon.com/AmazonBasics-Wired-Keyboard-Mouse-Bundle/dp/B00B7GV802/ref=sr_1_4?ie=UTF8&qid=1502486034&sr=8-4&keywords=usb+mouse+and+keyboard).
26 |
--------------------------------------------------------------------------------
/angular/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /config.js
3 |
--------------------------------------------------------------------------------
/angular/Gruntfile.js:
--------------------------------------------------------------------------------
1 | var config = require("./config"); // copy and modify config.example.js
2 |
3 | module.exports = function(grunt) {
4 | var webpack = require("webpack");
5 | var webpackConfig = require("./webpack.config.js");
6 |
7 | // Project configuration.
8 | grunt.initConfig({
9 | pkg: grunt.file.readJSON('package.json'),
10 |
11 | webpack: {
12 | options: webpackConfig,
13 | build: {
14 | plugins: webpackConfig.plugins.concat(
15 | new webpack.optimize.UglifyJsPlugin({
16 | test: /.js$/,
17 | mangle: {
18 | except: ["App", "app"]
19 | },
20 | sourceMap: true
21 | })
22 | )
23 | }
24 | },
25 |
26 | "webpack-dev-server": {
27 | options: {
28 | webpack: webpackConfig,
29 | publicPath: webpackConfig.output.publicPath,
30 | contentBase: "build/assets",
31 | proxy: {
32 | // redirect all API calls to deployed runtime functions
33 | "/**": {
34 | target: config.RUNTIME_DOMAIN,
35 | changeOrigin: true,
36 | secure: false
37 | }
38 | }
39 | },
40 | start: {
41 | keepalive: true,
42 | port: 23845,
43 | webpack: {
44 | devtool: "eval"
45 | },
46 | }
47 | },
48 |
49 | clean: {
50 | folder: [
51 | 'build/assets'
52 | ]
53 | }
54 | });
55 |
56 | grunt.loadNpmTasks("grunt-webpack");
57 |
58 | grunt.loadNpmTasks('grunt-contrib-clean');
59 |
60 | grunt.registerTask("build", ["webpack:build"]);
61 |
62 | grunt.registerTask("dev", ["build", "webpack-dev-server:start"]);
63 |
64 | grunt.registerTask('default', ['build']);
65 | };
66 |
--------------------------------------------------------------------------------
/angular/README.md:
--------------------------------------------------------------------------------
1 | # Security Camera Blueprint
2 | ### Front-end single page application
3 | The scripts located in this directory are meant to run on [Twilio Runtime](https://www.twilio.com/docs/api/runtime/functions). These scripts are meant to be bundled into SPA artifacts to be uploaded to Twilio Runtime.
4 |
5 | Full instructions for this tutorial can be found in the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/). Below you will find the minimum steps necessary to build these SPA artifacts.
6 |
7 | ### What is Twilio Runtime?
8 | Twilio Runtime is a suite designed to help you build, scale and operate your application, consisting of a plethora of tools including helper libraries, API keys, asset storage, debugging tools, and a node based serverless hosting environment [Twilio Functions](https://www.twilio.com/docs/api/runtime/functions).
9 |
10 | ### Deploy Runtime assets
11 | Runtime assets are used to host the front-end of this Blueprint. The front-end is written using the [AngularJS](https://angularjs.org/) framework and compiled as a [single page application](https://en.wikipedia.org/wiki/Single-page_application). To deploy, you need to download the latest version of **index.html** and **index.min.js** from the **assets** folder.
12 |
13 | You may modify these files as you wish and bundle up by running the following commands in a terminal from the root directory:
14 |
15 | ```
16 | make prepare
17 | make
18 | ```
19 |
20 | Visit the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/) for more information.
21 |
--------------------------------------------------------------------------------
/angular/config.example.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // for local testing you need to point this to the runtime domain you use
3 | RUNTIME_DOMAIN: "https://your-runtime-domain.twil.io",
4 | };
5 |
--------------------------------------------------------------------------------
/angular/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Twilio's Security Camera Blueprint
5 |
6 |
7 |
8 |
9 |
10 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/angular/js/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const APP_CONFIGURATION_DOCUMENT_NAME = "app.configuration";
4 | function CAMERA_SNAPSHOT_DOCUMENT_NAME(cameraId) { return "cameras." + cameraId + ".snapshot"; }
5 | function CAMERA_CONTROL_MAP_NAME(cameraId) { return "cameras." + cameraId + ".control"; }
6 | function CAMERA_ALERTS_LIST_NAME(cameraId) { return "cameras." + cameraId + ".alerts"; }
7 | function CAMERA_ARCHIVES_LIST_NAME(cameraId, alertId) { return "cameras." + cameraId + ".archives." + alertId; }
8 |
9 | module.exports = function(callbacks) {
10 | const $ = require("jquery");
11 | const crypto = require("crypto");
12 | const SyncClient = require("twilio-sync").Client;
13 | var syncClient;
14 | var token;
15 | var auth = "username=twilio&pincode=928462";
16 | var configDocument;
17 |
18 | var cameras = {};
19 |
20 | function randomString(len) {
21 | var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
22 | var randomString = '';
23 | for (var i = 0; i < len; i++) {
24 | var randomPoz = Math.floor(Math.random() * charSet.length);
25 | randomString += charSet.substring(randomPoz,randomPoz+1);
26 | }
27 | return randomString;
28 | }
29 |
30 | function fetchSnapshotTmpUrl(mcs_url, callback) {
31 | $.ajax({
32 | type: "GET",
33 | url: mcs_url,
34 | dataType: 'json',
35 | beforeSend: function (xhr) { xhr.setRequestHeader('X-Twilio-Token', token); },
36 | success: function (data, status, xhr) {
37 | callback(data.links.content_direct_temporary);
38 | }
39 | });
40 | }
41 |
42 | function fetchCameraSnapshotTmpUrl(camera) {
43 | fetchSnapshotTmpUrl(camera.snapshot.mcs_url, function (snapshotTmpUrl) {
44 | // do not load new image when last image loading is in progress
45 | // otherwise user might experience stalling camera view
46 | if (!camera.snapshotLoadingInProgress) {
47 | camera.snapshotLoadingInProgress = true;
48 | camera.snapshot.img_url = snapshotTmpUrl;
49 | console.info("Loading image: " + camera.snapshot.img_url);
50 | }
51 | callbacks.refresh();
52 | });
53 | }
54 |
55 | function fetchSnapshot(camera) {
56 | syncClient.document(CAMERA_SNAPSHOT_DOCUMENT_NAME(camera.info.id)).then(function (doc) {
57 | camera.snapshotDocument = doc;
58 | camera.snapshot = doc.value;
59 | fetchCameraSnapshotTmpUrl(camera);
60 | doc.on("updated", function (data) {
61 | console.log("camera snapshot updated", camera.info.id, JSON.stringify(data));
62 | camera.snapshot = data;
63 | fetchCameraSnapshotTmpUrl(camera);
64 | });
65 | });
66 | }
67 |
68 | function fetchControl(camera) {
69 | syncClient.map(CAMERA_CONTROL_MAP_NAME(camera.info.id)).then(function (map) {
70 | Promise.all([
71 | map.get("preview"),
72 | map.get("alarm"),
73 | map.get("arm")
74 | ]).then(function (items) {
75 | camera.controlMap = map;
76 | camera.control = {
77 | preview : items[0].value,
78 | alarm : items[1].value,
79 | arm : items[2].value,
80 | };
81 | console.log("camera control fetched", camera.info.id, JSON.stringify(camera.control));
82 | map.on("itemUpdated", function (data) {
83 | console.log("camera control updated", camera.info.id, data.key, JSON.stringify(data.value));
84 | camera.control[data.key] = data.value;
85 | callbacks.refresh();
86 | });
87 | callbacks.refresh();
88 | });
89 | });
90 | }
91 |
92 | function loadCameras() {
93 | var invalidCameras = [];
94 |
95 | for (var cameraId in configDocument.value.cameras) {
96 | var camera = configDocument.value.cameras[cameraId];
97 | if (camera.id === cameraId &&
98 | typeof (camera.name) === "string" &&
99 | typeof(camera.contact_number) === "string" &&
100 | typeof(camera.twilio_sim_sid) === "string") {
101 | if (cameraId in cameras) {
102 | if (camera.name !== cameras[cameraId].info.name ||
103 | camera.contact_number !== cameras[cameraId].info.contact_number ||
104 | camera.twilio_sim_sid !== cameras[cameraId].info.twilio_sim_sid) {
105 | console.log("Updating camera", camera);
106 | cameras[cameraId].info = camera;
107 | }
108 | } else {
109 | console.log("Loading new camera", camera);
110 | cameras[cameraId] = {
111 | info: camera
112 | };
113 | fetchSnapshot(cameras[cameraId]);
114 | fetchControl(cameras[cameraId]);
115 | }
116 | } else {
117 | console.warn("Invalid camera configuration, removing from the list: ", cameraId, camera);
118 | invalidCameras.push(cameraId);
119 | }
120 | }
121 | for (var cameraId in cameras) {
122 | if (!(cameraId in configDocument.value.cameras)) {
123 | console.log("Deleting camera", camera);
124 | if (cameras[cameraId].snapshotDocument) {
125 | cameras[cameraId].snapshotDocument.removeAllListeners('updated');
126 | }
127 | if (cameras[cameraId].controlMap) {
128 | cameras[cameraId].controlMap.removeAllListeners('itemUpdated');
129 | }
130 | delete cameras[cameraId];
131 | }
132 | }
133 |
134 | return invalidCameras;
135 | }
136 |
137 | function cameraInfoCheck(camera, callback) {
138 | if (!camera.id || !camera.id.match(/^[a-zA-Z0-9]+$/)) { callback("camera id is invalid: " + camera.id); return false; }
139 | if (!camera.name) { callback("camera name is not specified"); return false; }
140 | if (!camera.contact_number || !camera.contact_number.match(/^[0-9]+$/)) { callback("camera contact number is invalid(only digits allowed): " + camera.contact_number); return false; }
141 | if (!camera.twilio_sim_sid || !camera.twilio_sim_sid.match(/^DE[a-z0-9]{32}$/)) { callback("camera sim SID is invalid: " + camera.twilio_sim_sid); return false; }
142 | return true;
143 | }
144 |
145 | function genToken() {
146 | var token = randomString(16);
147 | var hash = crypto.createHash('sha512').update(token).digest("hex");
148 | return { token: token, hash: hash };
149 | }
150 |
151 | return {
152 | initialized: $.Deferred(),
153 |
154 | cameras: cameras,
155 |
156 | updateToken: function (cb) {
157 | var that = this;
158 | return $.get("/userauthenticator?" + auth, function (result) {
159 | if (result.success) {
160 | console.log("token updated:", result);
161 | token = result.token;
162 | if (syncClient) {
163 | syncClient.updateToken(token);
164 | } else {
165 | syncClient = new SyncClient(token);
166 | }
167 | if (cb) cb(token);
168 | setTimeout(that.updateToken.bind(that), result.ttl*1000 * 0.96); // update token slightly in adance of ttl
169 | } else {
170 | console.error("failed to authenticate the user: ", result.error);
171 | }
172 | }).fail(function (jqXHR, textStatus, error) {
173 | console.error("failed to send authentication request:", textStatus, error);
174 | setTimeout(that.updateToken.bind(that), 10000); // retry in 10 seconds
175 | });
176 | },
177 |
178 | fetchConfiguration: function () {
179 | return syncClient.document(APP_CONFIGURATION_DOCUMENT_NAME).then(function (doc) {
180 | configDocument = doc;
181 | var newDoc = null;
182 | var invalidCameras;
183 |
184 | if (doc.value.cameras) {
185 | invalidCameras = loadCameras();
186 | if (invalidCameras.length) {
187 | if (null === newDoc) newDoc = $.extend(true, doc.value, {});
188 | invalidCameras.forEach(function (idOfInvalidCamera) {
189 | delete newDoc.cameras[idOfInvalidCamera];
190 | });
191 | }
192 | } else {
193 | console.warn("cameras is not configured, creating an empty list");
194 | if (null === newDoc) newDoc = $.extend(true, doc.value, {});
195 | newDoc.cameras = {};
196 | }
197 | return newDoc;
198 | }).then(function (newDoc) {
199 | if (newDoc !== null) {
200 | return configDocument.set(newDoc).then(function () {
201 | console.log("app configuration updated with new value:", newDoc);
202 | });
203 | }
204 | });
205 | },
206 |
207 | addCamera: function (newCamera, callback) {
208 | if (!cameraInfoCheck(newCamera, callback)) return;
209 | if (newCamera.id in configDocument.value.cameras) return callback("Camera with the same ID exists");
210 | newCamera.created_at = (new Date()).getTime();
211 |
212 | var t = genToken();
213 | newCamera.hash = t.hash;
214 |
215 | configDocument.mutate(function (remoteData) {
216 | if (!remoteData.cameras) remoteData.cameras = {};
217 | remoteData.cameras[newCamera.id] = newCamera;
218 | return remoteData;
219 | }).then(function () {
220 | // create necessary objects
221 | return Promise.all([
222 | syncClient.map(CAMERA_CONTROL_MAP_NAME(newCamera.id)).then(function (controlMap) {
223 | return Promise.all[
224 | controlMap.set('alarm', { id: -1 }),
225 | controlMap.set('arm', { enabled: true, responded_alarm: -1}),
226 | controlMap.set('preview', { enabled : false })
227 | ];
228 | }),
229 | syncClient.list(CAMERA_ALERTS_LIST_NAME(newCamera.id)),
230 | ]);
231 | }).then(function () {
232 | loadCameras();
233 | // make token temporarily visible
234 | callback(null, $.extend(true, cameras[newCamera.id].info, { token: t.token }));
235 | callbacks.refresh();
236 | }).catch(function (err) {
237 | callback(err);
238 | });
239 | },
240 |
241 | updateCamera: function (updatedCamera, callback) {
242 | configDocument.mutate(function (remoteData) {
243 | if (updatedCamera.id in remoteData.cameras) {
244 | remoteData.cameras[updatedCamera.id] = $.extend(true, updatedCamera, {
245 | hash: remoteData.cameras[updatedCamera.id].hash
246 | });
247 | } else {
248 | callback("Camera is not in the list");
249 | }
250 | return remoteData;
251 | }).then(function () {
252 | loadCameras();
253 | callback(null);
254 | callbacks.refresh();
255 | }).catch(function (err) {
256 | callback(err);
257 | });
258 | },
259 |
260 | regenToken: function (cameraId, callback) {
261 | var t = genToken();
262 | configDocument.mutate(function (remoteData) {
263 | if (cameraId in remoteData.cameras) {
264 | remoteData.cameras[cameraId].hash = t.hash;
265 | } else {
266 | throw "unknown camera: " + cameraId;
267 | }
268 | return remoteData;
269 | }).then(function () {
270 | loadCameras();
271 | // make token temporarily visible
272 | callback($.extend(true, cameras[cameraId].info, { token: t.token }));
273 | callbacks.refresh();
274 | }).catch(function (err) {
275 | // ignore error
276 | console.error("regenToken", err);
277 | });
278 | },
279 |
280 | deleteCamera: function (cameraId) {
281 | configDocument.mutate(function (remoteData) {
282 | delete remoteData.cameras[cameraId];
283 | return remoteData;
284 | }).then(function () {
285 | loadCameras();
286 | callbacks.refresh();
287 | }).then(function () {
288 | syncClient.map(CAMERA_CONTROL_MAP_NAME(cameraId)).then(function (map) { map.removeMap(); });
289 | syncClient.list(CAMERA_ALERTS_LIST_NAME(cameraId)).then(function (list) { list.removeList(); });
290 | });
291 | },
292 |
293 | controlPreview: function (cameraId) {
294 | var camera = cameras[cameraId];
295 | camera.controlMap.set("preview", camera.control.preview)
296 | .then(function () {
297 | console.log("switchPreview updated", cameraId, camera.control.preview);
298 | }).catch(function (err) {
299 | console.err("switchPreview failed", err);
300 | });
301 | },
302 |
303 | controlArm: function (cameraId) {
304 | var camera = cameras[cameraId];
305 | camera.controlMap.set("arm", camera.control.arm)
306 | .then(function () {
307 | console.log("switchArm updated", cameraId, camera.control.arm);
308 | }).catch(function (err) {
309 | console.err("switchArm failed", err);
310 | });
311 | },
312 |
313 | disarm: function (cameraId) {
314 | var camera = cameras[cameraId];
315 | camera.controlMap.set("arm", {
316 | enabled: camera.control.arm.enabled,
317 | responded_alarm: camera.control.alarm.id
318 | })
319 | .then(function () {
320 | console.log("disarm updated", cameraId, camera.control.arm);
321 | }).catch(function (err) {
322 | console.err("disarm failed", err);
323 | });
324 | },
325 |
326 | getAlerts: function (cameraId, callback) {
327 | syncClient.list(CAMERA_ALERTS_LIST_NAME(cameraId)).then(function (list) {
328 | return list.getItems({ order: "desc" }).then(function (page) {
329 | console.log("getAlerts", page);
330 | callback(page.items);
331 | });
332 | }).catch(function (err) {
333 | console.error("getAlerts failed", err);
334 | });
335 | },
336 |
337 | getNextArchivedSnapshot: function (cameraId, alertId, archiveId, callback) {
338 | syncClient.list(CAMERA_ARCHIVES_LIST_NAME(cameraId, alertId)).then(function (list) {
339 | return list.get(archiveId);
340 | }).then(function (item) {
341 | fetchSnapshotTmpUrl(item.data.value.mcs_url, function (snapshotTmpUrl) {
342 | callback(snapshotTmpUrl);
343 | });
344 | }).catch(function (err) {
345 | console.info("getNextArchivedSnapshot failed", err);
346 | callback(null);
347 | });
348 | },
349 |
350 | init: function () {
351 | var that = this;
352 | this.updateToken(function (token) {
353 | that.fetchConfiguration().then(function () {
354 | callbacks.refresh();
355 | }).then(function () {
356 | that.initialized.resolve();
357 | });
358 | });
359 | }
360 | };
361 | };
362 |
--------------------------------------------------------------------------------
/angular/js/cameraListView.js:
--------------------------------------------------------------------------------
1 | var $ = require("jquery");
2 |
3 | var cameraListView = {
4 | templateUrl: require("../views/camera_list.html"),
5 |
6 | init: function (app, $scope) {
7 | $scope.cameras = app.cameras;
8 | $scope.newCamera = {};
9 | $scope.addCamera = function () {
10 | app.addCamera(angular.copy($scope.newCamera), function (err, cameraAdded) {
11 | if (err) {
12 | $('#add-camera-failed').text(JSON.stringify(err));
13 | } else {
14 | $scope.editedCameraInfo = cameraAdded;
15 | $('.add-camera').hide();
16 | $('.add-camera-show').fadeIn(333);
17 | $scope.$apply();
18 | }
19 | });
20 | };
21 | $scope.editCamera = function (cameraId) {
22 | $scope.editedCameraInfo = app.cameras[cameraId].info;
23 | $('.edit-camera').fadeIn(333);
24 | };
25 | $scope.updateCamera = function () {
26 | app.updateCamera(angular.copy($scope.editedCameraInfo), function (err) {
27 | if (err) {
28 | $('#edit-camera-failed').text(JSON.stringify(err));
29 | } else {
30 | $('.edit-camera').hide();
31 | $scope.$apply();
32 | }
33 | });
34 | };
35 | $scope.deleteCamera = function (cameraId) {
36 | app.deleteCamera(cameraId);
37 | };
38 | $scope.regenTokenForCamera = function (cameraId) {
39 | app.regenToken(cameraId, function (cameraUpdated) {
40 | $scope.editedCameraInfo = cameraUpdated;
41 | });
42 | };
43 |
44 | $('.add-camera-show').click(function() {
45 | $(this).hide();
46 | $('.add-camera').fadeIn(333);
47 | });
48 | $('.add-camera-cancel').click(function() {
49 | $('.add-camera').hide();
50 | $('.add-camera-show').fadeIn(333);
51 | });
52 |
53 | $('.edit-camera-cancel').click(function() {
54 | $scope.editedCameraInfo = null;
55 | $('.edit-camera').hide();
56 | $scope.$apply();
57 | });
58 | },
59 | };
60 |
61 | module.exports = cameraListView;
62 |
--------------------------------------------------------------------------------
/angular/js/cameraView.js:
--------------------------------------------------------------------------------
1 | var $ = require("jquery");
2 |
3 | var cameraView = {
4 | templateUrl: require("../views/camera_view.html"),
5 |
6 | init: function (app, cameraId, $scope) {
7 | $scope.camera = app.cameras[cameraId];
8 | $scope.selectedAlertChanged = function () {
9 | console.log("selectedAlertChanged", $scope.selectedAlertId);
10 | $scope.selectedArchiveId = 0;
11 | $scope.selectNewArchiveId(0);
12 | $scope.snapshotTmpUrl = null;
13 | };
14 | $scope.selectNewArchiveId = function (newArchiveId) {
15 | console.log("selectNewArchiveId start", $scope.selectedAlertId, newArchiveId);
16 | app.getNextArchivedSnapshot(cameraId, $scope.selectedAlertId, newArchiveId, function (snapshotTmpUrl) {
17 | if (snapshotTmpUrl) {
18 | console.log("selectNewArchiveId accepted", $scope.selectedAlertId, snapshotTmpUrl);
19 | $scope.selectedArchiveId = newArchiveId;
20 | $scope.snapshotTmpUrl = snapshotTmpUrl;
21 | $scope.$apply();
22 | } else {
23 | console.log("selectNewArchiveId rejected", $scope.selectedAlertId);
24 | }
25 | });
26 | };
27 | $scope.selectPreviousArchive = function () {
28 | if ($scope.selectedArchiveId > 0) {
29 | $scope.selectNewArchiveId($scope.selectedArchiveId - 1);
30 | }
31 | };
32 | $scope.selectNextArchive = function () {
33 | $scope.selectNewArchiveId($scope.selectedArchiveId + 1);
34 | };
35 | app.getAlerts(cameraId, function (alerts) {
36 | $scope.alerts = alerts;
37 | $scope.selectedAlertId = alerts[0].data.index+"";
38 | $scope.$apply();
39 | $scope.selectedAlertChanged();
40 | });
41 | }
42 | };
43 |
44 | module.exports = cameraView;
45 |
--------------------------------------------------------------------------------
/angular/js/dashboardView.js:
--------------------------------------------------------------------------------
1 | var dashboardView = {
2 | templateUrl: require("../views/dashboard.html"),
3 |
4 | init: function (app, $scope) {
5 | $scope.cameras = app.cameras;
6 | $scope.modes = ["live-feed", "arm"];
7 | $scope.noCamera = function () { return Object.keys(app.cameras).length === 0; }
8 | $scope.update = function(value, camera) {
9 | if (value == "live-feed") {
10 | camera.control.preview.enabled = true;
11 | camera.control.arm.enabled = false;
12 | }
13 | else {
14 | camera.control.preview.enabled = false;
15 | camera.control.arm.enabled = true;
16 | }
17 | app.controlPreview(camera.info.id);
18 | app.controlArm(camera.info.id);
19 | console.log(camera.info)
20 | };
21 | $scope.img_oninit = function (camera) {
22 | camera.snapshotLoadingInProgress = false;
23 | };
24 | $scope.img_onloaded = function (camera) {
25 | console.info("Image loaded: " + camera.snapshot.img_url);
26 | camera.snapshotLoadingInProgress = false;
27 | };
28 | $scope.switchPreview = function (cameraId) {
29 | app.controlPreview(cameraId);
30 | };
31 | $scope.switchArm = function (cameraId) {
32 | app.controlArm(cameraId);
33 | };
34 | $scope.disarm = function (cameraId) {
35 | app.disarm(cameraId);
36 | };
37 | }
38 | };
39 |
40 | module.exports = dashboardView;
41 |
--------------------------------------------------------------------------------
/angular/js/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const $ = require("jquery");
4 | const angular = require("angular");
5 | const moment = require("moment");
6 | require("angular-route");
7 |
8 | const MOMENT_FORMAT = "MMM DD YYYY @ hh:mm";
9 |
10 | // style sheets
11 | require("bootstrap-webpack");
12 | require("../scss/main.scss");
13 |
14 | // index.html
15 | require("../index.html");
16 |
17 | const dashboardView = require("./dashboardView");
18 | const cameraListView = require("./cameraListView");
19 | const cameraView = require("./cameraView");
20 |
21 | var currentView;
22 | var $currentViewScope;
23 |
24 | var App = require("./app");
25 | window.app = new App({
26 | refresh: function () {
27 | $currentViewScope.$apply();
28 | }
29 | });
30 |
31 | angular
32 | .module("app", [
33 | 'ngRoute'
34 | ])
35 | .controller('DashboardViewCtrl', ['$scope', function ($scope) {
36 | $currentViewScope = $scope;
37 | currentView = dashboardView;
38 | $.when(app.initialized).done(function () {
39 | dashboardView.init(app, $scope);
40 | });
41 | }])
42 | .controller('CameraListViewCtrl', ['$scope', function ($scope) {
43 | $currentViewScope = $scope;
44 | currentView = cameraListView;
45 | $.when(app.initialized).done(function () {
46 | cameraListView.init(app, $scope);
47 | });
48 | }])
49 | .controller('CameraView', ['$routeParams', '$scope', function ($routeParams, $scope) {
50 | $currentViewScope = $scope;
51 | currentView = cameraView;
52 | $.when(app.initialized).done(function () {
53 | cameraView.init(app, $routeParams.id, $scope);
54 | });
55 | }])
56 | .config(['$routeProvider', function ($routeProvider) {
57 | $routeProvider
58 | .when('/dashboard', { controller: 'DashboardViewCtrl', templateUrl: dashboardView.templateUrl } )
59 | .when('/cameras', { controller: 'CameraListViewCtrl', templateUrl: cameraListView.templateUrl } )
60 | .when('/cameras/:id', { controller: 'CameraView', templateUrl: cameraView.templateUrl } )
61 | .otherwise({ redirectTo: '/dashboard' });
62 | }])
63 | .filter('moment', function () {
64 | return function (datestr) {
65 | return moment(datestr).format(MOMENT_FORMAT);
66 | };
67 | })
68 | .directive('imageloaded', function() {
69 | return {
70 | restrict: 'A',
71 | link: function(scope, element, attrs) {
72 | element.bind('load', function() {
73 | //call the function that was passed
74 | scope.$apply(attrs.imageloaded);
75 | });
76 | }
77 | };
78 | });
79 |
--------------------------------------------------------------------------------
/angular/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twilio-fleet-tracker-ui",
3 | "version": "1.0.0",
4 | "description": "Tracking your fleet with twilio wireless SIM card and connected devices services. This package includes its angular UI.",
5 | "main": "app.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/twilio/wireless-co-pilot.git"
12 | },
13 | "keywords": [
14 | "twilio",
15 | "twilio-sync",
16 | "iot"
17 | ],
18 | "author": "zmiao@twilio.com",
19 | "license": "ISC",
20 | "bugs": {
21 | "url": "https://github.com/twilio/wireless-co-pilot/issues"
22 | },
23 | "homepage": "https://github.com/twilio/wireless-co-pilot#readme",
24 | "devDependencies": {},
25 | "dependencies": {
26 | "bootstrap": ">=3.0.2",
27 | "bootstrap-webpack": "^0.0.6",
28 | "css-loader": "^0.28.1",
29 | "exports-loader": "^0.6.4",
30 | "extract-text-webpack-plugin": "^2.1.0",
31 | "file-loader": "^0.11.1",
32 | "grunt": "^1.0.1",
33 | "grunt-cli": "^1.2.0",
34 | "grunt-contrib-clean": "^1.1.0",
35 | "grunt-webpack": "^3.0.0",
36 | "html-loader": "^0.4.5",
37 | "imports-loader": "^0.7.1",
38 | "less": "^2.7.2",
39 | "less-loader": "^4.0.3",
40 | "moment": "^2.18.1",
41 | "ngtemplate-loader": "^1.3.1",
42 | "node-bourbon": "^4.2.8",
43 | "node-sass": "^4.5.2",
44 | "path": "^0.12.7",
45 | "sass-loader": "^6.0.5",
46 | "style-loader": "^0.17.0",
47 | "url-loader": "^0.5.8",
48 | "webpack": "^2.5.1",
49 | "webpack-dev-server": "^2.4.5",
50 | "webpack-vendor-chunk-plugin": "^1.0.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/angular/scripts/launch-chrome.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | D=$(dirname "$0")
3 | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
4 | --disable-web-security \
5 | --auto-open-devtools-for-tabs \
6 | --user-data-dir=$D/../build/chrome.data \
7 | http://localhost:23845/assets/index.html
8 |
--------------------------------------------------------------------------------
/angular/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wireless-security-camera-scripts",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "twilio-sync": "^0.5.10"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/angular/scripts/upload-image.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Simulated camera uploading script.
4 | #
5 | # How to use:
6 | # 1. Export RUNTIME_DOMAIN,RUNTIME_CAMERA_ID, RUNTIME_CAMERA_TOKEN environment variables in shell
7 | # 2. Run the script with the first parameter of a valid png or jpg image file.
8 | # cat photoes are recommended: https://github.com/maxogden/cats/tree/master/cat_photos
9 | # 3. To trigger a alarm, touch the file at /tmp/wireless-security-camera-alarm.trigger
10 | #
11 | # Example loopy loopy command:
12 | # $ while true;do for img in `ls ~/Pictures/cats/cat_photos/*.{png,jpg} | sort -R | head -n10`;do ./upload-image.sh $img;done;done
13 | #
14 |
15 | echo "Authenticate camera..."
16 | read camera_token camera_sync_instance camera_upload_url camera_snapshot_document < {
91 | doc.set({
92 | mcs_url : "$mcs_content_url",
93 | traits: {
94 | changes_detected: $alarm_triggered,
95 | }
96 | }).then(function () {
97 | process.exit(0);
98 | });
99 | });
100 | eof
101 | test $? -eq 0 && echo "Camera '$RUNTIME_CAMERA_ID' snapshot updated." || exit -1
102 |
103 | shift
104 | done
105 |
--------------------------------------------------------------------------------
/angular/scss/_reset.scss:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 | /* HTML5 display-role reset for older browsers */
27 | article, aside, details, figcaption, figure,
28 | footer, header, hgroup, menu, nav, section {
29 | display: block;
30 | }
31 | body {
32 | line-height: 1;
33 | }
34 | ol, ul {
35 | list-style: none;
36 | }
37 | blockquote, q {
38 | quotes: none;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: '';
43 | content: none;
44 | }
45 | table {
46 | border-collapse: collapse;
47 | border-spacing: 0;
48 | }
--------------------------------------------------------------------------------
/angular/scss/main.scss:
--------------------------------------------------------------------------------
1 | @import 'reset';
2 | @import 'bourbon';
3 | @import 'bootstrap';
4 |
5 | // Colors
6 | $white: #FFFFFF;
7 | $darkest-grey: #222222;
8 | $dark-grey: #3d3d3d;
9 | $grey: #464545;
10 | $blue: #3498db;
11 | $purple: #454DE1;
12 | $green: #00bc8c;
13 | $red: #e74c3c;
14 | $peach: #E78F3C;
15 | $orange: #f39c12;
16 | $wireless: #7D60A9;
17 | $tred: #F22F46;
18 | $tblue: #0D122B;
19 | $chalk: #F5F5F5;
20 | $ash: #E8E8E8;
21 | $smoke: #94979B;
22 |
23 | body {
24 | @include font-feature-settings("kern", "liga", "pnum");
25 | -webkit-font-smoothing: antialiased;
26 | padding-top: 50px;
27 | background-color: white;
28 | }
29 |
30 | a:link {
31 | color: $tred;
32 | text-decoration: none;
33 | border-bottom: 1px solid $tblue;
34 | }
35 |
36 | a:hover {
37 | color: white;
38 | border-bottom-color: $tred;
39 | }
40 |
41 | select {
42 | border: 1px solid $tblue;
43 | }
44 |
45 | h1 {
46 | margin-bottom: 20px;
47 | font-size: 30px;
48 | }
49 |
50 | .camera {
51 | padding-top: 5px;
52 | padding-bottom: 20px;
53 | h3 {
54 | a {
55 | font-size: 15px;
56 | }
57 | }
58 | .motion_stats {
59 | margin-top: 20px;
60 | text-align: center;
61 | }
62 | }
63 |
64 | .past_images {
65 | img {
66 | width: 100%;
67 | }
68 | }
69 |
70 | .add-camera-show {
71 | margin-top: 40px;
72 | background-color: $green;
73 | border: none;
74 | }
75 |
76 | .add-camera {
77 | margin-top: 40px;
78 | display: none;
79 | }
80 |
81 | .btn-primary {
82 | background-color: $green !important;
83 | }
84 |
85 | .edit-camera {
86 | margin-top: 40px;
87 | display: none;
88 | color: $tblue;
89 | }
90 |
91 | .camera-feed {
92 | display: flex;
93 |
94 | .buffer {
95 | margin: 20px 0 20px 0;
96 | }
97 | }
98 | .camera-feed > div {
99 | min-width: 0;
100 | }
101 | .camera-feed > img {
102 | max-width: 100%;
103 | height: auto;
104 | }
105 |
106 | .alerts-list {
107 | color: $tblue;
108 | margin-top: 20px;
109 | display: flex;
110 | align-items: center;
111 | select {
112 | display: inline;
113 | height: auto;
114 | width: auto;
115 | border: 1px solid $tblue;
116 | }
117 | select:focus {
118 | border: 1px solid $tblue;
119 | }
120 | }
121 |
122 | .archive-view {
123 | margin-top: 20px;
124 | display: flex;
125 | align-items: center;
126 | }
127 |
128 | .selectpicker {
129 | color: black;
130 | }
131 |
132 | .navbar-custom {
133 | background-color: $tred !important;
134 | a {
135 | color: white;
136 | border-bottom: none;
137 | }
138 | :hover,
139 | :link,
140 | :focus {
141 | color: $tblue;
142 | background-color: $tred !important;
143 | }
144 | }
145 |
146 | .chalk-background {
147 | background-color: $chalk !important;
148 | color: $tblue;
149 | input,
150 | p
151 | {
152 | width:100%;
153 | }
154 | a {
155 | color: $tblue;
156 | border-bottom: 1px solid $tblue;
157 | }
158 | a:hover {
159 | border-bottom-color: $tred;
160 | text-decoration: none;
161 | }
162 | a:focus {
163 | text-decoration: none;
164 | }
165 | label {
166 | margin-left: 5px;
167 | }
168 | p {
169 | color: $smoke;
170 | margin: 5px 0px 0px 5px;
171 | }
172 | .btn-primary,
173 | .btn-default {
174 | border: none;
175 | }
176 | }
177 |
178 | h1,
179 | h2,
180 | h3 span {
181 | color: $tblue !important;
182 | }
183 |
184 | .bs-component {
185 | td,
186 | th {
187 | color: $tblue;
188 | }
189 | }
190 |
191 | .table-striped > tbody > tr:nth-of-type(odd) {
192 | background-color: $chalk;
193 | }
194 |
195 | .table-hover > tbody > tr:hover {
196 | background-color: $ash;
197 | }
198 |
199 | .navigate-archive {
200 | text-align: center;
201 | color: $tblue;
202 | a {
203 | color: $tblue;
204 | text-decoration: none;
205 | border-bottom-color: 1px solid $tblue;
206 | }
207 | a:hover {
208 | border-bottom-color: $tred;
209 | }
210 | }
211 |
212 | .archive-notification {
213 | color: $tblue;
214 | }
215 |
216 | @media only screen and (min-width : 1000px) {
217 | .camera-feed {
218 | margin: 0 auto;
219 | max-width: 703px;
220 | }
221 | }
--------------------------------------------------------------------------------
/angular/test.data:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/wireless-security-camera/3f74a9a0fc787eaa08ba5dd5293e5a6a0797468a/angular/test.data
--------------------------------------------------------------------------------
/angular/views/camera_list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 | Id |
12 | Name |
13 | Phone Number |
14 | SIM Sid |
15 | Token |
16 | Created |
17 | Actions |
18 |
19 |
20 |
21 |
22 | {{ camera.info.id }} |
23 | {{ camera.info.name }} |
24 | {{ camera.info.contact_number }} |
25 | {{ camera.info.twilio_sim_sid }} |
26 |
27 | {{ editedCameraInfo.token }}
28 |
29 | |
30 | {{ camera.info.created_at | moment }} |
31 |
32 |
33 |
34 |
35 | |
36 |
37 |
38 |
39 |
< >
40 |
41 |
42 |
43 |
44 |
45 |
95 |
96 |
146 |
--------------------------------------------------------------------------------
/angular/views/camera_view.html:
--------------------------------------------------------------------------------
1 |
2 |
No Security Camera Alerts
3 |
No notifications have been logged.
4 |
5 |
6 |
7 |
{{ camera.info.name }}
8 |
9 |
10 | Select an alert to view
11 |
15 |
16 |
17 |
18 |
19 |
20 |
![]()
21 |
22 |
23 |
24 |
28 |
29 |
--------------------------------------------------------------------------------
/angular/views/dashboard.html:
--------------------------------------------------------------------------------
1 | Active Security Cameras
2 |
3 | We aren't tracking any Security Cameras right now. Click the Add Security Camera button to get started.
4 |
5 |
6 |
7 | {{ camera.info.name }}
8 |
9 | Select the mode for your camera
10 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
![]()
25 |
26 |
27 |
28 |
29 |
30 |
31 | No Active Alarm
32 |
33 |
36 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/angular/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require("path");
2 | var webpack = require("webpack");
3 |
4 | module.exports = {
5 | cache: true,
6 | // Group dependencies into different entries and use CommonsChunkPlugin to share
7 | // the common parts and generate smaller chunks
8 | entry: {
9 | "index": "./js/index.js",
10 | "styles": [
11 | "./scss/main.scss",
12 | ],
13 | "vendor": [
14 | "bootstrap", "bootstrap-webpack",
15 | "moment",
16 | "crypto",
17 | ]
18 | },
19 | // use externals to exclude components that are statically included from CDNs
20 | externals: {
21 | "jquery": 'jQuery',
22 | "twilio-sync": "Twilio.Sync",
23 | "angular": "angular",
24 | "angular-route": { amd: "angular-route" },
25 | },
26 | output: {
27 | path: path.join(__dirname, "build", "assets"),
28 | publicPath: "/assets/",
29 | filename: "[name].js",
30 | chunkFilename: "[name]-[chunkhash].js"
31 | },
32 | module: {
33 | loaders: [
34 | // required to write "require('./style.css')"
35 | { test: /\.css$/, loader: "style-loader!css-loader" },
36 |
37 | // required to write "require('./style.scss')"
38 | {
39 | test: /\.scss$/, use: [{
40 | loader: "style-loader"
41 | }, {
42 | loader: "css-loader"
43 | }, {
44 | loader: "sass-loader",
45 | options: {
46 | includePaths :
47 | require('node-bourbon').includePaths
48 | }
49 | }]
50 | },
51 |
52 | // required for bootstrap icons
53 | { test: /\.woff$/, loader: "url-loader?limit=5000&mimetype=application/font-woff" },
54 | { test: /\.woff2$/, loader: "url-loader?limit=5000&mimetype=application/font-woff2" },
55 | { test: /\.ttf$/, loader: "file-loader" },
56 | { test: /\.eot$/, loader: "file-loader" },
57 | { test: /\.svg$/, loader: "file-loader" },
58 |
59 | // generate index.html
60 | { test: /\/index\.html$/, loader : "file-loader?name=index.html" },
61 |
62 | { test: /views\/\w+\.html$/, loader: "ngtemplate-loader?relativeTo=" + __dirname + "/!html-loader" }
63 | ]
64 | },
65 | devtool: 'source-map',
66 | plugins: [
67 | new webpack.optimize.CommonsChunkPlugin({
68 | names: ['styles', 'vendor'],
69 | minChunks: Infinity
70 | }),
71 | ]
72 | };
73 |
--------------------------------------------------------------------------------
/assets/.gitattributes:
--------------------------------------------------------------------------------
1 | /* binary
2 | /README.md text diff
3 |
--------------------------------------------------------------------------------
/assets/README.md:
--------------------------------------------------------------------------------
1 | # Security Camera Blueprint
2 | ### Twilio Runtime assets
3 | The scripts located in this directory are meant to run on [Twilio Runtime](https://www.twilio.com/docs/api/runtime/functions).
4 |
5 | ### What is Twilio Runtime?
6 | Twilio Runtime is a suite designed to help you build, scale and operate your application, consisting of a plethora of tools including helper libraries, API keys, asset storage, debugging tools, and a node based serverless hosting environment [Twilio Functions](https://www.twilio.com/docs/api/runtime/functions).
7 |
8 | ### Deploy Runtime assets
9 | Runtime assets are used to host the front-end of this Blueprint. The front-end is written using the [AngularJS](https://angularjs.org/) framework and compiled as a [single page application](https://en.wikipedia.org/wiki/Single-page_application). To deploy, you need to download the latest version of **index.html** and **index.min.js** which you could find in the Security Camera repository.
10 |
11 | Full instructions for this tutorial can be found in the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/). Below you will find the minimum steps necessary to get this up and running.
12 |
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Twilio's Security Camera Blueprint
5 |
6 |
7 |
8 |
9 |
10 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/assets/index.js:
--------------------------------------------------------------------------------
1 | webpackJsonp([0],{202:function(e,n,t){"use strict";function a(e){return"cameras."+e+".snapshot"}function i(e){return"cameras."+e+".control"}function o(e){return"cameras."+e+".alerts"}function r(e,n){return"cameras."+e+".archives."+n}e.exports=function(e){function n(e){for(var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t="",a=0;a0&&n.selectNewArchiveId(n.selectedArchiveId-1)},n.selectNextArchive=function(){n.selectNewArchiveId(n.selectedArchiveId+1)},app.getAlerts(e,function(e){n.alerts=e,n.selectedAlertId=e[0].data.index+"",n.$apply(),n.selectedAlertChanged()})}});e.exports=a},205:function(e,n,t){var a={templateUrl:t(297),init:function(app,e){e.cameras=app.cameras,e.modes=["live-feed","arm"],e.noCamera=function(){return 0===Object.keys(app.cameras).length},e.update=function(e,n){"live-feed"==e?(n.control.preview.enabled=!0,n.control.arm.enabled=!1):(n.control.preview.enabled=!1,n.control.arm.enabled=!0),app.controlPreview(n.info.id),app.controlArm(n.info.id),console.log(n.info)},e.img_oninit=function(e){e.snapshotLoadingInProgress=!1},e.img_onloaded=function(e){console.info("Image loaded: "+e.snapshot.img_url),e.snapshotLoadingInProgress=!1},e.switchPreview=function(e){app.controlPreview(e)},e.switchArm=function(e){app.controlArm(e)},e.disarm=function(e){app.disarm(e)}}};e.exports=a},207:function(e,n,t){e.exports=t.p+"index.html"},208:function(e,n){e.exports=angular},209:function(e,n){e.exports=void 0},210:function(e,n,t){"use strict";const a=t(4),i=t(208),o=t(0);t(209);t(30),t(31),t(207);const r=t(205),c=t(203),l=t(204);var s,d,App=t(202);window.app=new App({refresh:function(){d.$apply()}}),i.module("app",["ngRoute"]).controller("DashboardViewCtrl",["$scope",function(e){d=e,s=r,a.when(app.initialized).done(function(){r.init(app,e)})}]).controller("CameraListViewCtrl",["$scope",function(e){d=e,s=c,a.when(app.initialized).done(function(){c.init(app,e)})}]).controller("CameraView",["$routeParams","$scope",function(e,n){d=n,s=l,a.when(app.initialized).done(function(){l.init(app,e.id,n)})}]).config(["$routeProvider",function(e){e.when("/dashboard",{controller:"DashboardViewCtrl",templateUrl:r.templateUrl}).when("/cameras",{controller:"CameraListViewCtrl",templateUrl:c.templateUrl}).when("/cameras/:id",{controller:"CameraView",templateUrl:l.templateUrl}).otherwise({redirectTo:"/dashboard"})}]).filter("moment",function(){return function(e){return o(e).format("MMM DD YYYY @ hh:mm")}}).directive("imageloaded",function(){return{restrict:"A",link:function(e,n,t){n.bind("load",function(){e.$apply(t.imageloaded)})}}})},295:function(e,n){var t="views/camera_list.html";window.angular.module("ng").run(["$templateCache",function(e){e.put(t,'\n
\n \n\n
\n
\n \n \n Id | \n Name | \n Phone Number | \n SIM Sid | \n Token | \n Created | \n Actions | \n
\n \n \n \n {{ camera.info.id }} | \n {{ camera.info.name }} | \n {{ camera.info.contact_number }} | \n {{ camera.info.twilio_sim_sid }} | \n \n {{ editedCameraInfo.token }}\n \n | \n {{ camera.info.created_at | moment }} | \n \n \n \n \n | \n
\n \n
\n
< >
\x3c!-- /example --\x3e\n
\n
\n\n\n\n\n\n\n')}]),e.exports=t},296:function(e,n){var t="views/camera_view.html";window.angular.module("ng").run(["$templateCache",function(e){e.put(t,'\n
No Security Camera Alerts
\n
No notifications have been logged.
\n
\n\n\n
{{ camera.info.name }}
\n\n
\n Select an alert to view\n \n \n
\n\n
\n
\n
![]()
\n
\n
\n\n
\n
\n')}]),e.exports=t},297:function(e,n){var t="views/dashboard.html";window.angular.module("ng").run(["$templateCache",function(e){e.put(t,'Active Security Cameras
\n\nWe aren\'t tracking any Security Cameras right now. Click the Add Security Camera button to get started.
\n\n\n
\n {{ camera.info.name }}\n
\n Select the mode for your camera\n \n
\n \n\n
\n
\n
\n
![]()
\n
\n
\n
\n
\n
\n
\n No Active Alarm\n
\n
\n
\n
\n
\n
\n')}]),e.exports=t},322:function(e,n){e.exports=Twilio.Sync}},[210]);
2 | //# sourceMappingURL=index.js.map
--------------------------------------------------------------------------------
/assets/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///index.js","webpack:///./js/app.js","webpack:///./js/cameraListView.js","webpack:///./js/cameraView.js","webpack:///./js/dashboardView.js","webpack:///./index.html","webpack:///external \"angular\"","webpack:///external {\"amd\":\"angular-route\"}","webpack:///./js/index.js","webpack:///./views/camera_list.html","webpack:///./views/camera_view.html","webpack:///./views/dashboard.html","webpack:///external \"Twilio.Sync\""],"names":["webpackJsonp","202","module","exports","__webpack_require__","CAMERA_SNAPSHOT_DOCUMENT_NAME","cameraId","CAMERA_CONTROL_MAP_NAME","CAMERA_ALERTS_LIST_NAME","CAMERA_ARCHIVES_LIST_NAME","alertId","callbacks","randomString","len","charSet","i","randomPoz","Math","floor","random","length","substring","fetchSnapshotTmpUrl","mcs_url","callback","$","ajax","type","url","dataType","beforeSend","xhr","setRequestHeader","token","success","data","status","links","content_direct_temporary","fetchCameraSnapshotTmpUrl","camera","snapshot","snapshotTmpUrl","snapshotLoadingInProgress","img_url","console","info","refresh","fetchSnapshot","syncClient","document","id","then","doc","snapshotDocument","value","on","log","JSON","stringify","fetchControl","map","Promise","all","get","items","controlMap","control","preview","alarm","arm","key","loadCameras","invalidCameras","configDocument","cameras","name","contact_number","twilio_sim_sid","warn","push","removeAllListeners","cameraInfoCheck","match","genToken","hash","crypto","createHash","update","digest","SyncClient","Client","initialized","Deferred","updateToken","cb","that","this","result","setTimeout","bind","ttl","error","fail","jqXHR","textStatus","fetchConfiguration","newDoc","extend","forEach","idOfInvalidCamera","set","addCamera","newCamera","created_at","Date","getTime","t","mutate","remoteData","enabled","responded_alarm","list","catch","err","updateCamera","updatedCamera","regenToken","deleteCamera","removeMap","removeList","controlPreview","controlArm","disarm","getAlerts","getItems","order","page","getNextArchivedSnapshot","archiveId","item","init","resolve","203","cameraListView","templateUrl","app","$scope","angular","copy","cameraAdded","text","editedCameraInfo","hide","fadeIn","$apply","editCamera","regenTokenForCamera","cameraUpdated","click","204","cameraView","selectedAlertChanged","selectedAlertId","selectedArchiveId","selectNewArchiveId","newArchiveId","selectPreviousArchive","selectNextArchive","alerts","index","205","dashboardView","modes","noCamera","Object","keys","img_oninit","img_onloaded","switchPreview","switchArm","207","p","208","209","undefined","210","moment","currentView","$currentViewScope","App","window","controller","when","done","$routeParams","config","$routeProvider","otherwise","redirectTo","filter","datestr","format","directive","restrict","link","scope","element","attrs","imageloaded","295","path","run","c","put","296","297","322","Twilio","Sync"],"mappings":"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","file":"index.js","sourcesContent":["webpackJsonp([0],{\n\n/***/ 202:\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n\n\nconst APP_CONFIGURATION_DOCUMENT_NAME = \"app.configuration\";\nfunction CAMERA_SNAPSHOT_DOCUMENT_NAME(cameraId) { return \"cameras.\" + cameraId + \".snapshot\"; }\nfunction CAMERA_CONTROL_MAP_NAME(cameraId) { return \"cameras.\" + cameraId + \".control\"; }\nfunction CAMERA_ALERTS_LIST_NAME(cameraId) { return \"cameras.\" + cameraId + \".alerts\"; }\nfunction CAMERA_ARCHIVES_LIST_NAME(cameraId, alertId) { return \"cameras.\" + cameraId + \".archives.\" + alertId; }\n\nmodule.exports = function(callbacks) {\n const $ = __webpack_require__(4);\n const crypto = __webpack_require__(43);\n const SyncClient = __webpack_require__(322).Client;\n var syncClient;\n var token;\n var auth = \"username=twilio&pincode=928462\";\n var configDocument;\n\n var cameras = {};\n\n function randomString(len) {\n var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n var randomString = '';\n for (var i = 0; i < len; i++) {\n var randomPoz = Math.floor(Math.random() * charSet.length);\n randomString += charSet.substring(randomPoz,randomPoz+1);\n }\n return randomString;\n }\n\n function fetchSnapshotTmpUrl(mcs_url, callback) {\n $.ajax({\n type: \"GET\",\n url: mcs_url,\n dataType: 'json',\n beforeSend: function (xhr) { xhr.setRequestHeader('X-Twilio-Token', token); },\n success: function (data, status, xhr) {\n callback(data.links.content_direct_temporary);\n }\n });\n }\n\n function fetchCameraSnapshotTmpUrl(camera) {\n fetchSnapshotTmpUrl(camera.snapshot.mcs_url, function (snapshotTmpUrl) {\n // do not load new image when last image loading is in progress\n // otherwise user might experience stalling camera view\n if (!camera.snapshotLoadingInProgress) {\n camera.snapshotLoadingInProgress = true;\n camera.snapshot.img_url = snapshotTmpUrl;\n console.info(\"Loading image: \" + camera.snapshot.img_url);\n }\n callbacks.refresh();\n });\n }\n\n function fetchSnapshot(camera) {\n syncClient.document(CAMERA_SNAPSHOT_DOCUMENT_NAME(camera.info.id)).then(function (doc) {\n camera.snapshotDocument = doc;\n camera.snapshot = doc.value;\n fetchCameraSnapshotTmpUrl(camera);\n doc.on(\"updated\", function (data) {\n console.log(\"camera snapshot updated\", camera.info.id, JSON.stringify(data));\n camera.snapshot = data;\n fetchCameraSnapshotTmpUrl(camera);\n });\n });\n }\n\n function fetchControl(camera) {\n syncClient.map(CAMERA_CONTROL_MAP_NAME(camera.info.id)).then(function (map) {\n Promise.all([\n map.get(\"preview\"),\n map.get(\"alarm\"),\n map.get(\"arm\")\n ]).then(function (items) {\n camera.controlMap = map;\n camera.control = {\n preview : items[0].value,\n alarm : items[1].value,\n arm : items[2].value,\n };\n console.log(\"camera control fetched\", camera.info.id, JSON.stringify(camera.control));\n map.on(\"itemUpdated\", function (data) {\n console.log(\"camera control updated\", camera.info.id, data.key, JSON.stringify(data.value));\n camera.control[data.key] = data.value;\n callbacks.refresh();\n });\n callbacks.refresh();\n });\n });\n }\n\n function loadCameras() {\n var invalidCameras = [];\n\n for (var cameraId in configDocument.value.cameras) {\n var camera = configDocument.value.cameras[cameraId];\n if (camera.id === cameraId &&\n typeof (camera.name) === \"string\" &&\n typeof(camera.contact_number) === \"string\" &&\n typeof(camera.twilio_sim_sid) === \"string\") {\n if (cameraId in cameras) {\n if (camera.name !== cameras[cameraId].info.name ||\n camera.contact_number !== cameras[cameraId].info.contact_number ||\n camera.twilio_sim_sid !== cameras[cameraId].info.twilio_sim_sid) {\n console.log(\"Updating camera\", camera);\n cameras[cameraId].info = camera;\n }\n } else {\n console.log(\"Loading new camera\", camera);\n cameras[cameraId] = {\n info: camera\n };\n fetchSnapshot(cameras[cameraId]);\n fetchControl(cameras[cameraId]);\n }\n } else {\n console.warn(\"Invalid camera configuration, removing from the list: \", cameraId, camera);\n invalidCameras.push(cameraId);\n }\n }\n for (var cameraId in cameras) {\n if (!(cameraId in configDocument.value.cameras)) {\n console.log(\"Deleting camera\", camera);\n if (cameras[cameraId].snapshotDocument) {\n cameras[cameraId].snapshotDocument.removeAllListeners('updated');\n }\n if (cameras[cameraId].controlMap) {\n cameras[cameraId].controlMap.removeAllListeners('itemUpdated');\n }\n delete cameras[cameraId];\n }\n }\n\n return invalidCameras;\n }\n\n function cameraInfoCheck(camera, callback) {\n if (!camera.id || !camera.id.match(/^[a-zA-Z0-9]+$/)) { callback(\"camera id is invalid: \" + camera.id); return false; }\n if (!camera.name) { callback(\"camera name is not specified\"); return false; }\n if (!camera.contact_number || !camera.contact_number.match(/^[0-9]+$/)) { callback(\"camera contact number is invalid(only digits allowed): \" + camera.contact_number); return false; }\n if (!camera.twilio_sim_sid || !camera.twilio_sim_sid.match(/^DE[a-z0-9]{32}$/)) { callback(\"camera sim SID is invalid: \" + camera.twilio_sim_sid); return false; }\n return true;\n }\n\n function genToken() {\n var token = randomString(16);\n var hash = crypto.createHash('sha512').update(token).digest(\"hex\");\n return { token: token, hash: hash };\n }\n\n return {\n initialized: $.Deferred(),\n\n cameras: cameras,\n\n updateToken: function (cb) {\n var that = this;\n return $.get(\"/userauthenticate?\" + auth, function (result) {\n if (result.success) {\n console.log(\"token updated:\", result);\n token = result.token;\n if (syncClient) {\n syncClient.updateToken(token);\n } else {\n syncClient = new SyncClient(token);\n }\n if (cb) cb(token);\n setTimeout(that.updateToken.bind(that), result.ttl*1000 * 0.96); // update token slightly in adance of ttl\n } else {\n console.error(\"failed to authenticate the user: \", result.error);\n }\n }).fail(function (jqXHR, textStatus, error) {\n console.error(\"failed to send authentication request:\", textStatus, error);\n setTimeout(that.updateToken.bind(that), 10000); // retry in 10 seconds\n });\n },\n\n fetchConfiguration: function () {\n return syncClient.document(APP_CONFIGURATION_DOCUMENT_NAME).then(function (doc) {\n configDocument = doc;\n var newDoc = null;\n var invalidCameras;\n\n if (doc.value.cameras) {\n invalidCameras = loadCameras();\n if (invalidCameras.length) {\n if (null === newDoc) newDoc = $.extend(true, doc.value, {});\n invalidCameras.forEach(function (idOfInvalidCamera) {\n delete newDoc.cameras[idOfInvalidCamera];\n });\n }\n } else {\n console.warn(\"cameras is not configured, creating an empty list\");\n if (null === newDoc) newDoc = $.extend(true, doc.value, {});\n newDoc.cameras = {};\n }\n return newDoc;\n }).then(function (newDoc) {\n if (newDoc !== null) {\n return configDocument.set(newDoc).then(function () {\n console.log(\"app configuration updated with new value:\", newDoc);\n });\n }\n });\n },\n\n addCamera: function (newCamera, callback) {\n if (!cameraInfoCheck(newCamera, callback)) return;\n if (newCamera.id in configDocument.value.cameras) return callback(\"Camera with the same ID exists\");\n newCamera.created_at = (new Date()).getTime();\n\n var t = genToken();\n newCamera.hash = t.hash;\n\n configDocument.mutate(function (remoteData) {\n if (!remoteData.cameras) remoteData.cameras = {};\n remoteData.cameras[newCamera.id] = newCamera;\n return remoteData;\n }).then(function () {\n // create necessary objects\n return Promise.all([\n syncClient.map(CAMERA_CONTROL_MAP_NAME(newCamera.id)).then(function (controlMap) {\n return Promise.all[\n controlMap.set('alarm', { id: -1 }),\n controlMap.set('arm', { enabled: true, responded_alarm: -1}),\n controlMap.set('preview', { enabled : false })\n ];\n }),\n syncClient.list(CAMERA_ALERTS_LIST_NAME(newCamera.id)),\n ]);\n }).then(function () {\n loadCameras();\n // make token temporarily visible\n callback(null, $.extend(true, cameras[newCamera.id].info, { token: t.token }));\n callbacks.refresh();\n }).catch(function (err) {\n callback(err);\n });\n },\n\n updateCamera: function (updatedCamera, callback) {\n configDocument.mutate(function (remoteData) {\n if (updatedCamera.id in remoteData.cameras) {\n remoteData.cameras[updatedCamera.id] = $.extend(true, updatedCamera, {\n hash: remoteData.cameras[updatedCamera.id].hash\n });\n } else {\n callback(\"Camera is not in the list\");\n }\n return remoteData;\n }).then(function () {\n loadCameras();\n callback(null);\n callbacks.refresh();\n }).catch(function (err) {\n callback(err);\n });\n },\n\n regenToken: function (cameraId, callback) {\n var t = genToken();\n configDocument.mutate(function (remoteData) {\n if (cameraId in remoteData.cameras) {\n remoteData.cameras[cameraId].hash = t.hash;\n } else {\n throw \"unknown camera: \" + cameraId;\n }\n return remoteData;\n }).then(function () {\n loadCameras();\n // make token temporarily visible\n callback($.extend(true, cameras[cameraId].info, { token: t.token }));\n callbacks.refresh();\n }).catch(function (err) {\n // ignore error\n console.error(\"regenToken\", err);\n });\n },\n\n deleteCamera: function (cameraId) {\n configDocument.mutate(function (remoteData) {\n delete remoteData.cameras[cameraId];\n return remoteData;\n }).then(function () {\n loadCameras();\n callbacks.refresh();\n }).then(function () {\n syncClient.map(CAMERA_CONTROL_MAP_NAME(cameraId)).then(function (map) { map.removeMap(); });\n syncClient.list(CAMERA_ALERTS_LIST_NAME(cameraId)).then(function (list) { list.removeList(); });\n });\n },\n\n controlPreview: function (cameraId) {\n var camera = cameras[cameraId];\n camera.controlMap.set(\"preview\", camera.control.preview)\n .then(function () {\n console.log(\"switchPreview updated\", cameraId, camera.control.preview);\n }).catch(function (err) {\n console.err(\"switchPreview failed\", err);\n });\n },\n\n controlArm: function (cameraId) {\n var camera = cameras[cameraId];\n camera.controlMap.set(\"arm\", camera.control.arm)\n .then(function () {\n console.log(\"switchArm updated\", cameraId, camera.control.arm);\n }).catch(function (err) {\n console.err(\"switchArm failed\", err);\n });\n },\n\n disarm: function (cameraId) {\n var camera = cameras[cameraId];\n camera.controlMap.set(\"arm\", {\n enabled: camera.control.arm.enabled,\n responded_alarm: camera.control.alarm.id\n })\n .then(function () {\n console.log(\"disarm updated\", cameraId, camera.control.arm);\n }).catch(function (err) {\n console.err(\"disarm failed\", err);\n });\n },\n\n getAlerts: function (cameraId, callback) {\n syncClient.list(CAMERA_ALERTS_LIST_NAME(cameraId)).then(function (list) {\n return list.getItems({ order: \"desc\" }).then(function (page) {\n console.log(\"getAlerts\", page);\n callback(page.items);\n });\n }).catch(function (err) {\n console.error(\"getAlerts failed\", err);\n });\n },\n\n getNextArchivedSnapshot: function (cameraId, alertId, archiveId, callback) {\n syncClient.list(CAMERA_ARCHIVES_LIST_NAME(cameraId, alertId)).then(function (list) {\n return list.get(archiveId);\n }).then(function (item) {\n fetchSnapshotTmpUrl(item.data.value.mcs_url, function (snapshotTmpUrl) {\n callback(snapshotTmpUrl);\n });\n }).catch(function (err) {\n console.info(\"getNextArchivedSnapshot failed\", err);\n callback(null);\n });\n },\n\n init: function () {\n var that = this;\n this.updateToken(function (token) {\n that.fetchConfiguration().then(function () {\n callbacks.refresh();\n }).then(function () {\n that.initialized.resolve();\n });\n });\n }\n };\n};\n\n\n/***/ }),\n\n/***/ 203:\n/***/ (function(module, exports, __webpack_require__) {\n\nvar $ = __webpack_require__(4);\n\nvar cameraListView = {\n templateUrl: __webpack_require__(295),\n\n init: function (app, $scope) {\n $scope.cameras = app.cameras;\n $scope.newCamera = {};\n $scope.addCamera = function () {\n app.addCamera(angular.copy($scope.newCamera), function (err, cameraAdded) {\n if (err) {\n $('#add-camera-failed').text(JSON.stringify(err)); \n } else {\n $scope.editedCameraInfo = cameraAdded;\n $('.add-camera').hide();\n $('.add-camera-show').fadeIn(333); \n $scope.$apply();\n }\n });\n };\n $scope.editCamera = function (cameraId) {\n $scope.editedCameraInfo = app.cameras[cameraId].info;\n $('.edit-camera').fadeIn(333);\n };\n $scope.updateCamera = function () {\n app.updateCamera(angular.copy($scope.editedCameraInfo), function (err) {\n if (err) {\n $('#edit-camera-failed').text(JSON.stringify(err)); \n } else {\n $('.edit-camera').hide();\n $scope.$apply();\n }\n });\n };\n $scope.deleteCamera = function (cameraId) {\n app.deleteCamera(cameraId);\n };\n $scope.regenTokenForCamera = function (cameraId) {\n app.regenToken(cameraId, function (cameraUpdated) {\n $scope.editedCameraInfo = cameraUpdated;\n });\n };\n\n $('.add-camera-show').click(function() {\n $(this).hide();\n $('.add-camera').fadeIn(333);\n });\n $('.add-camera-cancel').click(function() {\n $('.add-camera').hide();\n $('.add-camera-show').fadeIn(333);\n });\n\n $('.edit-camera-cancel').click(function() {\n $scope.editedCameraInfo = null;\n $('.edit-camera').hide();\n $scope.$apply();\n });\n },\n};\n\nmodule.exports = cameraListView;\n\n\n/***/ }),\n\n/***/ 204:\n/***/ (function(module, exports, __webpack_require__) {\n\nvar $ = __webpack_require__(4);\n\nvar cameraView = {\n templateUrl: __webpack_require__(296),\n\n init: function (app, cameraId, $scope) {\n $scope.camera = app.cameras[cameraId];\n $scope.selectedAlertChanged = function () {\n console.log(\"selectedAlertChanged\", $scope.selectedAlertId);\n $scope.selectedArchiveId = 0;\n $scope.selectNewArchiveId(0);\n $scope.snapshotTmpUrl = null;\n };\n $scope.selectNewArchiveId = function (newArchiveId) {\n console.log(\"selectNewArchiveId start\", $scope.selectedAlertId, newArchiveId);\n app.getNextArchivedSnapshot(cameraId, $scope.selectedAlertId, newArchiveId, function (snapshotTmpUrl) {\n if (snapshotTmpUrl) {\n console.log(\"selectNewArchiveId accepted\", $scope.selectedAlertId, snapshotTmpUrl);\n $scope.selectedArchiveId = newArchiveId;\n $scope.snapshotTmpUrl = snapshotTmpUrl;\n $scope.$apply();\n } else {\n console.log(\"selectNewArchiveId rejected\", $scope.selectedAlertId);\n }\n });\n };\n $scope.selectPreviousArchive = function () {\n if ($scope.selectedArchiveId > 0) {\n $scope.selectNewArchiveId($scope.selectedArchiveId - 1);\n }\n };\n $scope.selectNextArchive = function () {\n $scope.selectNewArchiveId($scope.selectedArchiveId + 1);\n };\n app.getAlerts(cameraId, function (alerts) {\n $scope.alerts = alerts;\n $scope.selectedAlertId = alerts[0].data.index+\"\";\n $scope.$apply();\n $scope.selectedAlertChanged();\n });\n }\n};\n\nmodule.exports = cameraView;\n\n\n/***/ }),\n\n/***/ 205:\n/***/ (function(module, exports, __webpack_require__) {\n\nvar dashboardView = {\n templateUrl: __webpack_require__(297),\n\n init: function (app, $scope) {\n $scope.cameras = app.cameras;\n $scope.modes = [\"live-feed\", \"arm\"];\n $scope.noCamera = function () { return Object.keys(app.cameras).length === 0; }\n $scope.update = function(value, camera) {\n if (value == \"live-feed\") {\n camera.control.preview.enabled = true;\n camera.control.arm.enabled = false;\n }\n else {\n camera.control.preview.enabled = false;\n camera.control.arm.enabled = true; \n }\n app.controlPreview(camera.info.id);\n app.controlArm(camera.info.id);\n console.log(camera.info)\n };\n $scope.img_oninit = function (camera) {\n camera.snapshotLoadingInProgress = false;\n };\n $scope.img_onloaded = function (camera) {\n console.info(\"Image loaded: \" + camera.snapshot.img_url);\n camera.snapshotLoadingInProgress = false;\n };\n $scope.switchPreview = function (cameraId) {\n app.controlPreview(cameraId);\n };\n $scope.switchArm = function (cameraId) {\n app.controlArm(cameraId);\n };\n $scope.disarm = function (cameraId) {\n app.disarm(cameraId);\n };\n }\n};\n\nmodule.exports = dashboardView;\n\n\n/***/ }),\n\n/***/ 207:\n/***/ (function(module, exports, __webpack_require__) {\n\nmodule.exports = __webpack_require__.p + \"index.html\";\n\n/***/ }),\n\n/***/ 208:\n/***/ (function(module, exports) {\n\nmodule.exports = angular;\n\n/***/ }),\n\n/***/ 209:\n/***/ (function(module, exports) {\n\nmodule.exports = undefined;\n\n/***/ }),\n\n/***/ 210:\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n\n\nconst $ = __webpack_require__(4);\nconst angular = __webpack_require__(208);\nconst moment = __webpack_require__(0);\n__webpack_require__(209);\n\nconst MOMENT_FORMAT = \"MMM DD YYYY @ hh:mm\";\n\n// style sheets\n__webpack_require__(30);\n__webpack_require__(31);\n\n// index.html\n__webpack_require__(207);\n\nconst dashboardView = __webpack_require__(205);\nconst cameraListView = __webpack_require__(203);\nconst cameraView = __webpack_require__(204);\n\nvar currentView;\nvar $currentViewScope;\n\nvar App = __webpack_require__(202);\nwindow.app = new App({\n refresh: function () {\n $currentViewScope.$apply();\n }\n});\n\nangular\n .module(\"app\", [\n 'ngRoute'\n ])\n .controller('DashboardViewCtrl', ['$scope', function ($scope) {\n $currentViewScope = $scope;\n currentView = dashboardView;\n $.when(app.initialized).done(function () {\n dashboardView.init(app, $scope);\n });\n }])\n .controller('CameraListViewCtrl', ['$scope', function ($scope) {\n $currentViewScope = $scope;\n currentView = cameraListView;\n $.when(app.initialized).done(function () {\n cameraListView.init(app, $scope);\n });\n }])\n .controller('CameraView', ['$routeParams', '$scope', function ($routeParams, $scope) {\n $currentViewScope = $scope;\n currentView = cameraView;\n $.when(app.initialized).done(function () {\n cameraView.init(app, $routeParams.id, $scope);\n });\n }])\n .config(['$routeProvider', function ($routeProvider) {\n $routeProvider\n .when('/dashboard', { controller: 'DashboardViewCtrl', templateUrl: dashboardView.templateUrl } )\n .when('/cameras', { controller: 'CameraListViewCtrl', templateUrl: cameraListView.templateUrl } )\n .when('/cameras/:id', { controller: 'CameraView', templateUrl: cameraView.templateUrl } )\n .otherwise({ redirectTo: '/dashboard' }); \n }])\n .filter('moment', function () {\n return function (datestr) {\n return moment(datestr).format(MOMENT_FORMAT);\n };\n })\n .directive('imageloaded', function() {\n return {\n restrict: 'A',\n link: function(scope, element, attrs) {\n element.bind('load', function() {\n //call the function that was passed\n scope.$apply(attrs.imageloaded);\n });\n }\n };\n }); \n\n\n/***/ }),\n\n/***/ 295:\n/***/ (function(module, exports) {\n\nvar path = 'views/camera_list.html';\nvar html = \"\\n
\\n \\n\\n
\\n
\\n \\n \\n Id | \\n Name | \\n Phone Number | \\n SIM Sid | \\n Token | \\n Created | \\n Actions | \\n
\\n \\n \\n \\n {{ camera.info.id }} | \\n {{ camera.info.name }} | \\n {{ camera.info.contact_number }} | \\n {{ camera.info.twilio_sim_sid }} | \\n \\n {{ editedCameraInfo.token }}\\n \\n | \\n {{ camera.info.created_at | moment }} | \\n \\n \\n \\n \\n | \\n
\\n \\n
\\n
< >
\\n
\\n
\\n\\n\\n\\n\\n\\n\\n\";\nwindow.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]);\nmodule.exports = path;\n\n/***/ }),\n\n/***/ 296:\n/***/ (function(module, exports) {\n\nvar path = 'views/camera_view.html';\nvar html = \"\\n
No Security Camera Alerts
\\n
No notifications have been logged.
\\n
\\n\\n\\n
{{ camera.info.name }}
\\n\\n
\\n Select an alert to view\\n \\n \\n
\\n\\n
\\n
\\n
![]()
\\n
\\n
\\n\\n
\\n
\\n\";\nwindow.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]);\nmodule.exports = path;\n\n/***/ }),\n\n/***/ 297:\n/***/ (function(module, exports) {\n\nvar path = 'views/dashboard.html';\nvar html = \"Active Security Cameras
\\n\\nWe aren't tracking any Security Cameras right now. Click the Add Security Camera button to get started.
\\n\\n\\n
\\n {{ camera.info.name }}\\n
\\n Select the mode for your camera\\n \\n
\\n \\n\\n
\\n
\\n
\\n
![]()
\\n
\\n
\\n
\\n
\\n
\\n
\\n No Active Alarm\\n
\\n
\\n
\\n
\\n
\\n
\\n\";\nwindow.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]);\nmodule.exports = path;\n\n/***/ }),\n\n/***/ 322:\n/***/ (function(module, exports) {\n\nmodule.exports = Twilio.Sync;\n\n/***/ })\n\n},[210]);\n\n\n// WEBPACK FOOTER //\n// index.js","'use strict';\n\nconst APP_CONFIGURATION_DOCUMENT_NAME = \"app.configuration\";\nfunction CAMERA_SNAPSHOT_DOCUMENT_NAME(cameraId) { return \"cameras.\" + cameraId + \".snapshot\"; }\nfunction CAMERA_CONTROL_MAP_NAME(cameraId) { return \"cameras.\" + cameraId + \".control\"; }\nfunction CAMERA_ALERTS_LIST_NAME(cameraId) { return \"cameras.\" + cameraId + \".alerts\"; }\nfunction CAMERA_ARCHIVES_LIST_NAME(cameraId, alertId) { return \"cameras.\" + cameraId + \".archives.\" + alertId; }\n\nmodule.exports = function(callbacks) {\n const $ = require(\"jquery\");\n const crypto = require(\"crypto\");\n const SyncClient = require(\"twilio-sync\").Client;\n var syncClient;\n var token;\n var auth = \"username=twilio&pincode=928462\";\n var configDocument;\n\n var cameras = {};\n\n function randomString(len) {\n var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n var randomString = '';\n for (var i = 0; i < len; i++) {\n var randomPoz = Math.floor(Math.random() * charSet.length);\n randomString += charSet.substring(randomPoz,randomPoz+1);\n }\n return randomString;\n }\n\n function fetchSnapshotTmpUrl(mcs_url, callback) {\n $.ajax({\n type: \"GET\",\n url: mcs_url,\n dataType: 'json',\n beforeSend: function (xhr) { xhr.setRequestHeader('X-Twilio-Token', token); },\n success: function (data, status, xhr) {\n callback(data.links.content_direct_temporary);\n }\n });\n }\n\n function fetchCameraSnapshotTmpUrl(camera) {\n fetchSnapshotTmpUrl(camera.snapshot.mcs_url, function (snapshotTmpUrl) {\n // do not load new image when last image loading is in progress\n // otherwise user might experience stalling camera view\n if (!camera.snapshotLoadingInProgress) {\n camera.snapshotLoadingInProgress = true;\n camera.snapshot.img_url = snapshotTmpUrl;\n console.info(\"Loading image: \" + camera.snapshot.img_url);\n }\n callbacks.refresh();\n });\n }\n\n function fetchSnapshot(camera) {\n syncClient.document(CAMERA_SNAPSHOT_DOCUMENT_NAME(camera.info.id)).then(function (doc) {\n camera.snapshotDocument = doc;\n camera.snapshot = doc.value;\n fetchCameraSnapshotTmpUrl(camera);\n doc.on(\"updated\", function (data) {\n console.log(\"camera snapshot updated\", camera.info.id, JSON.stringify(data));\n camera.snapshot = data;\n fetchCameraSnapshotTmpUrl(camera);\n });\n });\n }\n\n function fetchControl(camera) {\n syncClient.map(CAMERA_CONTROL_MAP_NAME(camera.info.id)).then(function (map) {\n Promise.all([\n map.get(\"preview\"),\n map.get(\"alarm\"),\n map.get(\"arm\")\n ]).then(function (items) {\n camera.controlMap = map;\n camera.control = {\n preview : items[0].value,\n alarm : items[1].value,\n arm : items[2].value,\n };\n console.log(\"camera control fetched\", camera.info.id, JSON.stringify(camera.control));\n map.on(\"itemUpdated\", function (data) {\n console.log(\"camera control updated\", camera.info.id, data.key, JSON.stringify(data.value));\n camera.control[data.key] = data.value;\n callbacks.refresh();\n });\n callbacks.refresh();\n });\n });\n }\n\n function loadCameras() {\n var invalidCameras = [];\n\n for (var cameraId in configDocument.value.cameras) {\n var camera = configDocument.value.cameras[cameraId];\n if (camera.id === cameraId &&\n typeof (camera.name) === \"string\" &&\n typeof(camera.contact_number) === \"string\" &&\n typeof(camera.twilio_sim_sid) === \"string\") {\n if (cameraId in cameras) {\n if (camera.name !== cameras[cameraId].info.name ||\n camera.contact_number !== cameras[cameraId].info.contact_number ||\n camera.twilio_sim_sid !== cameras[cameraId].info.twilio_sim_sid) {\n console.log(\"Updating camera\", camera);\n cameras[cameraId].info = camera;\n }\n } else {\n console.log(\"Loading new camera\", camera);\n cameras[cameraId] = {\n info: camera\n };\n fetchSnapshot(cameras[cameraId]);\n fetchControl(cameras[cameraId]);\n }\n } else {\n console.warn(\"Invalid camera configuration, removing from the list: \", cameraId, camera);\n invalidCameras.push(cameraId);\n }\n }\n for (var cameraId in cameras) {\n if (!(cameraId in configDocument.value.cameras)) {\n console.log(\"Deleting camera\", camera);\n if (cameras[cameraId].snapshotDocument) {\n cameras[cameraId].snapshotDocument.removeAllListeners('updated');\n }\n if (cameras[cameraId].controlMap) {\n cameras[cameraId].controlMap.removeAllListeners('itemUpdated');\n }\n delete cameras[cameraId];\n }\n }\n\n return invalidCameras;\n }\n\n function cameraInfoCheck(camera, callback) {\n if (!camera.id || !camera.id.match(/^[a-zA-Z0-9]+$/)) { callback(\"camera id is invalid: \" + camera.id); return false; }\n if (!camera.name) { callback(\"camera name is not specified\"); return false; }\n if (!camera.contact_number || !camera.contact_number.match(/^[0-9]+$/)) { callback(\"camera contact number is invalid(only digits allowed): \" + camera.contact_number); return false; }\n if (!camera.twilio_sim_sid || !camera.twilio_sim_sid.match(/^DE[a-z0-9]{32}$/)) { callback(\"camera sim SID is invalid: \" + camera.twilio_sim_sid); return false; }\n return true;\n }\n\n function genToken() {\n var token = randomString(16);\n var hash = crypto.createHash('sha512').update(token).digest(\"hex\");\n return { token: token, hash: hash };\n }\n\n return {\n initialized: $.Deferred(),\n\n cameras: cameras,\n\n updateToken: function (cb) {\n var that = this;\n return $.get(\"/userauthenticate?\" + auth, function (result) {\n if (result.success) {\n console.log(\"token updated:\", result);\n token = result.token;\n if (syncClient) {\n syncClient.updateToken(token);\n } else {\n syncClient = new SyncClient(token);\n }\n if (cb) cb(token);\n setTimeout(that.updateToken.bind(that), result.ttl*1000 * 0.96); // update token slightly in adance of ttl\n } else {\n console.error(\"failed to authenticate the user: \", result.error);\n }\n }).fail(function (jqXHR, textStatus, error) {\n console.error(\"failed to send authentication request:\", textStatus, error);\n setTimeout(that.updateToken.bind(that), 10000); // retry in 10 seconds\n });\n },\n\n fetchConfiguration: function () {\n return syncClient.document(APP_CONFIGURATION_DOCUMENT_NAME).then(function (doc) {\n configDocument = doc;\n var newDoc = null;\n var invalidCameras;\n\n if (doc.value.cameras) {\n invalidCameras = loadCameras();\n if (invalidCameras.length) {\n if (null === newDoc) newDoc = $.extend(true, doc.value, {});\n invalidCameras.forEach(function (idOfInvalidCamera) {\n delete newDoc.cameras[idOfInvalidCamera];\n });\n }\n } else {\n console.warn(\"cameras is not configured, creating an empty list\");\n if (null === newDoc) newDoc = $.extend(true, doc.value, {});\n newDoc.cameras = {};\n }\n return newDoc;\n }).then(function (newDoc) {\n if (newDoc !== null) {\n return configDocument.set(newDoc).then(function () {\n console.log(\"app configuration updated with new value:\", newDoc);\n });\n }\n });\n },\n\n addCamera: function (newCamera, callback) {\n if (!cameraInfoCheck(newCamera, callback)) return;\n if (newCamera.id in configDocument.value.cameras) return callback(\"Camera with the same ID exists\");\n newCamera.created_at = (new Date()).getTime();\n\n var t = genToken();\n newCamera.hash = t.hash;\n\n configDocument.mutate(function (remoteData) {\n if (!remoteData.cameras) remoteData.cameras = {};\n remoteData.cameras[newCamera.id] = newCamera;\n return remoteData;\n }).then(function () {\n // create necessary objects\n return Promise.all([\n syncClient.map(CAMERA_CONTROL_MAP_NAME(newCamera.id)).then(function (controlMap) {\n return Promise.all[\n controlMap.set('alarm', { id: -1 }),\n controlMap.set('arm', { enabled: true, responded_alarm: -1}),\n controlMap.set('preview', { enabled : false })\n ];\n }),\n syncClient.list(CAMERA_ALERTS_LIST_NAME(newCamera.id)),\n ]);\n }).then(function () {\n loadCameras();\n // make token temporarily visible\n callback(null, $.extend(true, cameras[newCamera.id].info, { token: t.token }));\n callbacks.refresh();\n }).catch(function (err) {\n callback(err);\n });\n },\n\n updateCamera: function (updatedCamera, callback) {\n configDocument.mutate(function (remoteData) {\n if (updatedCamera.id in remoteData.cameras) {\n remoteData.cameras[updatedCamera.id] = $.extend(true, updatedCamera, {\n hash: remoteData.cameras[updatedCamera.id].hash\n });\n } else {\n callback(\"Camera is not in the list\");\n }\n return remoteData;\n }).then(function () {\n loadCameras();\n callback(null);\n callbacks.refresh();\n }).catch(function (err) {\n callback(err);\n });\n },\n\n regenToken: function (cameraId, callback) {\n var t = genToken();\n configDocument.mutate(function (remoteData) {\n if (cameraId in remoteData.cameras) {\n remoteData.cameras[cameraId].hash = t.hash;\n } else {\n throw \"unknown camera: \" + cameraId;\n }\n return remoteData;\n }).then(function () {\n loadCameras();\n // make token temporarily visible\n callback($.extend(true, cameras[cameraId].info, { token: t.token }));\n callbacks.refresh();\n }).catch(function (err) {\n // ignore error\n console.error(\"regenToken\", err);\n });\n },\n\n deleteCamera: function (cameraId) {\n configDocument.mutate(function (remoteData) {\n delete remoteData.cameras[cameraId];\n return remoteData;\n }).then(function () {\n loadCameras();\n callbacks.refresh();\n }).then(function () {\n syncClient.map(CAMERA_CONTROL_MAP_NAME(cameraId)).then(function (map) { map.removeMap(); });\n syncClient.list(CAMERA_ALERTS_LIST_NAME(cameraId)).then(function (list) { list.removeList(); });\n });\n },\n\n controlPreview: function (cameraId) {\n var camera = cameras[cameraId];\n camera.controlMap.set(\"preview\", camera.control.preview)\n .then(function () {\n console.log(\"switchPreview updated\", cameraId, camera.control.preview);\n }).catch(function (err) {\n console.err(\"switchPreview failed\", err);\n });\n },\n\n controlArm: function (cameraId) {\n var camera = cameras[cameraId];\n camera.controlMap.set(\"arm\", camera.control.arm)\n .then(function () {\n console.log(\"switchArm updated\", cameraId, camera.control.arm);\n }).catch(function (err) {\n console.err(\"switchArm failed\", err);\n });\n },\n\n disarm: function (cameraId) {\n var camera = cameras[cameraId];\n camera.controlMap.set(\"arm\", {\n enabled: camera.control.arm.enabled,\n responded_alarm: camera.control.alarm.id\n })\n .then(function () {\n console.log(\"disarm updated\", cameraId, camera.control.arm);\n }).catch(function (err) {\n console.err(\"disarm failed\", err);\n });\n },\n\n getAlerts: function (cameraId, callback) {\n syncClient.list(CAMERA_ALERTS_LIST_NAME(cameraId)).then(function (list) {\n return list.getItems({ order: \"desc\" }).then(function (page) {\n console.log(\"getAlerts\", page);\n callback(page.items);\n });\n }).catch(function (err) {\n console.error(\"getAlerts failed\", err);\n });\n },\n\n getNextArchivedSnapshot: function (cameraId, alertId, archiveId, callback) {\n syncClient.list(CAMERA_ARCHIVES_LIST_NAME(cameraId, alertId)).then(function (list) {\n return list.get(archiveId);\n }).then(function (item) {\n fetchSnapshotTmpUrl(item.data.value.mcs_url, function (snapshotTmpUrl) {\n callback(snapshotTmpUrl);\n });\n }).catch(function (err) {\n console.info(\"getNextArchivedSnapshot failed\", err);\n callback(null);\n });\n },\n\n init: function () {\n var that = this;\n this.updateToken(function (token) {\n that.fetchConfiguration().then(function () {\n callbacks.refresh();\n }).then(function () {\n that.initialized.resolve();\n });\n });\n }\n };\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./js/app.js\n// module id = 202\n// module chunks = 0","var $ = require(\"jquery\");\n\nvar cameraListView = {\n templateUrl: require(\"../views/camera_list.html\"),\n\n init: function (app, $scope) {\n $scope.cameras = app.cameras;\n $scope.newCamera = {};\n $scope.addCamera = function () {\n app.addCamera(angular.copy($scope.newCamera), function (err, cameraAdded) {\n if (err) {\n $('#add-camera-failed').text(JSON.stringify(err)); \n } else {\n $scope.editedCameraInfo = cameraAdded;\n $('.add-camera').hide();\n $('.add-camera-show').fadeIn(333); \n $scope.$apply();\n }\n });\n };\n $scope.editCamera = function (cameraId) {\n $scope.editedCameraInfo = app.cameras[cameraId].info;\n $('.edit-camera').fadeIn(333);\n };\n $scope.updateCamera = function () {\n app.updateCamera(angular.copy($scope.editedCameraInfo), function (err) {\n if (err) {\n $('#edit-camera-failed').text(JSON.stringify(err)); \n } else {\n $('.edit-camera').hide();\n $scope.$apply();\n }\n });\n };\n $scope.deleteCamera = function (cameraId) {\n app.deleteCamera(cameraId);\n };\n $scope.regenTokenForCamera = function (cameraId) {\n app.regenToken(cameraId, function (cameraUpdated) {\n $scope.editedCameraInfo = cameraUpdated;\n });\n };\n\n $('.add-camera-show').click(function() {\n $(this).hide();\n $('.add-camera').fadeIn(333);\n });\n $('.add-camera-cancel').click(function() {\n $('.add-camera').hide();\n $('.add-camera-show').fadeIn(333);\n });\n\n $('.edit-camera-cancel').click(function() {\n $scope.editedCameraInfo = null;\n $('.edit-camera').hide();\n $scope.$apply();\n });\n },\n};\n\nmodule.exports = cameraListView;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./js/cameraListView.js\n// module id = 203\n// module chunks = 0","var $ = require(\"jquery\");\n\nvar cameraView = {\n templateUrl: require(\"../views/camera_view.html\"),\n\n init: function (app, cameraId, $scope) {\n $scope.camera = app.cameras[cameraId];\n $scope.selectedAlertChanged = function () {\n console.log(\"selectedAlertChanged\", $scope.selectedAlertId);\n $scope.selectedArchiveId = 0;\n $scope.selectNewArchiveId(0);\n $scope.snapshotTmpUrl = null;\n };\n $scope.selectNewArchiveId = function (newArchiveId) {\n console.log(\"selectNewArchiveId start\", $scope.selectedAlertId, newArchiveId);\n app.getNextArchivedSnapshot(cameraId, $scope.selectedAlertId, newArchiveId, function (snapshotTmpUrl) {\n if (snapshotTmpUrl) {\n console.log(\"selectNewArchiveId accepted\", $scope.selectedAlertId, snapshotTmpUrl);\n $scope.selectedArchiveId = newArchiveId;\n $scope.snapshotTmpUrl = snapshotTmpUrl;\n $scope.$apply();\n } else {\n console.log(\"selectNewArchiveId rejected\", $scope.selectedAlertId);\n }\n });\n };\n $scope.selectPreviousArchive = function () {\n if ($scope.selectedArchiveId > 0) {\n $scope.selectNewArchiveId($scope.selectedArchiveId - 1);\n }\n };\n $scope.selectNextArchive = function () {\n $scope.selectNewArchiveId($scope.selectedArchiveId + 1);\n };\n app.getAlerts(cameraId, function (alerts) {\n $scope.alerts = alerts;\n $scope.selectedAlertId = alerts[0].data.index+\"\";\n $scope.$apply();\n $scope.selectedAlertChanged();\n });\n }\n};\n\nmodule.exports = cameraView;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./js/cameraView.js\n// module id = 204\n// module chunks = 0","var dashboardView = {\n templateUrl: require(\"../views/dashboard.html\"),\n\n init: function (app, $scope) {\n $scope.cameras = app.cameras;\n $scope.modes = [\"live-feed\", \"arm\"];\n $scope.noCamera = function () { return Object.keys(app.cameras).length === 0; }\n $scope.update = function(value, camera) {\n if (value == \"live-feed\") {\n camera.control.preview.enabled = true;\n camera.control.arm.enabled = false;\n }\n else {\n camera.control.preview.enabled = false;\n camera.control.arm.enabled = true; \n }\n app.controlPreview(camera.info.id);\n app.controlArm(camera.info.id);\n console.log(camera.info)\n };\n $scope.img_oninit = function (camera) {\n camera.snapshotLoadingInProgress = false;\n };\n $scope.img_onloaded = function (camera) {\n console.info(\"Image loaded: \" + camera.snapshot.img_url);\n camera.snapshotLoadingInProgress = false;\n };\n $scope.switchPreview = function (cameraId) {\n app.controlPreview(cameraId);\n };\n $scope.switchArm = function (cameraId) {\n app.controlArm(cameraId);\n };\n $scope.disarm = function (cameraId) {\n app.disarm(cameraId);\n };\n }\n};\n\nmodule.exports = dashboardView;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./js/dashboardView.js\n// module id = 205\n// module chunks = 0","module.exports = __webpack_public_path__ + \"index.html\";\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./index.html\n// module id = 207\n// module chunks = 0","module.exports = angular;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"angular\"\n// module id = 208\n// module chunks = 0","module.exports = undefined;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external {\"amd\":\"angular-route\"}\n// module id = 209\n// module chunks = 0","'use strict';\n\nconst $ = require(\"jquery\");\nconst angular = require(\"angular\");\nconst moment = require(\"moment\");\nrequire(\"angular-route\");\n\nconst MOMENT_FORMAT = \"MMM DD YYYY @ hh:mm\";\n\n// style sheets\nrequire(\"bootstrap-webpack\");\nrequire(\"../scss/main.scss\");\n\n// index.html\nrequire(\"../index.html\");\n\nconst dashboardView = require(\"./dashboardView\");\nconst cameraListView = require(\"./cameraListView\");\nconst cameraView = require(\"./cameraView\");\n\nvar currentView;\nvar $currentViewScope;\n\nvar App = require(\"./app\");\nwindow.app = new App({\n refresh: function () {\n $currentViewScope.$apply();\n }\n});\n\nangular\n .module(\"app\", [\n 'ngRoute'\n ])\n .controller('DashboardViewCtrl', ['$scope', function ($scope) {\n $currentViewScope = $scope;\n currentView = dashboardView;\n $.when(app.initialized).done(function () {\n dashboardView.init(app, $scope);\n });\n }])\n .controller('CameraListViewCtrl', ['$scope', function ($scope) {\n $currentViewScope = $scope;\n currentView = cameraListView;\n $.when(app.initialized).done(function () {\n cameraListView.init(app, $scope);\n });\n }])\n .controller('CameraView', ['$routeParams', '$scope', function ($routeParams, $scope) {\n $currentViewScope = $scope;\n currentView = cameraView;\n $.when(app.initialized).done(function () {\n cameraView.init(app, $routeParams.id, $scope);\n });\n }])\n .config(['$routeProvider', function ($routeProvider) {\n $routeProvider\n .when('/dashboard', { controller: 'DashboardViewCtrl', templateUrl: dashboardView.templateUrl } )\n .when('/cameras', { controller: 'CameraListViewCtrl', templateUrl: cameraListView.templateUrl } )\n .when('/cameras/:id', { controller: 'CameraView', templateUrl: cameraView.templateUrl } )\n .otherwise({ redirectTo: '/dashboard' }); \n }])\n .filter('moment', function () {\n return function (datestr) {\n return moment(datestr).format(MOMENT_FORMAT);\n };\n })\n .directive('imageloaded', function() {\n return {\n restrict: 'A',\n link: function(scope, element, attrs) {\n element.bind('load', function() {\n //call the function that was passed\n scope.$apply(attrs.imageloaded);\n });\n }\n };\n }); \n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./js/index.js\n// module id = 210\n// module chunks = 0","var path = 'views/camera_list.html';\nvar html = \"\\n
\\n \\n\\n
\\n
\\n \\n \\n Id | \\n Name | \\n Phone Number | \\n SIM Sid | \\n Token | \\n Created | \\n Actions | \\n
\\n \\n \\n \\n {{ camera.info.id }} | \\n {{ camera.info.name }} | \\n {{ camera.info.contact_number }} | \\n {{ camera.info.twilio_sim_sid }} | \\n \\n {{ editedCameraInfo.token }}\\n \\n | \\n {{ camera.info.created_at | moment }} | \\n \\n \\n \\n \\n | \\n
\\n \\n
\\n
< >
\\n
\\n
\\n\\n\\n\\n\\n\\n\\n\";\nwindow.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]);\nmodule.exports = path;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./views/camera_list.html\n// module id = 295\n// module chunks = 0","var path = 'views/camera_view.html';\nvar html = \"\\n
No Security Camera Alerts
\\n
No notifications have been logged.
\\n
\\n\\n\\n
{{ camera.info.name }}
\\n\\n
\\n Select an alert to view\\n \\n \\n
\\n\\n
\\n
\\n
![]()
\\n
\\n
\\n\\n
\\n
\\n\";\nwindow.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]);\nmodule.exports = path;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./views/camera_view.html\n// module id = 296\n// module chunks = 0","var path = 'views/dashboard.html';\nvar html = \"Active Security Cameras
\\n\\nWe aren't tracking any Security Cameras right now. Click the Add Security Camera button to get started.
\\n\\n\\n
\\n {{ camera.info.name }}\\n
\\n Select the mode for your camera\\n \\n
\\n \\n\\n
\\n
\\n
\\n
![]()
\\n
\\n
\\n
\\n
\\n
\\n
\\n No Active Alarm\\n
\\n
\\n
\\n
\\n
\\n
\\n\";\nwindow.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]);\nmodule.exports = path;\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./views/dashboard.html\n// module id = 297\n// module chunks = 0","module.exports = Twilio.Sync;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"Twilio.Sync\"\n// module id = 322\n// module chunks = 0"],"sourceRoot":""}
--------------------------------------------------------------------------------
/models/README.md:
--------------------------------------------------------------------------------
1 | # Security Camera Blueprint
2 | ### 3d-printed enclosure
3 | This Security Camera Blueprint comes with CAD files designed for this 3D printout. You can find the CAD files here. Visit [Voodoo Manufacturing](https://voodoomfg.com/) to take advantage of their developer-first API driven 3d-printers to print out the models. Use [Sculpteo](http://sculpteo.com/) to print out the acrylic to cover the sides of the Security Camera.
4 |
5 | **Note:** these files are optional and not necessary to construct this Blueprint.
6 |
7 | ### File descriptions
8 |
9 | | Filename | Description |
10 | |--------------------------------------|------------------------------------------------------------------------|
11 | | security-camera-body.stl | The enclosure for the Security Camera hardware. You need one of these. |
12 | | security-camera-cover-plate.dxf | The acrylic cover for the enclosure. You need 2 of these. |
13 | | security-camera-lens-cover-plate.dxf | The acrylic cover for the Raspberry Pi camera. You need one of these. |
14 |
15 | Full instructions for this tutorial can be found in the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/).
16 |
--------------------------------------------------------------------------------
/models/security-camera-body.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/wireless-security-camera/3f74a9a0fc787eaa08ba5dd5293e5a6a0797468a/models/security-camera-body.stl
--------------------------------------------------------------------------------
/pi/README.md:
--------------------------------------------------------------------------------
1 | # Security Camera Blueprint
2 | ### Set up the Raspberry Pi Security Camera Script
3 | The scripts located in this folder are meant to be run on the Raspberry Pi operating system to send data (images and notifications) to the Security Camera service running on Functions.
4 |
5 | Full instructions for this tutorial can be found in the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/). Below you will find the minimum steps necessary to get this up and running.
6 |
7 | Open a terminal and type the following commands:
8 |
9 | ```
10 | sudo apt-get update
11 | sudo apt-get install nodejs libopencv-dev
12 | ```
13 | The main system packages that Security Camera depends on are **nodejs** and **libopencv-dev**.
14 |
15 | 1. Create a directory named camera in /home/pi
16 | 2. Download the security-camera.js and package.json from this repository into the camera directory
17 | - An alternative is to download the files to a USB Flash Drive and transfer them to the Pi
18 | 3. From the terminal’s command line, type the following command:
19 |
20 | ```
21 | npm install
22 | ```
23 |
24 | The main Node.js packages our application depends on are **raspicam**, **opencv**, and **twilio-sync**.
25 |
26 | 1. Create an **images** directory inside the **camera** directory
27 |
28 | ```
29 | mkdir /home/pi/camera/images
30 | ```
31 |
32 | 2. Ensure that the path where you added your images folder matches the path on line 14 in the security-camera.js script
33 | 3. Change the **clientBootstrapUrl** variable on line 13 to the URL where your CameraAuthenticator function is deployed
34 | 4. Change the **cameraId** variable on line 11 to the ID used when adding the camera
35 | 5. Change the **cameraSecret** variable on line 12 to the token generated by the server application
36 | 6. Run the script by entering the following command in a terminal:
37 | ```
38 | cd /home/pi/camera
39 | npm start
40 | ```
41 |
42 | That's it! As a reminder, full instructions for this tutorial can be found in the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/).
43 |
--------------------------------------------------------------------------------
/pi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wireless-security-camera",
3 | "version": "0.0.1",
4 | "description": "Wireless Security Camera application",
5 | "engines": {
6 | "node": "5.9.1"
7 | },
8 | "main": "security-camera.js",
9 | "scripts": {
10 | "start": "node security-camera.js"
11 | },
12 | "dependencies": {
13 | "opencv": "^6.0.0",
14 | "raspicam": "^0.2.13",
15 | "request": "^2.81.0",
16 | "twilio-common": "^0.1.6",
17 | "twilio-sync": "^0.5.6",
18 | "uuid": "^3.1.0"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/twilio/wireless-security-camera"
23 | },
24 | "license": "MIT"
25 | }
26 |
--------------------------------------------------------------------------------
/pi/security-camera.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const request = require('request');
5 | const uuid = require('uuid/v4');
6 |
7 | const TwilioCommon = require('twilio-common');
8 | const SyncClient = require('twilio-sync');
9 | const RaspiCam = require("raspicam");
10 | const CV = require('opencv');
11 |
12 | const cameraId = 'your-camera-id';
13 | const cameraSecret = 'your-camera-secret';
14 | const clientBootstrapUrl = 'https://your-domain.twil.io/cameraauthenticate';
15 | const imageDirectory = '/home/pi/camera/images/';
16 |
17 | let accessManager;
18 | let config;
19 |
20 | let cameraSnapshot;
21 | let stateCapturing = false;
22 | let statePreviewing = false;
23 | let stateArmed = false;
24 |
25 | let pendingAlarm = -1;
26 | let respondedAlarm = -1;
27 |
28 | let captureSettings = {
29 | width: 640, height: 360,
30 | mode: "timelapse",
31 | awb: 'cloud',
32 | output: imageDirectory + 'camera%03d.jpg',
33 | q: 80, rot: 180, th: '0:0:0',
34 | nopreview: true,
35 | timeout: 1800000, // camera runs for 30 minutes by default
36 | timelapse: 250 // camera runs at roughly 4 fps
37 | };
38 |
39 | let capturer = new RaspiCam(captureSettings);
40 | let previousImage;
41 |
42 | function bootstrapClient(id, secret) {
43 | return new Promise(resolve => pollConfiguration(id, secret, resolve));
44 | }
45 |
46 | function pollConfiguration(id, secret, resolve) {
47 | request(clientBootstrapUrl + '?camera_id=' + id + '&camera_token=' + secret, (err, res) => {
48 | if (!err) {
49 | let response = JSON.parse(res.body);
50 | console.log('Got configuration for camera:', response.camera_id);
51 | resolve(response);
52 | } else {
53 | console.log('Failed fetching camera configuration:', err);
54 | setTimeout(() => pollConfiguration(id, secret, resolve), 2000);
55 | }
56 | });
57 | }
58 |
59 | function updateCameraState(item) {
60 | switch (item.key) {
61 | case 'preview':
62 | statePreviewing = item.value.enabled;
63 | break;
64 | case 'arm':
65 | stateArmed = item.value.enabled;
66 | respondedAlarm = item.value.responded_alarm;
67 | break;
68 | case 'alarm':
69 | pendingAlarm = item.value.id;
70 | break;
71 | }
72 | if (!stateCapturing && (statePreviewing || stateArmed)) {
73 | // start capturing images
74 | console.log("Starting camera capture");
75 | capturer.start();
76 | stateCapturing = true;
77 | } else if (stateCapturing && (!statePreviewing && !stateArmed)) {
78 | // stop capturing images
79 | console.log("Stopping camera capture");
80 | capturer.stop();
81 | stateCapturing = false;
82 | }
83 | }
84 |
85 | function uploadImage(file, token, isSticky) {
86 | return new Promise(resolve => {
87 | request({
88 | url: config.links.upload_url,
89 | method: 'POST',
90 | body: fs.createReadStream(file),
91 | qs: isSticky ? {
92 | MessageSid: 'ME' + uuid().replace(/-/g, ''),
93 | ChannelSid: cameraSnapshot.sid
94 | } : null,
95 | headers: {
96 | 'X-Twilio-Token': token,
97 | 'Content-Type': 'image/jpeg'
98 | }
99 | }, (err, res) => {
100 | let response = JSON.parse(res.body);
101 | if (err) {
102 | throw new Error(res.text);
103 | }
104 | resolve({ media: response, location: res.headers.location });
105 | });
106 | });
107 | }
108 |
109 | capturer.on("read", function(err, timeStamp, fileName) {
110 | console.log('Frame captured:', err, timeStamp, fileName);
111 | if (!fileName.endsWith('~')) {
112 | let filePath = imageDirectory + fileName;
113 | CV.readImage(filePath, (err, im) => {
114 | console.log('CV loaded:', filePath, im)
115 | if (previousImage && im.width() > 1 && im.height() > 1) {
116 | CV.ImageSimilarity(im, previousImage, function (err, dissimilarity) {
117 | console.log('Dissimilarity:', dissimilarity);
118 | previousImage = im;
119 |
120 | let changesDetected = dissimilarity > 0; // silly change detector
121 | if (statePreviewing || changesDetected || pendingAlarm != respondedAlarm) {
122 | // upload the image either if the preview is enabled
123 | // or the image has artifacts and an unresponded alarm is pending
124 | uploadImage(filePath, config.token, changesDetected).then(res => {
125 | console.log('Uploaded:', res.media.sid, res.location);
126 | cameraSnapshot.set({
127 | date_captured: new Date(timeStamp).toUTCString(),
128 | mcs_sid: res.media.sid,
129 | mcs_url: res.location,
130 | traits: { changes_detected: changesDetected }
131 | });
132 | });
133 | }
134 | });
135 | } else {
136 | previousImage = im;
137 | }
138 | });
139 | }
140 | });
141 |
142 | capturer.on("exit", function(timestamp) {
143 | console.log('Stopping camera timelapse')
144 | capturer.stop();
145 | });
146 |
147 | bootstrapClient(cameraId, cameraSecret)
148 | .then(function(cfg) {
149 | config = cfg;
150 | return new SyncClient(config.token);
151 | })
152 | .then(client => {
153 | client.document(config.sync_objects.camera_snapshot_document).then(doc => {
154 | console.log('Snapshot document:', doc.sid);
155 | cameraSnapshot = doc;
156 | });
157 | client.map(config.sync_objects.camera_control_map).then(map => {
158 | console.log('Control map:', map.sid);
159 | map.get('arm')
160 | .then(item => updateCameraState(item))
161 | .catch(e => map.set('arm', { enabled: false }));
162 | map.get('preview')
163 | .then(item => updateCameraState(item))
164 | .catch(e => map.set('preview', { enabled: false }));
165 | map.get('alarm')
166 | .then(item => updateCameraState(item))
167 | .catch(e => console.log('Alarm state not initialized!'));
168 | map.on('itemUpdated', item => {
169 | console.log('Remote control:', item.key, item.value);
170 | updateCameraState(item);
171 | });
172 | });
173 | accessManager = new TwilioCommon.AccessManager(config.token);
174 | accessManager.on('tokenUpdated', am => {
175 | config.token = am.token;
176 | client.updateToken(am.token);
177 | });
178 | accessManager.on('tokenExpired', () => {
179 | bootstrapClient(cameraId, cameraSecret)
180 | .then(cfg => accessManager.updateToken(cfg.token));
181 | });
182 | })
183 | .catch(function(error) {
184 | console.error("Failed initializing:", error);
185 | });
186 |
--------------------------------------------------------------------------------
/runtime/.gitignore:
--------------------------------------------------------------------------------
1 | \context-*.yaml
2 |
--------------------------------------------------------------------------------
/runtime/AlertGenerator/AlertGenerator.js:
--------------------------------------------------------------------------------
1 | const SNAPSHOT_PATTERN = /^cameras.([a-zA-Z0-9]+).snapshot$/
2 |
3 | exports.handler = function(context, event, callback) {
4 | if ("document_updated" === event.EventType &&
5 | event.DocumentUniqueName.match(SNAPSHOT_PATTERN)) {
6 | let cameraId = SNAPSHOT_PATTERN.exec(event.DocumentUniqueName)[1];
7 | let camera_control_map = "cameras." + cameraId + ".control";
8 | let camera_alerts_list = "cameras." + cameraId + ".alerts";
9 |
10 | let snapshotData = JSON.parse(event.DocumentData);
11 | let config;
12 |
13 | if (snapshotData.traits && snapshotData.traits.changes_detected) {
14 | let client = context.getTwilioClient();
15 | let syncService = client.sync.services(context.SERVICE_SID);
16 | Promise.all([
17 | syncService.syncMaps(camera_control_map).syncMapItems("alarm").fetch(),
18 | syncService.syncMaps(camera_control_map).syncMapItems("arm").fetch(),
19 | syncService.documents("app.configuration").fetch()
20 | ])
21 | .then(function (items) {
22 | let alarmItem = items[0];
23 | let armItem = items[1];
24 | config = items[2].data;
25 |
26 | let disarmed = alarmItem.data.id === armItem.data.responded_alarm;
27 |
28 | if (disarmed) {
29 | // raise new alarm
30 | return Promise.all([
31 | alarmItem.data.id,
32 | syncService
33 | .syncLists(camera_alerts_list)
34 | .syncListItems.create({
35 | data: {
36 | datetime_utc: (new Date()).getTime(),
37 | reason: "changes_detected"
38 | }
39 | })
40 | ]);
41 | } else {
42 | return [alarmItem.data.id];
43 | }
44 | })
45 | .then(function (promises) {
46 | let currentAlarmId = promises[0];
47 | if (promises.length >= 2) {
48 | let newItemResponse = promises[1];
49 | if (newItemResponse) {
50 | let newAlarmId = newItemResponse.index;
51 | return syncService
52 | .syncMaps(camera_control_map)
53 | .syncMapItems("alarm")
54 | .update({
55 | data: {
56 | id: newAlarmId
57 | }
58 | })
59 | .then(function () {
60 | return syncService
61 | .syncLists.create({
62 | uniqueName: "cameras." + cameraId + ".archives." + newAlarmId
63 | });
64 | })
65 | .then(function () {
66 | let camera = config.cameras[cameraId];
67 | return client.messages.create({
68 | from: context.ALERT_FROM_NUMBER,
69 | to: camera.contact_number,
70 | body: "Twilio Security Camera Alarm alert from camera \"" + cameraId + "\", alert id \"" + newAlarmId + "\""
71 | });
72 | })
73 | .then(function () {
74 | return { result: "new alarm create", alarm_id: newAlarmId };
75 | });
76 | }
77 | } else {
78 | return { result: "alarm archive updated", alarm_id: currentAlarmId };
79 | }
80 | })
81 | .then(function (result) {
82 | return syncService
83 | .syncLists("cameras." + cameraId + ".archives." + result.alarm_id)
84 | .syncListItems.create({
85 | data: snapshotData
86 | })
87 | .then(function (response) {
88 | result.archive_id = response.index;
89 | callback(null, result);
90 | })
91 | })
92 | .catch(function (err) {
93 | callback(null, { result: "sync operations failed", error: err.toString() });
94 | });
95 | } else {
96 | callback(null, { result: "not interesting trait" });
97 | }
98 | } else {
99 | callback(null, { result: "not interested event type" });
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/runtime/AlertGenerator/descriptor.yaml:
--------------------------------------------------------------------------------
1 | name: AlertGenerator
2 | type: function
3 | path: /alertgenerator
4 | description: |
5 | Generate alerts according to incoming snapshots.
6 | script: AlertGenerator.js
7 |
--------------------------------------------------------------------------------
/runtime/AlertGenerator/testdata/ignoreevent.yaml:
--------------------------------------------------------------------------------
1 | AccountSid: AC853deee1d40f428141c6b28812aa253f
2 | EndpointId: twi1-9cebc901e9ac4631a29172afedd6443d
3 | Identity: security-cam-sync-qchht23ys1i57y14i
4 | DocumentSid: ET5d600ce03d1f44399868c2b9c28065cd
5 | DocumentRevision: 42
6 | DocumentData: "{\"mcs_url\":\"https://mcs.us1.twilio.com/v1/Services/IS8d7c53a5fd4d537f4e50e80c7cb1a688/Media/MEe875e4eb04e3405ce5d1ededfd7b7380\"}"
7 | EventType: not_interesting_event
8 | DocumentUniqueName: cameras.TEST.snapshot
9 | DateCreated: 2017-05-31T06:54:06.038-07:00
10 | ServiceSid: IS8d7c53a5fd4d537f4e50e80c7cb1a688
11 | ProtocolVersion: v3
12 |
--------------------------------------------------------------------------------
/runtime/AlertGenerator/testdata/newsnapshot.yaml:
--------------------------------------------------------------------------------
1 | AccountSid: AC853deee1d40f428141c6b28812aa253f
2 | EndpointId: twi1-9cebc901e9ac4631a29172afedd6443d
3 | Identity: security-cam-sync-qchht23ys1i57y14i
4 | DocumentSid: ET5d600ce03d1f44399868c2b9c28065cd
5 | DocumentRevision: 42
6 | DocumentData: "{\"mcs_url\":\"https://mcs.us1.twilio.com/v1/Services/IS8d7c53a5fd4d537f4e50e80c7cb1a688/Media/MEe875e4eb04e3405ce5d1ededfd7b7380\",\"traits\":{\"changes_detected\":true}}"
7 | EventType: document_updated
8 | DocumentUniqueName: cameras.TEST.snapshot
9 | DateCreated: 2017-05-31T06:54:06.038-07:00
10 | ServiceSid: IS8d7c53a5fd4d537f4e50e80c7cb1a688
11 | ProtocolVersion: v3
12 |
--------------------------------------------------------------------------------
/runtime/CameraAuthenticator/CameraAuthenticator.js:
--------------------------------------------------------------------------------
1 | const crypto = require("crypto");
2 | const AccessToken = Twilio.jwt.AccessToken;
3 | const SyncGrant = AccessToken.SyncGrant;
4 |
5 | function cameraAuth(cameras, camera_id, camera_token) {
6 | if (!(camera_id in cameras)) return false;
7 | let hash = crypto.createHash('sha512').update(camera_token).digest("hex");
8 | return hash === cameras[camera_id].hash;
9 | }
10 |
11 | exports.handler = function(context, event, callback) {
12 | let camera_id = event.camera_id;
13 | let camera_token = event.camera_token;
14 |
15 | if (!camera_id) return callback(null, { success: false, error: "camera_id is not defined in event" });
16 | if (!camera_token) return callback(null, { success: false, error: "camera_token is not defined in event" });
17 |
18 | // Create a "grant" which enables a client to use Sync as a given user,
19 | // on a given device
20 | let syncGrant = new SyncGrant({
21 | serviceSid: context.SERVICE_SID
22 | });
23 |
24 | // Create an access token which we will sign and return to the client,
25 | // containing the grant we just created
26 | let token = new AccessToken(
27 | context.ACCOUNT_SID,
28 | context.API_KEY,
29 | context.API_SECRET, {
30 | ttl : parseInt(context.TOKEN_TTL) // int and string are different for AccessToken
31 | }
32 | );
33 | token.addGrant(syncGrant);
34 | token.identity = camera_id;
35 |
36 | // verify camera token
37 | let client = context.getTwilioClient();
38 | let syncService = client.sync.services(context.SERVICE_SID);
39 | syncService.documents("app.configuration").fetch()
40 | .then(function (configDocument) {
41 | let config = configDocument.data;
42 | if (cameraAuth(config.cameras, camera_id, camera_token)) {
43 | // Serialize the token to a JWT string and include it in a JSON response
44 | callback(null, {
45 | success: true,
46 | camera_id: camera_id,
47 | service_sid: context.SERVICE_SID,
48 | ttl: context.TOKEN_TTL,
49 | token: token.toJwt(),
50 | links: {
51 | upload_url: "https://mcs.us1.twilio.com/v1/Services/" + context.SERVICE_SID + "/Media"
52 | },
53 | sync_objects: {
54 | camera_snapshot_document: "cameras." + camera_id + ".snapshot",
55 | camera_control_map: "cameras." + camera_id + ".control",
56 | camera_alerts_list: "cameras." + camera_id + ".alerts",
57 | camera_archives_list_prefix: "cameras." + camera_id + ".archives."
58 | }
59 | });
60 | } else {
61 | callback(null, { success: false, error: "Unauthorized camera: " + camera_id });
62 | }
63 | })
64 | .catch(function (error) {
65 | callback(null, { success: false, error: "Sync service error: " + error });
66 | });
67 | };
68 |
--------------------------------------------------------------------------------
/runtime/CameraAuthenticator/descriptor.yaml:
--------------------------------------------------------------------------------
1 | name: CameraAuthenticator
2 | type: function
3 | path: /cameraauthenticate
4 | description: |
5 | Generate short-term jwt token and links for cameras to access twilio services
6 | script: CameraAuthenticator.js
7 |
--------------------------------------------------------------------------------
/runtime/CameraAuthenticator/testdata/simple.yaml:
--------------------------------------------------------------------------------
1 | username: "twilio"
2 | pincode: 928462
3 |
--------------------------------------------------------------------------------
/runtime/README.md:
--------------------------------------------------------------------------------
1 | # Security Camera Blueprint
2 | ### Twilio Functions scripts
3 | The scripts located in this directory are meant to run on [Twilio Functions](https://www.twilio.com/functions).
4 |
5 | ### What is Functions?
6 | Serverless architecture is a software design pattern where applications are hosted by a third-party service, eliminating the need for server software and hardware management by the developer. Applications are broken up into individual functions that can be invoked and scaled individually.
7 |
8 | ### What is Twilio Runtime?
9 | Twilio Runtime is a suite designed to help you build, scale and operate your application, consisting of a plethora of tools including helper libraries, API keys, asset storage, debugging tools, and a node based serverless hosting environment [Twilio Functions](https://www.twilio.com/docs/api/runtime/functions).
10 |
11 | Full instructions for this tutorial can be found in the [Security Camera Blueprint](https://www.twilio.com/wireless/blueprints/security-camera/). Below you will find the minimum steps necessary to get this up and running.
12 |
--------------------------------------------------------------------------------
/runtime/UserAuthenticator/UserAuthenticator.js:
--------------------------------------------------------------------------------
1 | const AccessToken = Twilio.jwt.AccessToken;
2 | const SyncGrant = AccessToken.SyncGrant;
3 |
4 |
5 | function userAuth(context, username, pincode) {
6 | var pincodes = JSON.parse(context.USER_PINCODES);
7 | return pincodes[username] === pincode;
8 | }
9 |
10 | exports.handler = function(context, event, callback) {
11 | let username = event.username;
12 | let pincode = event.pincode;
13 |
14 | if (!username) return callback(null, { success: false, error: "username is not defined in event" });
15 | if (!pincode) return callback(null, { success: false, error: "pincode is not defined in event" });
16 | if (!userAuth(context, username, pincode)) return callback(null, { success: false, error: "username or token provided is invalid" });
17 |
18 | // Create a "grant" which enables a client to use Sync as a given user,
19 | // on a given device
20 | let syncGrant = new SyncGrant({
21 | serviceSid: context.SERVICE_SID
22 | });
23 |
24 | // Create an access token which we will sign and return to the client,
25 | // containing the grant we just created
26 | let token = new AccessToken(
27 | context.ACCOUNT_SID,
28 | context.API_KEY,
29 | context.API_SECRET, {
30 | ttl : parseInt(context.TOKEN_TTL) // int and string are different for AccessToken
31 | }
32 | );
33 | token.addGrant(syncGrant);
34 | token.identity = username;
35 |
36 | // Serialize the token to a JWT string and include it in a JSON response
37 | callback(null, {
38 | success: true,
39 | username: username,
40 | service_sid: context.SERVICE_SID,
41 | ttl: context.TOKEN_TTL,
42 | token: token.toJwt()
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/runtime/UserAuthenticator/descriptor.yaml:
--------------------------------------------------------------------------------
1 | name: UserAuthenticator
2 | type: function
3 | path: /userauthenticate
4 | description: |
5 | Generate short-term jwt token for UI and its authorized operator to access twilio services
6 | script: UserAuthenticator.js
7 |
--------------------------------------------------------------------------------
/runtime/UserAuthenticator/testdata/simple.yaml:
--------------------------------------------------------------------------------
1 | username: "twilio"
2 | pincode: "928462"
3 |
--------------------------------------------------------------------------------
/runtime/example-context.yaml:
--------------------------------------------------------------------------------
1 | # required field
2 | ACCOUNT_SID: AC00000000000000000000000000000000
3 | AUTH_TOKEN: 00000000000000000000000000
4 |
5 | # Common
6 | USER_PINCODES:
7 | usera: "203455"
8 | API_KEY: SK00000000000000000000000000000000
9 | API_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
10 | SERVICE_SID: IS000000000000000000000000000000
11 | TOKEN_TTL: 3600 # in seconds
12 |
13 | # used by AlertGenerator
14 | ALERT_FROM_NUMBER : 003725000000
15 |
--------------------------------------------------------------------------------