Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
TheConnMan committed May 21, 2017
2 parents 078effb + 2ccd489 commit 840d863
Show file tree
Hide file tree
Showing 50 changed files with 969 additions and 564 deletions.
38 changes: 13 additions & 25 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
sudo: required

language: node_js
node_js:
- "6"

services:
- docker

script:
- docker build -t theconnman/jukebot .

after_success:
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
VERSION=`node -e "console.log(require('./package.json').version);"`;
if [ "$TRAVIS_BRANCH" == "master" ]; then
docker tag theconnman/jukebot theconnman/jukebot:$VERSION;
docker push theconnman/jukebot:latest;
docker push theconnman/jukebot:$VERSION;
elif [ "$TRAVIS_BRANCH" == "dev" ]; then
sed -i "s/$VERSION/$VERSION-dev/g" package.json;
docker build -t theconnman/jukebot .;
docker tag theconnman/jukebot theconnman/jukebot:latest-dev;
docker push theconnman/jukebot:latest-dev;
fi
sudo: required

language: node_js
node_js:
- "6"

install: true

services:
- docker

script:
- chmod a+x travis-build.sh && ./travis-build.sh
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && npm start; fi
85 changes: 59 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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=<your-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=<your-API-key> npm start` and go to <http://localhost:1337>

## 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 <https://console.developers.google.com>
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 <youtube-url> - 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

<youtube-url> - Add a YouTube video to the playlist"</youtube-url>

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
Expand All @@ -54,29 +62,54 @@ docker run -d -p 80:1337 -e GOOGLE_API_KEY=<your-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
```

### 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=<migration-name>` and implement the `up` and `down` steps once the migration is created.

## Environment Variables

- **GOOGLE_API_KEY** - Google project API key
- **GOOGLE_ID** (Optional) - Google OAuth application ID (set up a [Google App](https://cloud.google.com/console#/project), create OAuth credentials, and enable the Google+ API) - **NOTE:** Persistence of user properties and favorites is disabled if this is not provided
- **GOOGLE_SECRET** (Optional) - Google OAuth secret key - **NOTE:** Persistence of user properties and favorites is disabled if this is not provided
- **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
- **SLACK_SONG_PLAYING** (default: true) - Toggle for "New Song Playing" Slack notifications, only applicable if **SLACK_WEBHOOK** is provided
Expand Down
91 changes: 57 additions & 34 deletions api/controllers/ApiController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,28 @@ 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 ? user.name : null);
});
} 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) {
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);
});
Expand All @@ -55,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);
});
Expand Down Expand Up @@ -145,6 +133,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) {
Expand Down
31 changes: 31 additions & 0 deletions api/controllers/AuthController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
var passport = require('passport');

module.exports = {

index: function(req, res) {
res.view();
},

logout: function(req, res) {
req.logout();
res.redirect('/');
},

google: function(req, res) {
passport.authenticate('google', {
failureRedirect: '/login',
scope: ['https://www.googleapis.com/auth/plus.login', 'https://www.googleapis.com/auth/plus.profile.emails.read']
}, function(err, user) {
req.logIn(user, function(err) {
if (err) {
console.log(err);
res.view('500');
return;
}

res.redirect('/');
return;
});
})(req, res);
}
};
53 changes: 33 additions & 20 deletions api/controllers/ChatController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? user.name : null);
});
} 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);
}
});
}
Loading

0 comments on commit 840d863

Please sign in to comment.