Minimal server side so that boats can groove
If you don't know, you better check out what's Grooveboat.
To run this, you'll need a recent version of node and yarn (or npm) installed.
Clone the repo:
$ git clone https://github.com/stevenleeg/groovebuoy.git
Run setup:
$ ./setup.sh
Spin up a local webserver:
$ yarn start
The buoy will be spun up and a seed invite will be available for you in the console, you can share it with your friends so they can join this buoy.
The main piece of information contained in the seed invite is the URL to this server, so you should make sure that this URL is reachable by the peers.
If you're interested in creating a bot (or alternative client) for Grooveboat, this reference should provide you with all of the information necessary to do so. In addition, you may wish to browse through Groovebot to see an example of what this could look like in practice with a Node.js bot.
Table of Contents:
- Connecting
- Authentication
- Server RPC Methods
- Client RPC Methods
- Object schemas
Buoys expose an RPC API using socket.io as the communication medium. Socket.io has libraries in a variety of different languages (see python, golang, etc.) that can be used to develop first-class clients with buoys.
For the sake of simplicity, this guide will be written using the standard Node client library.
Connecting to a buoy looks more or less like this:
const io = require('socket.io-client');
// (Assuming you're running a buoy locally)
const socket = io('ws://localhost:8000');
socket.on('connect', () => {
console.log('connected to buoy!');
});
Once you're connected you'll have access to an RPC API, which can be used to make API calls like so:
socket.emit('call', {name: 'methodName', params: {some: 'params', go: 'here'}});
It's usually wise to set up a helper function to wrap this, eg:
const callRPC = ({name, params}) => {
return new Promise((resolve, reject) => {
this.socket.emit('call', {name, params}, (resp) => {
resolve(resp);
});
});
};
This will allow you to use the async/await syntax:
const resp = await callRPC({name: 'someMethod', params: {some: 'params'}});
console.log(resp);
The remainder of this guide will assume you have access to the callRPC
function.
TODO: At some point in the future we'll write a nice node wrapper to build bots with, but for now this guide will have to do.
Once you've connected to a buoy, your client has ~5 seconds to authenticate. The authentication process requires an invite code in the form of a JSON Web Token signed by the buoy. As of now the only way to get one of these tokens is by looking at the logs of the buoy as it starts, which should look like this:
yarn run v1.6.0
$ babel-node index.js --presets es2015,stage-2
Seed invite:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJnIjoxNTYyNzA3NDQ2LCJ1Ijoid3M6Ly8xMC4wLjEzLjgwOjgwMDAvIiwibiI6InllZXQgcGFsYWNlIiwiaWF0IjoxNTYyNzA3NDQ2fQ.fHo1mCwexgnBADzHshI_LHz3J-yLudHCAvtp3G-82zo
Server is listening on port 8000
YIKES: The infrastructure around invite codes will be changing shortly, as this quite obviously sucks and needs some rethinking.
This invite code will allow a client to join a server and receive an authentication token, which can then be used to authenticate in subsequent connections.
Joining a server:
const INVITE_CODE = '...';
socket.on('connect', async () => {
const resp = await callRPC({name: 'join', params: {jwt: INVITE_CODE});
if (resp.error) {
console.log('could not join server:', resp.message);
return;
}
const {token, peerId} = resp;
console.log('joined buoy with peerId', peerId);
console.log('received auth token:', token);
// ... the client should store the auth token in a safe place.
});
After joining, the client can authenticate with the auth token like so:
const authToken = fetchAuthTokenFromStorage();
socket.on('connect', () => {
const resp = await callRPC({name: 'authenticate', params: {jwt: authToken}});
if (resp.error) {
console.log('could not join server:', resp.message);
return;
}
const {peerId} = resp;
console.log('connected to buoy with peerId', peerId);
});
Note that the join
method will both join the server and authenticate the client, meaning you do not need to join and then authenticate on the first connection to a buoy.
Once you've successfully authenticated you have access to the full suite of RPC methods, all of which are listed below.
Authenticate to the server using an auth token. The auth token is a JWT with the following schema:
{
"sid": "buoy_id", // The ID of the buoy
"i": "[uuid]" // The UUID of the authenticated peer
}
Params:
jwt
: The auth token used to authenticate
Response:
success
: A boolean representing whether or not the operation was a success.
Attempt to become a DJ in the room. This method will fail if there are 5 or more DJs already present, or if the peer has yet to join a room.
Note that, if successful, the server will immediately call setDjs
on the client to update the DJ list. The client should not update its internal DJ list after receiving a successful response from this RPC call.
Params:
None
Response:
success
: A boolean representing whether or not the operation was a success.
Creates a new room on the buoy.
Params:
name
: The name of the room
Response:
room
: A serialized room object.adminToken
: A token that can be used to recreate a room with the same ID and admin peer ID in the future (if the room somehow gets deleted).
Fetches the current list of rooms on the buoy
Params:
None
Response:
An array of serialized rooms.
Join the buoy, exchanging an invite code for an authentication token that can be used to authenticate a given peer in future connections.
Params:
jwt
: The invite code
Response:
token
: The auth token that can be used in future connections (presented to theauthenticate
method).peerId
: The ID of the new peer. This identifier will be reused between connections if the client uses thetoken
to authenticate.
Joins a room on the buoy, also subscribing the client to any events that take place within that room.
Params:
id
: The ID of the room to join
Response:
A serialized room object.
Leaves the peer's current room. This method will also unsubscribe the peer from any events that take place within the room.
Params:
None
Response:
success
: A boolean representing that the operation was successful.
Sends a chat message to the peer's current room.
Params:
message
: The text message to be sent to the room.
Response:
success
: A boolean representing that the operation was successful.
Sets the profile of the peer. A profile is an object that can contain whatever values, though the default Grooveboat client currently recognizes the following schema:
{
"handle": "coolkid42", // The display name of the peer
"emoji": "π§" // An emoji associated with the peer (their "avatar")
}
Params:
profile
: A profile object, as defined in the description above.
Response:
success
: A boolean representing that the operation was successful.
Skips the peer's turn if they are the active DJ of a room, otherwise fails.
Params:
None
Response:
success
: A boolean representing that the operation was successful.
Steps down from being a DJ in the room, ie returning to the audience. Fails if the peer is not currently a DJ.
Params:
None
Response:
success
: A boolean representing that the operation was successful.
Alerts the buoy that the current track is over, allowing the next DJ to begin their turn. Fails if the peer is not the currently active DJ in the room.
Note that, yes, you can technically be a jerk and not call this method if it is your turn and cause the room to freeze. Generally people will vote you down and cause the song to skip if this happens, so it's best to be a good citizen and call this method as soon as your track has ended.
Notifies the buoy that your currently active queue has been updated. This allows the buoy to intelligently preload tracks from DJs in the room, responding to upcoming DJs changing their queues as necessary. This method should only be called if the peer updates their active queue while being a DJ.
Note that this has no params, as the server will call requestTrack
on the client if it decides it wishes to preload a track off of the peer's freshly updated queue.
Params:
None
Response:
success
: A boolean representing that the operation was successful.
Sets the peer's vote for the currently playing track. Will fail if the user is not currently in a room or there is no track playing.
Note that there is currently no way to revoke a vote, you can only change to the other direction by calling this method again on the same track.
Params:
direction
: A boolean deciding the direction of the vote.true
means up,false
means down.
Response:
success
: A boolean representing that the operation was successful.
Causes the client to cycle the first track of their active queue to the end of the queue. This is used after a peer has completed their turn as DJ in order to prevent the same track from playing on their next turn (assuming they have more than one track in their queue).
Params:
None
Response:
None
Causes the client to append the associated chat message onto their chat log.
Params:
id
: ID of the messagemessage
: Text of the messagefromPeerId
: The peer ID of who sent the message.timestamp
: A timestamp in UNIX epoch time.
Response:
None
Causes the client to begin to play the specified chat. Before loading the track, the client should check the current value of the track on deck. If the on deck track ID matches the provided track ID of this method, it should use the preloaded track rather than reloading the URL from this method.
Params:
track
: A track objectvotes
: An object of votes on the track. The keys will be peer IDs and the values will be vote directions (see vote).startedAt
: The timestamp, in UNIX epoch, that this track should begin playing and have the playhead synced up to. Note that this may be in the future, in which case the client should wait before starting playback.
Response:
None
Requests a track from the client's queue, generally the first track.
Params:
None
Response:
filename
: The name of the track's source file.artist
: The name of the track artists (if present in the file's ID3 data).album
: The name of the album (if present in the file's ID3 data).title
: The name of the track (if present in the file's ID3 data).contentType
: The MIME content type associted with the file.data
: A base64 encoded string representing the binary data of the file.
Sets the active DJ in a room
Params:
djId
: The ID of the peer that is now the active DJ. This value will also be contained by the set of the room's current DJs.
Response:
None
Sets the set of peers that are considered DJs in the room.
Params:
djs
: An array of peer IDs
Response:
None
Provides the client with a track that should be assumed to be the next track after the current track completes playback. The client should begin to preload this track in order to reduce dead airspace due to loading times after the current track is completed.
Params:
track
: A track object.
Response:
None
Sets the current peers in the room.
Params:
peers
: An array of peer objects
Response:
None
Sets the array of rooms currently available on the buoy. This is only called on the client if they are not currently associated with a room (ie they're in the room selector screen on the default Grooveboat client). It is called every time there is a new room, a room's current track is updated, or its peer count changes.
Params:
rooms
: An array of room objects
Response:
None
Sets the skip warning for a track to true
or false
. The skip warning is activated when a song has been downvoted by the room's peers and is about to be skipped. When this is set to true, clients should show a warning that the track will be skipped unless peers change their vote to up.
Params:
value
: A boolean representing whether or not the skip warning is active.
Response:
None
Sets the votes
object for the currently playing track.
Params:
votes
: An object of votes on the track. See playTrack for schema.
Response:
None
Stop playback of the currently playing track. This should also reset any UI the client has displaying the currently playing track to an "awaiting track" state.
Params:
None
Response:
None
id
: ID of the roomname
: Name of the roompeerCount
: The number of peers in the roomnowPlaying
: The currently playing track ornull
If the nowPlaying
key is not null, it will be a Track object
id
: ID of the trackfilename
: The name of the track's source fileartist
: The name of the track artists (if present in the file's ID3 data)album
: The name of the album (if present in the file's ID3 data)title
: The name of the track (if present in the file's ID3 data)url
: (optional) An HTTP(S) URL that can be used to download the file.
When presenting this information to an end user, you should generally fall back to using the filename
key if the artist
, album
, or title
keys are null
.
id
: The ID of the peer.profile
: The profile object of the peer.