From 2f9c6051178984d698ad4ba1c3bccc48e502451e Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Sun, 19 Feb 2017 14:37:59 -0500 Subject: [PATCH 01/35] Switch all YouTube REST calls to use the YouTube lib --- api/services/YouTubeService.js | 50 ++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/api/services/YouTubeService.js b/api/services/YouTubeService.js index ab8d3d3..9a56f21 100644 --- a/api/services/YouTubeService.js +++ b/api/services/YouTubeService.js @@ -30,10 +30,13 @@ function parseYouTubeLink(link) { function getYouTubeVideo(key, user, canSave=true) { return new Promise((resolve, reject) => { - request(`https://www.googleapis.com/youtube/v3/videos?id=${key}&part=snippet,contentDetails&key=${process.env.GOOGLE_API_KEY}`, (error, response, body) => { - if (!error && response.statusCode == 200) { + Youtube.videos.list({ + id: key, + part: 'snippet,contentDetails' + }, (error, data) => { + if (!error) { try { - parseYouTubeVideo(JSON.parse(body), user, canSave).then((video, err) => { + parseYouTubeVideo(data, user, canSave).then((video, err) => { if (err) { throw err; } @@ -73,9 +76,14 @@ function parseYouTubeVideo(data, user, canSave) { function search(query, maxResults) { return new Promise((resolve, reject) => { - request(`https://www.googleapis.com/youtube/v3/search?q=${query}&part=snippet&key=${process.env.GOOGLE_API_KEY}&maxResults=${maxResults || 15}&type=video,playlist`, (error, response, body) => { - if (!error && response.statusCode == 200) { - var results = JSON.parse(body).items.map(function(video) { + Youtube.search.list({ + q: query, + part: 'snippet', + maxResults: maxResults || 15, + type: 'video,playlist' + }, (error, data) => { + if (!error) { + var results = data.items.map(function(video) { var item = { playlistId: video.id.playlistId, key: video.id.videoId, @@ -97,7 +105,7 @@ function enrichPlaylist(playlist) { Youtube.playlistItems.list({ playlistId: playlist.playlistId, part: 'snippet,contentDetails' - }, (err, data) => { + }, (error, data) => { playlist.playlistItems = data.pageInfo.totalResults; resolve(playlist); }); @@ -109,7 +117,7 @@ function enrichVideo(video) { Youtube.videos.list({ id: video.key, part: 'snippet,contentDetails' - }, (err, data) => { + }, (error, data) => { video.duration = data.items.length == 1 ? moment.duration(data.items[0].contentDetails.duration).asMilliseconds() : undefined; resolve(video); }); @@ -136,14 +144,18 @@ function nextRelated(key) { function relatedVideos(key, maxResults = 10) { return new Promise((resolve, reject) => { - request(`https://www.googleapis.com/youtube/v3/search?relatedToVideoId=${key}&part=snippet&key=${process.env.GOOGLE_API_KEY}&maxResults=${maxResults}&type=video`, (error, response, body) => { - if (!error && response.statusCode == 200) { - var items = JSON.parse(body).items; - if (items.length === 0) { + Youtube.search.list({ + relatedToVideoId: key, + part: 'snippet', + maxResults: maxResults, + type: 'video' + }, (error, data) => { + if (!error) { + if (data.items.length === 0) { reject('No related video found'); } - var itemsPromise = items.map((i) => { + var itemsPromise = data.items.map((i) => { return getYouTubeVideo(i.id.videoId, 'JukeBot', false); }); @@ -171,10 +183,14 @@ function getPlaylistVideos(playlistId, user) { function getPlaylistVideosRecursive(playlistId, videos, pageToken) { return new Promise((resolve, reject) => { if (pageToken === '' || pageToken) { - request(`https://www.googleapis.com/youtube/v3/playlistItems?maxResults=50&part=snippet&key=${process.env.GOOGLE_API_KEY}&playlistId=${playlistId}&pageToken=${pageToken}`, (error, response, body) => { - if (!error && response.statusCode == 200) { - let playlist = JSON.parse(body); - getPlaylistVideosRecursive(playlistId, videos.concat(playlist.items), playlist.nextPageToken).then(function(videos) { + Youtube.playlistItems.list({ + maxResults: 50, + part: 'snippet', + playlistId: playlistId, + pageToken: pageToken + }, (error, data) => { + if (!error) { + getPlaylistVideosRecursive(playlistId, videos.concat(data.items), data.nextPageToken).then(function(videos) { resolve(videos); }); } else { From 1e14da05b54a6f1ce3f8db425f6a9e13c5fee55c Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Mon, 1 May 2017 18:34:46 -0400 Subject: [PATCH 02/35] Add MySQL support --- README.md | 77 ++++++++++++++++++++++++------------ config/connections.js | 92 ++++--------------------------------------- config/models.js | 16 ++------ package.json | 1 + 4 files changed, 62 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 997457d..5b03f1b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ [Demo](https://demo.jukebot.club/) -[![Build Status](https://travis-ci.org/TheConnMan/jukebot.svg?branch=master)](https://travis-ci.org/TheConnMan/jukebot) -[![Docker Pulls](https://img.shields.io/docker/pulls/theconnman/jukebot.svg)](https://hub.docker.com/r/theconnman/jukebot/) +[![Build Status](https://travis-ci.org/TheConnMan/jukebot.svg?branch=master)](https://travis-ci.org/TheConnMan/jukebot) [![Docker Pulls](https://img.shields.io/docker/pulls/theconnman/jukebot.svg)](https://hub.docker.com/r/theconnman/jukebot/) Slack-Enabled Syncronized Music Listening @@ -12,39 +11,48 @@ Slack-Enabled Syncronized Music Listening **JukeBot** is for Slack teams who want to listen to music together, add music through Slack, and chat about the music in a Slack channel. ## Local Development + To get started with local development follow these steps: 1. Clone this repo and `cd` into it -1. Install **JukeBot** dependencies with `npm install` (make sure you're on npm v3+) -1. Get a Google API key using the setup steps below -1. Run **JukeBot** with `GOOGLE_API_KEY= npm start` and go to http://localhost:1337 +2. Install **JukeBot** dependencies with `npm install` (make sure you're on npm v3+) +3. Get a Google API key using the setup steps below +4. Run **JukeBot** with `GOOGLE_API_KEY= npm start` and go to ## Setup + ### Google API Key for YouTube -1. Go to https://console.developers.google.com -1. Create a new Google Project named JukeBot -1. Under **Create credentials** choose **API key** and use the API key for the environment variable below -1. Click **Library** in the left panel -1. Go to **YouTube Data API** -1. Click **Enable** at the top + +1. Go to +2. Create a new Google Project named JukeBot +3. Under **Create credentials** choose **API key** and use the API key for the environment variable below +4. Click **Library** in the left panel +5. Go to **YouTube Data API** +6. Click **Enable** at the top ### Slash Command (Optional) + To use a Slack Slash Command you'll need to set one up (preferably after the running the deployment steps below) and follow these instructions: 1. Go to the [Slack Slash Command setup page](https://my.slack.com/apps/A0F82E8CA-slash-commands), add a configuration, and name it **JukeBot** -1. Input the URL you configure during the deployment step and add a trailing `/slack/slash` (e.g. jukebot.my.domain.com/slack/slash) -1. Change the request method to GET -1. Copy the **Token** and use it as the **SLASH_TOKEN** environment variable -1. Customize the name to **JukeBot** -1. Check the box to "Show this command in the autocomplete list" -1. Add the description "Slack-Enabled Syncronized Music Listening" -1. Add the usage hint "add - Add a YouTube video to the playlist" -1. Click save! +2. Input the URL you configure during the deployment step and add a trailing `/slack/slash` (e.g. jukebot.my.domain.com/slack/slash) +3. Change the request method to GET +4. Copy the **Token** and use it as the **SLASH_TOKEN** environment variable +5. Customize the name to **JukeBot** +6. Check the box to "Show this command in the autocomplete list" +7. Add the description "Slack-Enabled Syncronized Music Listening" +8. Add the usage hint "add + + - Add a YouTube video to the playlist" + +9. Click save! ## Deployment + **WARNING:** Data persistence is not currently a priority. Videos are considered to be transient and once they are more than 24 hours old they aren't shown in the UI. Due to this, schema migrations are not a high priority. ### Deployment without HTTPS + **JukeBot** can easily be run with Docker using the following command: ```bash @@ -54,28 +62,45 @@ docker run -d -p 80:1337 -e GOOGLE_API_KEY= theconnman/jukebot:lat Make sure to add any additional environment variables as well to the above command. Then go to the URL of your server and listen to some music! ### Deployment with HTTPS (Required when using a Slash Command) -Slack Slash Commands require the slash command endpoint to be HTTPS, so JukeBot uses [Let's Encrypt](https://letsencrypt.org/) to get an SSL cert. You'll need to be hosting JukeBot on a domain you have DNS authority over for this to work as you'll need to create a couple DNS entries. Make sure you have entries pointing to the current box for the domain or subdomain to be used in the deployment as well as the wildcard subdomains of the given domain (e.g. \*.projects.theconnman.com). This is not only so Slack can locate your deployment, but also so Let's Encrypt can negotiate for your SSL cert. + +Slack Slash Commands require the slash command endpoint to be HTTPS, so JukeBot uses [Let's Encrypt](https://letsencrypt.org/) to get an SSL cert. You'll need to be hosting JukeBot on a domain you have DNS authority over for this to work as you'll need to create a couple DNS entries. Make sure you have entries pointing to the current box for the domain or subdomain to be used in the deployment as well as the wildcard subdomains of the given domain (e.g. *.projects.theconnman.com). This is not only so Slack can locate your deployment, but also so Let's Encrypt can negotiate for your SSL cert. Within this project are three files needing to be modified when deploying this project to your own server: `docker-compose.yml`, `traefik.toml`, and `.env`. -####`docker-compose.yml` +#### `docker-compose.yml` 1. Replace the two **localhost** references with your own domain or subdomain which points to the current box (e.g. projects.theconnman.com). -1. Update the volume paths so they point to the correct location in your host. +2. Update the volume paths so they point to the correct location in your host. -####`traefik.toml` +#### `traefik.toml` 1. Replace the email address with your own and the domain from localhost to the same one as before. -1. After saving that file, issue a `touch acme.json` to create an empty credentials file. +2. After saving that file, issue a `touch acme.json` to create an empty credentials file. -####`example.env` +#### `example.env` 1. Copy the provided example.env file to `.env`. -1. Modify the example environment variables (described below) before running JukeBot. +2. Modify the example environment variables (described below) before running JukeBot. After that run `docker-compose up -d` and you should be able to access the UI at jukebox.my.domain.com (after replacing with your domain or subdomain of course). +## Running with MySQL + +The default database is on disk, so it is recommended to run **JukeBot** with a MySQL DB in production. Use the following environment variables: + +- MYSQL_HOST (MySQL will be used as the datastore if this is supplied) +- MYSQL_USER (default: sails) +- MYSQL_PASSWORD (default: sails) +- MYSQL_DB (default: sails) + +The easiest way to run a MySQL instance is to run it in Docker using the following command: + +```bash +docker run -d -p 3306:3306 -e MYSQL_DATABASE=sails -e MYSQL_USER=sails -e MYSQL_PASSWORD=sails -e MYSQL_RANDOM_ROOT_PASSWORD=true --name=mysql mysql +``` + ## Environment Variables + - **GOOGLE_API_KEY** - Google project API key - **SLACK_WEBHOOK** (Optional) - [Slack Incoming Webhook URL](https://my.slack.com/apps/A0F7XDUAZ-incoming-webhooks) for sending song addition and currently now playing notifications - **SLACK_SONG_ADDED** (default: true) - Toggle for "Song Added" Slack notifications, only applicable if **SLACK_WEBHOOK** is provided diff --git a/config/connections.js b/config/connections.js index a07b9cd..6133918 100644 --- a/config/connections.js +++ b/config/connections.js @@ -1,94 +1,16 @@ -/** - * Connections - * (sails.config.connections) - * - * `Connections` are like "saved settings" for your adapters. What's the difference between - * a connection and an adapter, you might ask? An adapter (e.g. `sails-mysql`) is generic-- - * it needs some additional information to work (e.g. your database host, password, user, etc.) - * A `connection` is that additional information. - * - * Each model must have a `connection` property (a string) which is references the name of one - * of these connections. If it doesn't, the default `connection` configured in `config/models.js` - * will be applied. Of course, a connection can (and usually is) shared by multiple models. - * . - * Note: If you're using version control, you should put your passwords/api keys - * in `config/local.js`, environment variables, or use another strategy. - * (this is to prevent you inadvertently sensitive credentials up to your repository.) - * - * For more information on configuration, check out: - * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.connections.html - */ - module.exports.connections = { - - /*************************************************************************** - * * - * Local disk storage for DEVELOPMENT ONLY * - * * - * Installed by default. * - * * - ***************************************************************************/ localDiskDb: { adapter: 'sails-disk', filePath: 'data/', fileName: 'jukebot.db' }, - /*************************************************************************** - * * - * MySQL is the world's most popular relational database. * - * http://en.wikipedia.org/wiki/MySQL * - * * - * Run: npm install sails-mysql * - * * - ***************************************************************************/ - // someMysqlServer: { - // adapter: 'sails-mysql', - // host: 'YOUR_MYSQL_SERVER_HOSTNAME_OR_IP_ADDRESS', - // user: 'YOUR_MYSQL_USER', //optional - // password: 'YOUR_MYSQL_PASSWORD', //optional - // database: 'YOUR_MYSQL_DB' //optional - // }, - - /*************************************************************************** - * * - * MongoDB is the leading NoSQL database. * - * http://en.wikipedia.org/wiki/MongoDB * - * * - * Run: npm install sails-mongo * - * * - ***************************************************************************/ - // someMongodbServer: { - // adapter: 'sails-mongo', - // host: 'localhost', - // port: 27017, - // user: 'username', //optional - // password: 'password', //optional - // database: 'your_mongo_db_name_here' //optional - // }, - - /*************************************************************************** - * * - * PostgreSQL is another officially supported relational database. * - * http://en.wikipedia.org/wiki/PostgreSQL * - * * - * Run: npm install sails-postgresql * - * * - * * - ***************************************************************************/ - // somePostgresqlServer: { - // adapter: 'sails-postgresql', - // host: 'YOUR_POSTGRES_SERVER_HOSTNAME_OR_IP_ADDRESS', - // user: 'YOUR_POSTGRES_USER', // optional - // password: 'YOUR_POSTGRES_PASSWORD', // optional - // database: 'YOUR_POSTGRES_DB' //optional - // } - - - /*************************************************************************** - * * - * More adapters: https://github.com/balderdashy/sails * - * * - ***************************************************************************/ + mysql: { + adapter: 'sails-mysql', + host: process.env.MYSQL_HOST, + user: process.env.MYSQL_USER || 'sails', + password: process.env.MYSQL_PASSWORD || 'sails', + database: process.env.MYSQL_DB || 'sails', + } }; diff --git a/config/models.js b/config/models.js index f77d7a7..c787b64 100644 --- a/config/models.js +++ b/config/models.js @@ -1,15 +1,5 @@ -/** - * Default model configuration - * (sails.config.models) - * - * Unless you override them, the following properties will be included - * in each of your models. - * - * For more info on Sails models, see: - * http://sailsjs.org/#!/documentation/concepts/ORM - */ - module.exports.models = { - migrate: 'alter', - connection: 'localDiskDb' + connection: process.env.MYSQL_HOST ? 'mysql' : 'localDiskDb', + + migrate: 'alter' }; diff --git a/package.json b/package.json index 170c0cc..91b6fd0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "rc": "1.0.1", "sails": "~0.12.7", "sails-disk": "~0.10.9", + "sails-mysql": "^0.11.5", "slack-webhook": "^1.0.0", "youtube-api": "^2.0.6" }, From 1725976f11b1e794e5b9cc071d16ddd5f5158c1f Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Mon, 1 May 2017 20:30:37 -0400 Subject: [PATCH 03/35] Add initial DB schema migration --- Dockerfile | 2 +- README.md | 6 ++ config/migrations.js | 4 ++ config/models.js | 2 +- migrations/20170501223203-initial-schema.js | 71 +++++++++++++++++++++ package.json | 6 +- tasks/register/dbMigrate.js | 1 + 7 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 config/migrations.js create mode 100644 migrations/20170501223203-initial-schema.js create mode 100644 tasks/register/dbMigrate.js diff --git a/Dockerfile b/Dockerfile index 026fcbd..b784aff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ COPY . /usr/src/app EXPOSE 1337 -CMD npm start +CMD if [[ -z "${MYSQL_HOST}" ]]; then npm start; else npm run migrate && node app.js; fi diff --git a/README.md b/README.md index 5b03f1b..46a42fb 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,12 @@ The easiest way to run a MySQL instance is to run it in Docker using the followi docker run -d -p 3306:3306 -e MYSQL_DATABASE=sails -e MYSQL_USER=sails -e MYSQL_PASSWORD=sails -e MYSQL_RANDOM_ROOT_PASSWORD=true --name=mysql mysql ``` +### Developing with MySQL + +Using MySQL automatically sets the migration strategy to `safe`, so running with MySQL requires you to run `npm migrate` with the appropriate environment variables to bring the DB schema up to speed. + +When developing a new migration script run `grunt db:migrate:create --name=` and implement the `up` and `down` steps once the migration is created. + ## Environment Variables - **GOOGLE_API_KEY** - Google project API key diff --git a/config/migrations.js b/config/migrations.js new file mode 100644 index 0000000..c47e082 --- /dev/null +++ b/config/migrations.js @@ -0,0 +1,4 @@ +module.exports.migrations = { + + connection: 'mysql' +}; diff --git a/config/models.js b/config/models.js index c787b64..3473674 100644 --- a/config/models.js +++ b/config/models.js @@ -1,5 +1,5 @@ module.exports.models = { connection: process.env.MYSQL_HOST ? 'mysql' : 'localDiskDb', - migrate: 'alter' + migrate: process.env.MYSQL_HOST ? 'safe' : 'alter' }; diff --git a/migrations/20170501223203-initial-schema.js b/migrations/20170501223203-initial-schema.js new file mode 100644 index 0000000..9df489b --- /dev/null +++ b/migrations/20170501223203-initial-schema.js @@ -0,0 +1,71 @@ +'use strict'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function(options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +exports.up = function(db, callback) { + createChats(db).then(() => { + return createVideos(db); + }).then(callback); +}; + +exports.down = function(db) { + return db.dropTable('chat', null, db.dropTable('video', null, callback)); +}; + +exports._meta = { + "version": 1 +}; + +function createChats(db) { + return new Promise((resolve, reject) => { + db.createTable('chat', { + id: { + type: 'int', + primaryKey: true, + autoIncrement: true + }, + username: 'string', + message: 'string', + type: 'string', + time: 'datetime', + data: 'string', + createdAt: 'datetime', + updatedAt: 'datetime' + }, resolve); + }); +} + +function createVideos(db) { + return new Promise((resolve, reject) => { + db.createTable('video', { + id: { + type: 'int', + primaryKey: true, + autoIncrement: true + }, + key: 'string', + title: 'string', + duration: 'int', + user: 'string', + startTime: 'datetime', + thumbnail: 'string', + playing: 'boolean', + played: 'boolean', + isSuggestion: 'boolean', + createdAt: 'datetime', + updatedAt: 'datetime' + }, resolve); + }); +} diff --git a/package.json b/package.json index 91b6fd0..50c3baa 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "description": "Slack-Enabled Syncronized Music Listening", "keywords": [], "dependencies": { + "db-migrate": "^0.10.0-beta.20", + "db-migrate-mysql": "^1.1.10", "ejs": "2.3.4", "fluent-logger": "github:theconnman/fluent-logger-node", "grunt": "1.0.1", @@ -23,6 +25,7 @@ "moment": "^2.17.1", "rc": "1.0.1", "sails": "~0.12.7", + "sails-db-migrate": "^1.5.0", "sails-disk": "~0.10.9", "sails-mysql": "^0.11.5", "slack-webhook": "^1.0.0", @@ -30,7 +33,8 @@ }, "scripts": { "debug": "node debug app.js", - "start": "node app.js" + "start": "node app.js", + "migrate": "grunt db:migrate:up" }, "main": "app.js", "repository": { diff --git a/tasks/register/dbMigrate.js b/tasks/register/dbMigrate.js new file mode 100644 index 0000000..f43e12f --- /dev/null +++ b/tasks/register/dbMigrate.js @@ -0,0 +1 @@ +module.exports = require('sails-db-migrate').gruntTasks; From 98532a9822ab89a9c73b7940928d119161fb555d Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Tue, 2 May 2017 19:14:55 -0400 Subject: [PATCH 04/35] Add callback parameter to DB migration down step --- migrations/20170501223203-initial-schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20170501223203-initial-schema.js b/migrations/20170501223203-initial-schema.js index 9df489b..7a882f0 100644 --- a/migrations/20170501223203-initial-schema.js +++ b/migrations/20170501223203-initial-schema.js @@ -20,7 +20,7 @@ exports.up = function(db, callback) { }).then(callback); }; -exports.down = function(db) { +exports.down = function(db, callback) { return db.dropTable('chat', null, db.dropTable('video', null, callback)); }; From 6ca20f8f26e5f8cd530894e507edfec2d3ca3869 Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Tue, 2 May 2017 19:22:21 -0400 Subject: [PATCH 05/35] Add user object and realuser fields --- api/models/Chat.js | 3 +- api/models/User.js | 14 ++++++ api/models/Video.js | 1 + migrations/20170502230145-user-properties.js | 46 ++++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 api/models/User.js create mode 100644 migrations/20170502230145-user-properties.js diff --git a/api/models/Chat.js b/api/models/Chat.js index a8beedb..54b3722 100644 --- a/api/models/Chat.js +++ b/api/models/Chat.js @@ -20,7 +20,8 @@ module.exports = { data: { type: 'string', required: false - } + }, + realuser: 'String' // room: { // model: 'room' // } diff --git a/api/models/User.js b/api/models/User.js new file mode 100644 index 0000000..e3691b6 --- /dev/null +++ b/api/models/User.js @@ -0,0 +1,14 @@ +module.exports = { + + attributes: { + provider: 'String', + uid: 'String', + name: 'String', + email: 'String', + + properties: { + type: 'json', + defaultsTo: '{}' + }, + } +}; diff --git a/api/models/Video.js b/api/models/Video.js index b7690db..ab3c886 100644 --- a/api/models/Video.js +++ b/api/models/Video.js @@ -17,6 +17,7 @@ module.exports = { type: 'string', required: true }, + realuser: 'String', startTime: { type: 'datetime' }, diff --git a/migrations/20170502230145-user-properties.js b/migrations/20170502230145-user-properties.js new file mode 100644 index 0000000..51f9848 --- /dev/null +++ b/migrations/20170502230145-user-properties.js @@ -0,0 +1,46 @@ +'use strict'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function(options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +exports.up = function(db, callback) { + createUsers(db).then(db.addColumn('chat', 'realuser', 'string', db.addColumn('video', 'realuser', 'string', callback))); +}; + +exports.down = function(db, callback) { + return db.dropTable('user', null, db.removeColumn('chat', 'realuser', db.removeColumn('video', 'realuser', callback))); +}; + +exports._meta = { + "version": 1 +}; + +function createUsers(db) { + return new Promise((resolve, reject) => { + db.createTable('user', { + id: { + type: 'int', + primaryKey: true, + autoIncrement: true + }, + provider: 'string', + uid: 'string', + name: 'string', + email: 'string', + properties: 'text', + createdAt: 'datetime', + updatedAt: 'datetime' + }, resolve); + }); +} From 8798f4db75029db63ec9fb31f166b10093a77d26 Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Wed, 3 May 2017 16:50:21 -0400 Subject: [PATCH 06/35] Add Google auth --- README.md | 2 + api/controllers/AuthController.js | 31 +++++++++ config/bootstrap.js | 48 +++++++++++++ config/globals.js | 9 ++- config/http.js | 110 ++++++------------------------ config/policies.js | 49 +------------ config/routes.js | 46 +------------ package.json | 2 + 8 files changed, 116 insertions(+), 181 deletions(-) create mode 100644 api/controllers/AuthController.js diff --git a/README.md b/README.md index 46a42fb..600b584 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ When developing a new migration script run `grunt db:migrate:create --name= { var pair = tag.split(':'); diff --git a/config/globals.js b/config/globals.js index f9da8c5..5cef1a6 100644 --- a/config/globals.js +++ b/config/globals.js @@ -7,5 +7,12 @@ module.exports.globals = { googleAnalyticsId: process.env.GOOGLE_ANALYTICS_ID, chatHistory: process.env.CHAT_HISTORY ? parseInt(process.env.CHAT_HISTORY) : 24 * 60, videoHistory: process.env.VIDEO_HISTORY ? parseInt(process.env.VIDEO_HISTORY) : 24 * 60, - autoplayDisableCount: process.env.AUTOPLAY_DISABLE_STREAK ? parseInt(process.env.AUTOPLAY_DISABLE_STREAK) : 10 + autoplayDisableCount: process.env.AUTOPLAY_DISABLE_STREAK ? parseInt(process.env.AUTOPLAY_DISABLE_STREAK) : 10, + + oauth: { + google: { + clientID: process.env.GOOGLE_ID, + clientSecret: process.env.GOOGLE_SECRET + }, + } }; diff --git a/config/http.js b/config/http.js index 1d096f4..9ee185e 100644 --- a/config/http.js +++ b/config/http.js @@ -1,93 +1,27 @@ -/** - * HTTP Server Settings - * (sails.config.http) - * - * Configuration for the underlying HTTP server in Sails. - * Only applies to HTTP requests (not WebSockets) - * - * For more information on configuration, check out: - * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.http.html - */ - module.exports.http = { - /**************************************************************************** - * * - * Express middleware to use for every Sails request. To add custom * - * middleware to the mix, add a function to the middleware config object and * - * add its key to the "order" array. The $custom key is reserved for * - * backwards-compatibility with Sails v0.9.x apps that use the * - * `customMiddleware` config option. * - * * - ****************************************************************************/ - middleware: { - /*************************************************************************** - * * - * The order in which middleware should be run for HTTP request. (the Sails * - * router is invoked by the "router" middleware below.) * - * * - ***************************************************************************/ - - // order: [ - // 'startRequestTimer', - // 'cookieParser', - // 'session', - // 'myRequestLogger', - // 'bodyParser', - // 'handleBodyParserError', - // 'compress', - // 'methodOverride', - // 'poweredBy', - // '$custom', - // 'router', - // 'www', - // 'favicon', - // '404', - // '500' - // ], - - /**************************************************************************** - * * - * Example custom middleware; logs each request to the console. * - * * - ****************************************************************************/ - - // myRequestLogger: function (req, res, next) { - // console.log("Requested :: ", req.method, req.url); - // return next(); - // } - - - /*************************************************************************** - * * - * The body parser that will handle incoming multipart HTTP requests. By * - * default as of v0.10, Sails uses * - * [skipper](http://github.com/balderdashy/skipper). See * - * http://www.senchalabs.org/connect/multipart.html for other options. * - * * - * Note that Sails uses an internal instance of Skipper by default; to * - * override it and specify more options, make sure to "npm install skipper" * - * in your project first. You can also specify a different body parser or * - * a custom function with req, res and next parameters (just like any other * - * middleware function). * - * * - ***************************************************************************/ - - // bodyParser: require('skipper')({strict: true}) - - }, - - /*************************************************************************** - * * - * The number of seconds to cache flat files on disk being served by * - * Express static middleware (by default, these files are in `.tmp/public`) * - * * - * The HTTP static cache is only active in a 'production' environment, * - * since that's the only time Express will cache flat-files. * - * * - ***************************************************************************/ - - // cache: 31557600000 + passportInit: require('passport').initialize(), + passportSession: require('passport').session(), + + order: [ + 'startRequestTimer', + 'cookieParser', + 'session', + 'passportInit', + 'passportSession', + 'myRequestLogger', + 'bodyParser', + 'handleBodyParserError', + 'compress', + 'methodOverride', + 'poweredBy', + 'router', + 'www', + 'favicon', + '404', + '500' + ] + } }; diff --git a/config/policies.js b/config/policies.js index 1785d4e..3ae3aff 100644 --- a/config/policies.js +++ b/config/policies.js @@ -1,51 +1,4 @@ -/** - * Policy Mappings - * (sails.config.policies) - * - * Policies are simple functions which run **before** your controllers. - * You can apply one or more policies to a given controller, or protect - * its actions individually. - * - * Any policy file (e.g. `api/policies/authenticated.js`) can be accessed - * below by its filename, minus the extension, (e.g. "authenticated") - * - * For more information on how policies work, see: - * http://sailsjs.org/#!/documentation/concepts/Policies - * - * For more information on configuring policies, check out: - * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.policies.html - */ - - module.exports.policies = { - /*************************************************************************** - * * - * Default policy for all controllers and actions (`true` allows public * - * access) * - * * - ***************************************************************************/ - - // '*': true, - - /*************************************************************************** - * * - * Here's an example of mapping some policies to run before a controller * - * and its actions * - * * - ***************************************************************************/ - // RabbitController: { - - // Apply the `false` policy as the default for all of RabbitController's actions - // (`false` prevents all access, which ensures that nothing bad happens to our rabbits) - // '*': false, - - // For the action `nurture`, apply the 'isRabbitMother' policy - // (this overrides `false` above) - // nurture : 'isRabbitMother', - - // Apply the `isNiceToAnimals` AND `hasRabbitFood` policies - // before letting any users feed our rabbits - // feed : ['isNiceToAnimals', 'hasRabbitFood'] - // } + '*': true, }; diff --git a/config/routes.js b/config/routes.js index 5d769d3..97709e7 100644 --- a/config/routes.js +++ b/config/routes.js @@ -1,49 +1,7 @@ -/** - * Route Mappings - * (sails.config.routes) - * - * Your routes map URLs to views and controllers. - * - * If Sails receives a URL that doesn't match any of the routes below, - * it will check for matching files (images, scripts, stylesheets, etc.) - * in your assets directory. e.g. `http://localhost:1337/images/foo.jpg` - * might match an image file: `/assets/images/foo.jpg` - * - * Finally, if those don't match either, the default 404 handler is triggered. - * See `api/responses/notFound.js` to adjust your app's 404 logic. - * - * Note: Sails doesn't ACTUALLY serve stuff from `assets`-- the default Gruntfile in Sails copies - * flat files from `assets` to `.tmp/public`. This allows you to do things like compile LESS or - * CoffeeScript for the front-end. - * - * For more information on configuring custom routes, check out: - * http://sailsjs.org/#!/documentation/concepts/Routes/RouteTargetSyntax.html - */ - module.exports.routes = { - /*************************************************************************** - * * - * Make the view located at `views/homepage.ejs` (or `views/homepage.jade`, * - * etc. depending on your default view engine) your home page. * - * * - * (Alternatively, remove this and add an `index.html` file in your * - * `assets` directory) * - * * - ***************************************************************************/ - '/': { view: 'homepage' - } - - /*************************************************************************** - * * - * Custom routes here... * - * * - * If a request to a URL doesn't match any of the custom routes above, it * - * is matched against Sails route blueprints. See `config/blueprints.js` * - * for configuration options and examples. * - * * - ***************************************************************************/ - + }, + '/logout': 'AuthController.logout' }; diff --git a/package.json b/package.json index 50c3baa..f15ce2a 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "include-all": "^1.0.0", "log4js": "^1.0.1", "moment": "^2.17.1", + "passport": "^0.3.2", + "passport-google-oauth": "^1.0.0", "rc": "1.0.1", "sails": "~0.12.7", "sails-db-migrate": "^1.5.0", From fbfca7a34fbcf64a896627000d1a7450d91139cf Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Wed, 3 May 2017 18:20:53 -0400 Subject: [PATCH 07/35] Add login and logout link --- api/controllers/HomeController.js | 7 +++++++ config/routes.js | 4 +--- views/partials/header.ejs | 9 +++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 api/controllers/HomeController.js diff --git a/api/controllers/HomeController.js b/api/controllers/HomeController.js new file mode 100644 index 0000000..659db9d --- /dev/null +++ b/api/controllers/HomeController.js @@ -0,0 +1,7 @@ +module.exports = { + index: function(req, res) { + res.view('homepage', { + user: req.user + }); + } +}; diff --git a/config/routes.js b/config/routes.js index 97709e7..031f15d 100644 --- a/config/routes.js +++ b/config/routes.js @@ -1,7 +1,5 @@ module.exports.routes = { + '/': 'HomeController.index', - '/': { - view: 'homepage' - }, '/logout': 'AuthController.logout' }; diff --git a/views/partials/header.ejs b/views/partials/header.ejs index af93e49..415a269 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -3,6 +3,15 @@
+ + <% if (user) { %> + + <%= user.name %> + <% } else { %> + + Login + <% } %> +
From 567ad3a70410c8d01ba26010d62c5411f712ccce Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Thu, 4 May 2017 20:29:56 -0400 Subject: [PATCH 08/35] Show tooltip of real user on chat usernames --- api/controllers/ChatController.js | 53 +++++++++++++++++++------------ assets/components/chat.html | 2 +- assets/styles/jukebot.css | 8 +++++ 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/api/controllers/ChatController.js b/api/controllers/ChatController.js index 71788ec..0aee4f8 100644 --- a/api/controllers/ChatController.js +++ b/api/controllers/ChatController.js @@ -2,26 +2,39 @@ var typers = []; module.exports = { subscribe(req, res) { - req.socket.join('chatting'); - ChatService.getChats().then((chats) => { - sails.io.sockets.in('chatting').emit('chats', chats); - }); + if (req.session.passport) { + User.findOne({ + id: req.session.passport.user + }).then(user => { + subscribe(req, res, user.name); + }); + } else { + subscribe(req, res, null); + } + } +}; - req.socket.on('chat', function(chat) { - ChatService.addUserMessage(chat); - }); +function subscribe(req, res, user) { + req.socket.join('chatting'); + ChatService.getChats().then((chats) => { + sails.io.sockets.in('chatting').emit('chats', chats); + }); - req.socket.join('typers'); + req.socket.on('chat', function(chat) { + chat.realuser = user; + ChatService.addUserMessage(chat); + }); - req.socket.on('typers', function(data) { - var index = typers.indexOf(data.username); - if (data.typing && index === -1) { - typers.push(data.username); - sails.io.sockets.in('typers').emit('typers', typers); - } else if (!data.typing && index !== -1) { - typers.splice(index, 1); - sails.io.sockets.in('typers').emit('typers', typers); - } - }); - } -}; + req.socket.join('typers'); + + req.socket.on('typers', function(data) { + var index = typers.indexOf(data.username); + if (data.typing && index === -1) { + typers.push(data.username); + sails.io.sockets.in('typers').emit('typers', typers); + } else if (!data.typing && index !== -1) { + typers.splice(index, 1); + sails.io.sockets.in('typers').emit('typers', typers); + } + }); +} diff --git a/assets/components/chat.html b/assets/components/chat.html index cfc2784..8d6177c 100644 --- a/assets/components/chat.html +++ b/assets/components/chat.html @@ -14,7 +14,7 @@

Chat
-
+
{{chat.type == 'user' ? chat.username : 'JukeBot'}}
diff --git a/assets/styles/jukebot.css b/assets/styles/jukebot.css index e03b6f3..a8131fa 100644 --- a/assets/styles/jukebot.css +++ b/assets/styles/jukebot.css @@ -53,6 +53,14 @@ input { margin-left: 37.5%; } +#chat-list [data-tooltip]:not([data-position]):before { + left: 75%; +} + +#chat-list [data-tooltip]:not([data-position]):after { + left: 100%; +} + .d-f { display: flex; } From d038662af43aae8750eb045ded2bded4b6cc05ee Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Thu, 4 May 2017 21:06:33 -0400 Subject: [PATCH 09/35] Add real user tooltips to liseners section --- api/controllers/ApiController.js | 69 +++++++++++++++++----------- views/partials/currently-playing.ejs | 4 +- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/api/controllers/ApiController.js b/api/controllers/ApiController.js index 5849c68..c750c1d 100644 --- a/api/controllers/ApiController.js +++ b/api/controllers/ApiController.js @@ -6,35 +6,15 @@ var recentlyLeft = []; module.exports = { subscribeUsers: function(req, res) { - var params = req.allParams(); - var id = req.socket.id; - req.socket.join('listeners'); - - var username = params.username || 'Anonymous'; - users[id] = username; - - var index = recentlyLeft.indexOf(username); - if (index === -1) { - logger.debug(users[id] + ' entered the room'); - ChatService.addMachineMessage(users[id] + ' entered the room', username, 'userEnter'); + if (req.session.passport) { + User.findOne({ + id: req.session.passport.user + }).then(user => { + subscribeUsers(req, res, user.name); + }); } else { - recentlyLeft.splice(index, 1); + subscribeUsers(req, res, null); } - emitListeners(); - - req.socket.on('disconnect', function() { - var username = users[id]; - recentlyLeft.push(username); - delete users[id]; - setTimeout(function() { - userDisconnected(username); - }, 1000); - }); - - req.socket.on('username', function(d) { - users[id] = d || 'Anonymous'; - emitListeners(); - }); }, add: function(req, res) { @@ -145,6 +125,41 @@ module.exports = { } }; +function subscribeUsers(req, res, realuser) { + var params = req.allParams(); + var id = req.socket.id; + req.socket.join('listeners'); + + var username = params.username || 'Anonymous'; + users[id] = { + username, + realuser + }; + + var index = recentlyLeft.indexOf(username); + if (index === -1) { + logger.debug(users[id].username + ' entered the room'); + ChatService.addMachineMessage(users[id].username + ' entered the room', username, 'userEnter'); + } else { + recentlyLeft.splice(index, 1); + } + emitListeners(); + + req.socket.on('disconnect', function() { + var username = users[id].username; + recentlyLeft.push(username); + delete users[id]; + setTimeout(function() { + userDisconnected(username); + }, 1000); + }); + + req.socket.on('username', function(d) { + users[id].username = d || 'Anonymous'; + emitListeners(); + }); +} + function userDisconnected(username) { var index = recentlyLeft.indexOf(username); if (index !== -1) { diff --git a/views/partials/currently-playing.ejs b/views/partials/currently-playing.ejs index 32a07f0..fe0dc7b 100644 --- a/views/partials/currently-playing.ejs +++ b/views/partials/currently-playing.ejs @@ -45,8 +45,8 @@
- - {{user}} + + {{user.username}}
From c162e8ed3d7ad42dd35ce832b4a3192e3b31d50b Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Thu, 4 May 2017 21:23:39 -0400 Subject: [PATCH 10/35] Show real username tooltip on videos --- api/controllers/ApiController.js | 22 +++++++++++++++------- api/controllers/SlackController.js | 2 +- api/services/SyncService.js | 2 +- api/services/YouTubeService.js | 13 +++++++------ assets/components/playlist-item.html | 2 +- views/partials/currently-playing.ejs | 2 +- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/api/controllers/ApiController.js b/api/controllers/ApiController.js index c750c1d..480181d 100644 --- a/api/controllers/ApiController.js +++ b/api/controllers/ApiController.js @@ -20,10 +20,14 @@ module.exports = { add: function(req, res) { var params = req.allParams(); try { - var key = YouTubeService.parseYouTubeLink(params.link); - YouTubeService.getYouTubeVideo(key, params.user || 'Anonymous').then(SyncService.addVideo).then(SyncService.sendAddMessages).then(function(video) { - SyncService.resetAutoplayStreak(); - res.send(200); + User.findOne({ + id: req.session.passport ? req.session.passport.user : null + }).then(user => { + var key = YouTubeService.parseYouTubeLink(params.link); + YouTubeService.getYouTubeVideo(key, params.user || 'Anonymous', user ? user.name : null).then(SyncService.addVideo).then(SyncService.sendAddMessages).then(function(video) { + SyncService.resetAutoplayStreak(); + res.send(200); + }); }).catch(function(err) { res.send(400, err); }); @@ -35,9 +39,13 @@ module.exports = { addPlaylist: function(req, res) { var params = req.allParams(); try { - YouTubeService.getPlaylistVideos(params.playlistId, params.user || 'Anonymous').then(SyncService.addPlaylist).then(SyncService.sendPlaylistAddMessages).then(function(video) { - SyncService.resetAutoplayStreak(); - res.send(200); + User.findOne({ + id: req.session.passport ? req.session.passport.user : null + }).then(user => { + YouTubeService.getPlaylistVideos(params.playlistId, params.user || 'Anonymous', user ? user.name : null).then(SyncService.addPlaylist).then(SyncService.sendPlaylistAddMessages).then(function(video) { + SyncService.resetAutoplayStreak(); + res.send(200); + }); }).catch(function(err) { res.send(400, err); }); diff --git a/api/controllers/SlackController.js b/api/controllers/SlackController.js index 6844b57..74d19b9 100644 --- a/api/controllers/SlackController.js +++ b/api/controllers/SlackController.js @@ -17,7 +17,7 @@ module.exports = { switch (command) { case 'add': var key = YouTubeService.parseYouTubeLink(args[0]); - YouTubeService.getYouTubeVideo(key, '@' + params.user_name).then(SyncService.addVideo).then(SyncService.sendAddMessages).then(function(video) { + YouTubeService.getYouTubeVideo(key, '@' + params.user_name, null).then(SyncService.addVideo).then(SyncService.sendAddMessages).then(function(video) { res.send('Successfully added "' + video.title + '"'); }).catch(function(err) { res.send(err); diff --git a/api/services/SyncService.js b/api/services/SyncService.js index 5a0d591..078c517 100644 --- a/api/services/SyncService.js +++ b/api/services/SyncService.js @@ -148,7 +148,7 @@ function findNextVideo(lastVideo) { } else if (autoplay) { autoplayStreak++; YouTubeService.nextRelated(lastVideo.key).then(function(nextKey) { - return YouTubeService.getYouTubeVideo(nextKey, 'Autoplay'); + return YouTubeService.getYouTubeVideo(nextKey, 'Autoplay', null); }).then(addVideo).then(sendAddMessages); } }); diff --git a/api/services/YouTubeService.js b/api/services/YouTubeService.js index f8c2b4d..bb4bb34 100644 --- a/api/services/YouTubeService.js +++ b/api/services/YouTubeService.js @@ -28,12 +28,12 @@ function parseYouTubeLink(link) { return match[1] || match[2]; } -function getYouTubeVideo(key, user, canSave=true) { +function getYouTubeVideo(key, user, realuser, canSave=true) { return new Promise((resolve, reject) => { request(`https://www.googleapis.com/youtube/v3/videos?id=${key}&part=snippet,contentDetails&key=${process.env.GOOGLE_API_KEY}`, (error, response, body) => { if (!error && response.statusCode == 200) { try { - parseYouTubeVideo(JSON.parse(body), user, canSave).then((video, err) => { + parseYouTubeVideo(JSON.parse(body), user, realuser, canSave).then((video, err) => { if (err) { throw err; } @@ -50,7 +50,7 @@ function getYouTubeVideo(key, user, canSave=true) { }); } -function parseYouTubeVideo(data, user, canSave) { +function parseYouTubeVideo(data, user, realuser, canSave) { if (data.items.length != 1) { throw 'YouTube link did not match a video'; } @@ -59,6 +59,7 @@ function parseYouTubeVideo(data, user, canSave) { key: item.id, duration: moment.duration(item.contentDetails.duration).asMilliseconds(), user: user, + realuser: realuser, isSuggestion: canSave ? false : true, thumbnail: item.snippet.thumbnails ? item.snippet.thumbnails.default.url : null, title: item.snippet.title @@ -144,7 +145,7 @@ function relatedVideos(key, maxResults = 10) { } var itemsPromise = items.map((i) => { - return getYouTubeVideo(i.id.videoId, 'JukeBot', false); + return getYouTubeVideo(i.id.videoId, 'JukeBot', null, false); }); return Promise.all(itemsPromise) @@ -158,10 +159,10 @@ function relatedVideos(key, maxResults = 10) { }); } -function getPlaylistVideos(playlistId, user) { +function getPlaylistVideos(playlistId, user, realuser) { return getPlaylistVideosRecursive(playlistId, [], '').then(videos => { return Promise.all(videos.map(function(video) { - return getYouTubeVideo(video.snippet.resourceId.videoId, user); + return getYouTubeVideo(video.snippet.resourceId.videoId, user, realuser); })); }); } diff --git a/assets/components/playlist-item.html b/assets/components/playlist-item.html index 3a5838d..f7aa439 100644 --- a/assets/components/playlist-item.html +++ b/assets/components/playlist-item.html @@ -7,7 +7,7 @@
- Added by {{$ctrl.video.user}} + Added by {{$ctrl.video.user}}
({{$ctrl.formatDuration()}})
diff --git a/views/partials/currently-playing.ejs b/views/partials/currently-playing.ejs index fe0dc7b..2c2bfc3 100644 --- a/views/partials/currently-playing.ejs +++ b/views/partials/currently-playing.ejs @@ -20,7 +20,7 @@ {{currentVideo().title}}
- Added by {{currentVideo().user}} + Added by {{currentVideo().user}}
From 0131379271eaca9a46012c47b95a946fdc83b784 Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Fri, 5 May 2017 11:34:37 -0400 Subject: [PATCH 11/35] Run Docker container with NPM so version number is injected Closes #189 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b784aff..9d295d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ COPY . /usr/src/app EXPOSE 1337 -CMD if [[ -z "${MYSQL_HOST}" ]]; then npm start; else npm run migrate && node app.js; fi +CMD if [[ -z "${MYSQL_HOST}" ]]; then npm start; else npm run migrate && npm start; fi From 4b2b33bdd50235fa65df755174bdc4a7cf2d1ca1 Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Fri, 5 May 2017 11:45:49 -0400 Subject: [PATCH 12/35] Better handle logout events --- api/controllers/ApiController.js | 2 +- api/controllers/ChatController.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/ApiController.js b/api/controllers/ApiController.js index 480181d..ced77c6 100644 --- a/api/controllers/ApiController.js +++ b/api/controllers/ApiController.js @@ -10,7 +10,7 @@ module.exports = { User.findOne({ id: req.session.passport.user }).then(user => { - subscribeUsers(req, res, user.name); + subscribeUsers(req, res, user ? user.name : null); }); } else { subscribeUsers(req, res, null); diff --git a/api/controllers/ChatController.js b/api/controllers/ChatController.js index 0aee4f8..94692eb 100644 --- a/api/controllers/ChatController.js +++ b/api/controllers/ChatController.js @@ -6,7 +6,7 @@ module.exports = { User.findOne({ id: req.session.passport.user }).then(user => { - subscribe(req, res, user.name); + subscribe(req, res, user ? user.name : null); }); } else { subscribe(req, res, null); From a94abf22bb1eac927a3a342c2797d765b9af12f2 Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Fri, 5 May 2017 11:50:22 -0400 Subject: [PATCH 13/35] Delimit listener list by commas without CSS --- assets/styles/jukebot.css | 4 ---- views/partials/currently-playing.ejs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/assets/styles/jukebot.css b/assets/styles/jukebot.css index a8131fa..9557f89 100644 --- a/assets/styles/jukebot.css +++ b/assets/styles/jukebot.css @@ -1,9 +1,5 @@ /* JukeBot Styles */ -.listeners span + span:before { - content: ', '; -} - #video-list { height: calc(100vh - 130px); overflow-y: auto; diff --git a/views/partials/currently-playing.ejs b/views/partials/currently-playing.ejs index 2c2bfc3..6e32632 100644 --- a/views/partials/currently-playing.ejs +++ b/views/partials/currently-playing.ejs @@ -46,7 +46,7 @@
- {{user.username}} + {{user.username}}{{$last ? '' : ', '}}
From c91d90264b6a43006d7504b5733e153f01802a19 Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Wed, 3 May 2017 18:52:26 -0400 Subject: [PATCH 14/35] Add user property controller --- api/controllers/ProfileController.js | 53 ++++++++++++++++++++++++++++ api/policies/isAuthenticated.js | 7 ++++ config/policies.js | 4 +++ 3 files changed, 64 insertions(+) create mode 100644 api/controllers/ProfileController.js create mode 100644 api/policies/isAuthenticated.js diff --git a/api/controllers/ProfileController.js b/api/controllers/ProfileController.js new file mode 100644 index 0000000..f3a4335 --- /dev/null +++ b/api/controllers/ProfileController.js @@ -0,0 +1,53 @@ +module.exports = { + + get: function(req, res) { + return User.findOne({ + id: req.session.passport.user + }) + .then(user => { + if (user) { + return res.ok(user.properties); + } else { + return res.notFound(); + } + }); + }, + + add: function(req, res) { + return User.findOne({ + id: req.session.passport.user + }) + .then(user => { + if (user) { + var body = req.body || {}; + Object.keys(body).forEach(key => { + user.properties[key] = body[key]; + }); + return user.save().then(() => { + return res.ok(user.properties); + }); + } else { + return res.notFound(); + } + }); + }, + + remove: function(req, res) { + return User.findOne({ + id: req.session.passport.user + }) + .then(user => { + if (user) { + var body = req.body || []; + body.forEach(key => { + delete user.properties[key]; + }); + return user.save().then(() => { + return res.ok(user.properties); + }); + } else { + return res.notFound(); + } + }); + } +}; diff --git a/api/policies/isAuthenticated.js b/api/policies/isAuthenticated.js new file mode 100644 index 0000000..e38a547 --- /dev/null +++ b/api/policies/isAuthenticated.js @@ -0,0 +1,7 @@ +module.exports = function(req, res, next) { + if (req.isAuthenticated()) { + return next(); + } else { + return res.send(401); + } +}; diff --git a/config/policies.js b/config/policies.js index 3ae3aff..876f077 100644 --- a/config/policies.js +++ b/config/policies.js @@ -1,4 +1,8 @@ module.exports.policies = { '*': true, + + 'ProfileController': { + '*': 'isAuthenticated' + } }; From 52db2aa174de65a422ad6f9820a9c7ba6efa6790 Mon Sep 17 00:00:00 2001 From: TheConnMan Date: Sat, 13 May 2017 12:50:26 -0400 Subject: [PATCH 15/35] Add persisted profile settings --- assets/components/playlist.html | 2 - assets/js/app.js | 32 +++++----------- assets/js/components/chat.js | 27 +++++--------- assets/js/components/playlist-item.js | 9 ++--- assets/js/components/playlist.js | 15 +------- assets/js/profile.js | 54 +++++++++++++++++++++++++++ assets/js/video.js | 6 +-- views/layout.ejs | 9 ++++- views/partials/currently-playing.ejs | 2 +- views/partials/header.ejs | 4 +- 10 files changed, 93 insertions(+), 67 deletions(-) create mode 100644 assets/js/profile.js diff --git a/assets/components/playlist.html b/assets/components/playlist.html index 23b7f90..00338d5 100644 --- a/assets/components/playlist.html +++ b/assets/components/playlist.html @@ -24,7 +24,6 @@
= new Date(Date.now() - chatHistory * 60 * 1000); }); }; - this.getUsername = function() { - return self.username || 'Anonymous'; - }; - this.showUsername = function(index) { let isFirst = index === 0; if (isFirst) { @@ -45,7 +41,7 @@ function ChatController($rootScope, $scope, $http, $notification, $storage, $vid this.sendChat = function() { io.socket._raw.emit('chat', { message: this.newChat, - username: this.getUsername(), + username: $rootScope.profile.username, type: 'user', time: Date.now() }); @@ -79,8 +75,8 @@ function ChatController($rootScope, $scope, $http, $notification, $storage, $vid io.socket.on('chat', (c) => { this.chats.push(c); - if (c.username !== this.getUsername() && this.notifications && c.type !== 'addVideo' && c.type !== 'videoSkipped' && c.type !== 'videoPlaying') { - let notification = $notification(c.username || 'JukeBot', { + if (c.username !== $rootScope.profile.username && this.notifications && c.type !== 'addVideo' && c.type !== 'videoSkipped' && c.type !== 'videoPlaying') { + let notification = $notification(c.username || 'Anonymous', { body: c.message, delay: 4000, icon: '/images/jukebot-72.png', @@ -100,15 +96,15 @@ function ChatController($rootScope, $scope, $http, $notification, $storage, $vid accuracy: 'exactly', className: 'highlight' }; - $('.chat > span').mark(self.getUsername(), markOptions); - $('.chat > span').mark('@' + self.getUsername(), markOptions); + $('.chat > span').mark($rootScope.profile.username, markOptions); + $('.chat > span').mark('@' + $rootScope.profile.username, markOptions); $('.chat > span').mark('@here', markOptions); $('.chat > span').mark('@channel', markOptions); } io.socket.on('typers', (typers) => { - if (typers.indexOf(self.getUsername()) !== -1) { - typers.splice(typers.indexOf(self.getUsername()), 1); + if (typers.indexOf($rootScope.profile.username) !== -1) { + typers.splice(typers.indexOf($rootScope.profile.username), 1); } if (typers.length === 0) { self.typers = ''; @@ -125,7 +121,7 @@ function ChatController($rootScope, $scope, $http, $notification, $storage, $vid function typing(isTyping) { io.socket._raw.emit('typers', { typing: isTyping, - username: self.getUsername() + username: $rootScope.profile.username }); } @@ -141,8 +137,5 @@ angular .module('app') .component('chat', { templateUrl: 'components/chat.html', - controller: ChatController, - bindings: { - username: '=' - } + controller: ChatController }); diff --git a/assets/js/components/playlist-item.js b/assets/js/components/playlist-item.js index cbf6abe..aa5dec4 100644 --- a/assets/js/components/playlist-item.js +++ b/assets/js/components/playlist-item.js @@ -1,4 +1,4 @@ -function PlaylistItemController($scope, $http, $video, $storage) { +function PlaylistItemController($rootScope, $scope, $http, $video, $storage) { this.formatDuration = function() { return $video.formatDuration(this.video.duration); }; @@ -12,15 +12,15 @@ function PlaylistItemController($scope, $http, $video, $storage) { }; this.skip = function() { - return $video.skip(this.username); + return $video.skip(); }; this.readd = function() { - return $video.addByKey(this.username, this.video.key); + return $video.addByKey($rootScope.profile.username, this.video.key); }; this.remove = function() { - return $video.removePermanently(this.username, this.video.id); + return $video.removePermanently($rootScope.profile.username, this.video.id); }; this.likeVideo = function() { @@ -44,7 +44,6 @@ angular controller: PlaylistItemController, bindings: { video: '<', - username: '<', showAddedBy: '<', showExpectedPlaytime: '<', isSuggestion: '<', diff --git a/assets/js/components/playlist.js b/assets/js/components/playlist.js index 43a9842..0772ce3 100644 --- a/assets/js/components/playlist.js +++ b/assets/js/components/playlist.js @@ -1,11 +1,8 @@ function PlaylistController($rootScope, $scope, $video, $storage, $log, $notification) { let self = this; - self.notifications = $storage.get('notifications') === 'true' || !$storage.get('notifications'); self.activeTab = 'up-next'; self.relatedVideos = []; - $rootScope.notifications = self.notifications; - $video.getAll().then(function() { setTimeout(function() { self.scrollToCurrentlyPlaying(); @@ -17,7 +14,7 @@ function PlaylistController($rootScope, $scope, $video, $storage, $log, $notific $log.log('Received a video update'); $log.log(obj); if (obj.verb === 'created') { - if ($video.current() && $scope.username !== obj.data.user && self.notifications) { + if ($video.current() && $rootScope.profile.username !== obj.data.user && $rootScope.profile.videoNotifications) { $notification('New Video Added', { body: obj.data.user + ' added ' + obj.data.title, icon: obj.data.thumbnail, @@ -44,11 +41,6 @@ function PlaylistController($rootScope, $scope, $video, $storage, $log, $notific return $video.getVideos(); }; - this.toggleNotifications = function(newVal) { - $storage.set('notifications', newVal); - $rootScope.notifications = newVal; - }; - this.scrollToCurrentlyPlaying = function() { let $list = $('#video-list'); let $playing = $list.find('.yellow').closest('playlistitem'); @@ -71,8 +63,5 @@ angular .module('app') .component('playlist', { templateUrl: 'components/playlist.html', - controller: PlaylistController, - bindings: { - username: '=' - } + controller: PlaylistController }); diff --git a/assets/js/profile.js b/assets/js/profile.js new file mode 100644 index 0000000..e172023 --- /dev/null +++ b/assets/js/profile.js @@ -0,0 +1,54 @@ +angular +.module('profile', []) +.factory('$profile', ['$rootScope', '$http', '$storage', function($rootScope, $http, $storage) { + + $rootScope.profile = { + username: $storage.get('username'), + theme: $storage.get('theme'), + minimizeVideo: $storage.get('minimizeVideo') === 'true', + videoNotifications: $storage.get('videoNotifications') === 'true' || !$storage.get('videoNotifications'), + chatNotifications: $storage.get('chatNotifications') === 'true' || !$storage.get('chatNotifications') + }; + + $rootScope.$watch('profile.username', function() { + persistProperty('username', $rootScope.profile.username); + }); + + $rootScope.$watch('profile.theme', function() { + persistProperty('theme', $rootScope.profile.theme); + }); + + $rootScope.$watch('profile.minimizeVideo', function() { + persistProperty('minimizeVideo', $rootScope.profile.minimizeVideo); + }); + + $rootScope.$watch('profile.videoNotifications', function() { + persistProperty('videoNotifications', $rootScope.profile.videoNotifications); + }); + + $rootScope.$watch('profile.chatNotifications', function() { + persistProperty('chatNotifications', $rootScope.profile.chatNotifications); + }); + + if (loggedIn) { + $http.get('/profile/get').then(({ data }) => { + Object.keys(data).forEach(key => { + $rootScope.profile[key] = data[key]; + }); + persistProperties(); + }); + } + + function persistProperty(key, value) { + $storage.set(key, value); + var payload = {}; + payload[key] = value; + return loggedIn ? $http.post('/profile/add', payload) : Promise.resolve(); + } + + function persistProperties() { + return loggedIn ? $http.post('/profile/add', $rootScope.profile) : Promise.resolve(); + } + + return {}; +}]); diff --git a/assets/js/video.js b/assets/js/video.js index 44dc897..d8651e7 100644 --- a/assets/js/video.js +++ b/assets/js/video.js @@ -1,6 +1,6 @@ angular .module('video', []) -.factory('$video', ['$http', '$log', function($http, $log) { +.factory('$video', ['$rootScope', '$http', '$log', function($rootScope, $http, $log) { var videos = []; const YOUTUBE_URL = 'https://www.youtube.com/watch?v='; @@ -44,9 +44,9 @@ angular }); } - function skip(username) { + function skip() { return $http.post('/api/skip', { - username + username: $rootScope.profile.username }); } diff --git a/views/layout.ejs b/views/layout.ejs index 4b43a9c..b05c233 100644 --- a/views/layout.ejs +++ b/views/layout.ejs @@ -31,7 +31,7 @@ - +
<%- body -%>
@@ -47,8 +47,13 @@ + + + @@ -56,7 +61,7 @@ - + diff --git a/views/partials/currently-playing.ejs b/views/partials/currently-playing.ejs index 6e32632..22a4d2b 100644 --- a/views/partials/currently-playing.ejs +++ b/views/partials/currently-playing.ejs @@ -8,7 +8,7 @@ class="big heart {{likesCurrentVideo() ? 'red' : 'outline'}} like icon">
- +
diff --git a/views/partials/header.ejs b/views/partials/header.ejs index 415a269..eda0071 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -14,7 +14,7 @@
- +
@@ -29,7 +29,7 @@