Improving web authentication

This commit is contained in:
MichaelOultram 2015-07-28 21:38:35 +01:00
parent 5f6c5425b1
commit 2489093740
9 changed files with 329 additions and 207 deletions

View file

@ -1,6 +1,6 @@
all: mumbledj all: mumbledj
mumbledj: main.go commands.go parseconfig.go strings.go service.go service_youtube.go songqueue.go cache.go mumbledj: main.go commands.go parseconfig.go strings.go service.go service_youtube.go songqueue.go cache.go web.go
go get github.com/nitrous-io/goop go get github.com/nitrous-io/goop
rm -rf Goopfile.lock rm -rf Goopfile.lock
goop install goop install
@ -12,8 +12,10 @@ clean:
install: install:
mkdir -p ~/.mumbledj/config mkdir -p ~/.mumbledj/config
mkdir -p ~/.mumbledj/songs mkdir -p ~/.mumbledj/songs
mkdir -p ~/.mumbledj/web
if [ -a ~/.mumbledj/config/mumbledj.gcfg ]; then mv ~/.mumbledj/config/mumbledj.gcfg ~/.mumbledj/config/mumbledj_backup.gcfg; fi; if [ -a ~/.mumbledj/config/mumbledj.gcfg ]; then mv ~/.mumbledj/config/mumbledj.gcfg ~/.mumbledj/config/mumbledj_backup.gcfg; fi;
cp -u config.gcfg ~/.mumbledj/config/mumbledj.gcfg cp -u config.gcfg ~/.mumbledj/config/mumbledj.gcfg
cp -u index.html ~/.mumbledj/web/index.html
if [ -d ~/bin ]; then cp -f mumbledj* ~/bin/mumbledj; else sudo cp -f mumbledj* /usr/local/bin/mumbledj; fi; if [ -d ~/bin ]; then cp -f mumbledj* ~/bin/mumbledj; else sudo cp -f mumbledj* /usr/local/bin/mumbledj; fi;
build: build:

View file

@ -28,6 +28,7 @@ All commandline parameters are optional. Below are descriptions of all the avail
* `-key`: Path to user PEM key. Defaults to no key. * `-key`: Path to user PEM key. Defaults to no key.
* `-insecure`: If included, the bot will not check the certs for the server. Try using this commandline flag if you are having connection issues. * `-insecure`: If included, the bot will not check the certs for the server. Try using this commandline flag if you are having connection issues.
* `-accesstokens`: List of access tokens for the bot separated by spaces. Defaults to no access tokens. * `-accesstokens`: List of access tokens for the bot separated by spaces. Defaults to no access tokens.
* `-verbose`: Prints out song status into the console.
## FEATURES ## FEATURES
* Plays audio from both YouTube videos and YouTube playlists! * Plays audio from both YouTube videos and YouTube playlists!
@ -50,6 +51,7 @@ Command | Description | Arguments | Admin | Example
**help** | Displays this list of commands in Mumble chat. | None | No | `!help` **help** | Displays this list of commands in Mumble chat. | None | No | `!help`
**volume** | Either outputs the current volume or changes the current volume. If desired volume is not provided, the current volume will be displayed in chat. Otherwise, the volume for the bot will be changed to desired volume if it is within the allowed volume range. | None OR desired volume | No | `!volume 0.5`, `!volume` **volume** | Either outputs the current volume or changes the current volume. If desired volume is not provided, the current volume will be displayed in chat. Otherwise, the volume for the bot will be changed to desired volume if it is within the allowed volume range. | None OR desired volume | No | `!volume 0.5`, `!volume`
**move** | Moves MumbleDJ into channel if it exists. | Channel | Yes | `!move Music` **move** | Moves MumbleDJ into channel if it exists. | Channel | Yes | `!move Music`
**web** | Displays a unique url for the user to control mumbledj from a web browser | None | No | `!web`
**reload** | Reloads `mumbledj.gcfg` to retrieve updated configuration settings. | None | Yes | `!reload` **reload** | Reloads `mumbledj.gcfg` to retrieve updated configuration settings. | None | Yes | `!reload`
**reset** | Stops all audio and resets the song queue. | None | Yes | `!reset` **reset** | Stops all audio and resets the song queue. | None | Yes | `!reset`
**numsongs** | Outputs the number of songs in the queue in chat. Individual songs and songs within playlists are both counted. | None | No | `!numsongs` **numsongs** | Outputs the number of songs in the queue in chat. Individual songs and songs within playlists are both counted. | None | No | `!numsongs`

View file

@ -36,35 +36,35 @@ func parseCommand(user *gumble.User, username, command string) {
// Add command // Add command
case dj.conf.Aliases.AddAlias: case dj.conf.Aliases.AddAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminAdd) { if dj.HasPermission(username, dj.conf.Permissions.AdminAdd) {
add(user, username, argument) add(user, argument)
} else { } else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG) dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
} }
// Skip command // Skip command
case dj.conf.Aliases.SkipAlias: case dj.conf.Aliases.SkipAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) { if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) {
skip(user, username, false, false) skip(user, false, false)
} else { } else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG) dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
} }
// Skip playlist command // Skip playlist command
case dj.conf.Aliases.SkipPlaylistAlias: case dj.conf.Aliases.SkipPlaylistAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminAddPlaylists) { if dj.HasPermission(username, dj.conf.Permissions.AdminAddPlaylists) {
skip(user, username, false, true) skip(user, false, true)
} else { } else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG) dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
} }
// Forceskip command // Forceskip command
case dj.conf.Aliases.AdminSkipAlias: case dj.conf.Aliases.AdminSkipAlias:
if dj.HasPermission(username, true) { if dj.HasPermission(username, true) {
skip(user, username, true, false) skip(user, true, false)
} else { } else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG) dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
} }
// Playlist forceskip command // Playlist forceskip command
case dj.conf.Aliases.AdminSkipPlaylistAlias: case dj.conf.Aliases.AdminSkipPlaylistAlias:
if dj.HasPermission(username, true) { if dj.HasPermission(username, true) {
skip(user, username, true, true) skip(user, true, true)
} else { } else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG) dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
} }
@ -78,14 +78,14 @@ func parseCommand(user *gumble.User, username, command string) {
// Volume command // Volume command
case dj.conf.Aliases.VolumeAlias: case dj.conf.Aliases.VolumeAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminVolume) { if dj.HasPermission(username, dj.conf.Permissions.AdminVolume) {
volume(user, username, argument) volume(user, argument)
} else { } else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG) dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
} }
// Web command // Web command
case dj.conf.Aliases.WebAlias: case dj.conf.Aliases.WebAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminWeb) { if dj.HasPermission(username, dj.conf.Permissions.AdminWeb) {
GetWebAddress(user) web.GetWebAddress(user)
} else { } else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG) dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
} }
@ -166,59 +166,30 @@ func parseCommand(user *gumble.User, username, command string) {
// add performs !add functionality. Checks input URL for service, and adds // add performs !add functionality. Checks input URL for service, and adds
// the URL to the queue if the format matches. // the URL to the queue if the format matches.
func add(user *gumble.User, username, url string) { func add(user *gumble.User, url string) error {
if url == "" { if url == "" {
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG) dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
return errors.New("NO_ARGUMENT")
} else { } else {
var urlService Service return findServiceAndAdd(user, url)
// Checks all services to see if any can take the URL
for _, service := range services {
if service.URLRegex(url) {
urlService = service
}
}
if urlService == nil {
dj.SendPrivateMessage(user, INVALID_URL_MSG)
} else {
oldLength := dj.queue.Len()
if err := urlService.NewRequest(user, url); err == nil {
dj.client.Self.Channel.Send(SONG_ADDED_HTML, false)
// Starts playing the new song if nothing else is playing
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
if err := dj.queue.CurrentSong().Download(); err == nil {
dj.queue.CurrentSong().Play()
} else {
dj.SendPrivateMessage(user, AUDIO_FAIL_MSG)
dj.queue.CurrentSong().Delete()
dj.queue.OnSongFinished()
}
}
} else {
dj.SendPrivateMessage(user, err.Error())
}
}
} }
} }
// skip performs !skip functionality. Adds a skip to the skippers slice for the current song, and then // skip performs !skip functionality. Adds a skip to the skippers slice for the current song, and then
// evaluates if a skip should be performed. Both skip and forceskip are implemented here. // evaluates if a skip should be performed. Both skip and forceskip are implemented here.
func skip(user *gumble.User, username string, admin, playlistSkip bool) { func skip(user *gumble.User, admin, playlistSkip bool) {
if dj.audioStream.IsPlaying() { if dj.audioStream.IsPlaying() {
if playlistSkip { if playlistSkip {
if dj.queue.CurrentSong().Playlist() != nil { if dj.queue.CurrentSong().Playlist() != nil {
if err := dj.queue.CurrentSong().Playlist().AddSkip(username); err == nil { if err := dj.queue.CurrentSong().Playlist().AddSkip(user.Name); err == nil {
submitterSkipped := false submitterSkipped := false
if admin { if admin {
dj.client.Self.Channel.Send(ADMIN_PLAYLIST_SKIP_MSG, false) dj.client.Self.Channel.Send(ADMIN_PLAYLIST_SKIP_MSG, false)
} else if dj.queue.CurrentSong().Submitter() == username { } else if dj.queue.CurrentSong().Submitter() == user.Name {
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SUBMITTER_SKIP_HTML, username), false) dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SUBMITTER_SKIP_HTML, user.Name), false)
submitterSkipped = true submitterSkipped = true
} else { } else {
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SKIP_ADDED_HTML, username), false) dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SKIP_ADDED_HTML, user.Name), false)
} }
if submitterSkipped || dj.queue.CurrentSong().Playlist().SkipReached(len(dj.client.Self.Channel.Users)) || admin { if submitterSkipped || dj.queue.CurrentSong().Playlist().SkipReached(len(dj.client.Self.Channel.Users)) || admin {
id := dj.queue.CurrentSong().Playlist().ID() id := dj.queue.CurrentSong().Playlist().ID()
@ -247,15 +218,15 @@ func skip(user *gumble.User, username string, admin, playlistSkip bool) {
dj.SendPrivateMessage(user, NO_PLAYLIST_PLAYING_MSG) dj.SendPrivateMessage(user, NO_PLAYLIST_PLAYING_MSG)
} }
} else { } else {
if err := dj.queue.CurrentSong().AddSkip(username); err == nil { if err := dj.queue.CurrentSong().AddSkip(user.Name); err == nil {
submitterSkipped := false submitterSkipped := false
if admin { if admin {
dj.client.Self.Channel.Send(ADMIN_SONG_SKIP_MSG, false) dj.client.Self.Channel.Send(ADMIN_SONG_SKIP_MSG, false)
} else if dj.queue.CurrentSong().Submitter() == username { } else if dj.queue.CurrentSong().Submitter() == user.Name {
dj.client.Self.Channel.Send(fmt.Sprintf(SUBMITTER_SKIP_HTML, username), false) dj.client.Self.Channel.Send(fmt.Sprintf(SUBMITTER_SKIP_HTML, user.Name), false)
submitterSkipped = true submitterSkipped = true
} else { } else {
dj.client.Self.Channel.Send(fmt.Sprintf(SKIP_ADDED_HTML, username), false) dj.client.Self.Channel.Send(fmt.Sprintf(SKIP_ADDED_HTML, user.Name), false)
} }
if submitterSkipped || dj.queue.CurrentSong().SkipReached(len(dj.client.Self.Channel.Users)) || admin { if submitterSkipped || dj.queue.CurrentSong().SkipReached(len(dj.client.Self.Channel.Users)) || admin {
if !(submitterSkipped || admin) { if !(submitterSkipped || admin) {
@ -280,7 +251,7 @@ func help(user *gumble.User) {
// volume performs !volume functionality. Checks input value against LowestVolume and HighestVolume from // volume performs !volume functionality. Checks input value against LowestVolume and HighestVolume from
// config to determine if the volume should be applied. If in the correct range, the new volume // config to determine if the volume should be applied. If in the correct range, the new volume
// is applied and is immediately in effect. // is applied and is immediately in effect.
func volume(user *gumble.User, username, value string) { func volume(user *gumble.User, value string) {
if value == "" { if value == "" {
dj.client.Self.Channel.Send(fmt.Sprintf(CUR_VOLUME_HTML, dj.audioStream.Volume), false) dj.client.Self.Channel.Send(fmt.Sprintf(CUR_VOLUME_HTML, dj.audioStream.Volume), false)
} else { } else {
@ -288,7 +259,7 @@ func volume(user *gumble.User, username, value string) {
newVolume := float32(parsedVolume) newVolume := float32(parsedVolume)
if newVolume >= dj.conf.Volume.LowestVolume && newVolume <= dj.conf.Volume.HighestVolume { if newVolume >= dj.conf.Volume.LowestVolume && newVolume <= dj.conf.Volume.HighestVolume {
dj.audioStream.Volume = newVolume dj.audioStream.Volume = newVolume
dj.client.Self.Channel.Send(fmt.Sprintf(VOLUME_SUCCESS_HTML, username, dj.audioStream.Volume), false) dj.client.Self.Channel.Send(fmt.Sprintf(VOLUME_SUCCESS_HTML, user.Name, dj.audioStream.Volume), false)
} else { } else {
dj.SendPrivateMessage(user, fmt.Sprintf(NOT_IN_VOLUME_RANGE_MSG, dj.conf.Volume.LowestVolume, dj.conf.Volume.HighestVolume)) dj.SendPrivateMessage(user, fmt.Sprintf(NOT_IN_VOLUME_RANGE_MSG, dj.conf.Volume.LowestVolume, dj.conf.Volume.HighestVolume))
} }

38
index.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>{{.User}} - mumbledj</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script type="text/javascript">
function api(type) {
return "http://{{.Site}}/" + type + "?token={{.Token}}";
}
function addURL() {
var url = $("#textbox");
$.ajax(api("add") + "&value=" + url.attr("value"));
url.attr("value", "");
}
function volume() {
var volume = $("#textbox");
$.ajax(api("volume") + "&value=" + volume.attr("value"));
volume.attr("value", "");
}
function skip(val) {
$.ajax(api("skip") + "&value=" + val);
}
</script>
</head>
<body>
<h1>Add Song Form</h1>
<input id="textbox" type="text"/>
<input id="add" type="button" value="Add Song" onclick="addURL()"/>
<input id="volume" type="button" value="Set Volume" onclick="volume()"/>
<input id="skipSong" type="button" value="Skip Current Song" onclick="skip('false')"/>
<input id="skipPlaylist" type="button" value="Skip Current Playlist" onclick="skip('true')"/>
</body>
</html>

25
main.go
View file

@ -14,6 +14,7 @@ import (
"os" "os"
"os/user" "os/user"
"reflect" "reflect"
"regexp"
"strings" "strings"
"time" "time"
@ -102,7 +103,7 @@ func (dj *mumbledj) OnTextMessage(e *gumble.TextMessageEvent) {
func (dj *mumbledj) OnUserChange(e *gumble.UserChangeEvent) { func (dj *mumbledj) OnUserChange(e *gumble.UserChangeEvent) {
if e.Type.Has(gumble.UserChangeDisconnected) { if e.Type.Has(gumble.UserChangeDisconnected) {
if dj.audioStream.IsPlaying() { if dj.audioStream.IsPlaying() {
if dj.queue.CurrentSong().Playlist() != nil { if !isNil(dj.queue.CurrentSong().Playlist()) {
dj.queue.CurrentSong().Playlist().RemoveSkip(e.User.Name) dj.queue.CurrentSong().Playlist().RemoveSkip(e.User.Name)
} }
dj.queue.CurrentSong().RemoveSkip(e.User.Name) dj.queue.CurrentSong().RemoveSkip(e.User.Name)
@ -143,7 +144,7 @@ func PerformStartupChecks() {
func Verbose(msg string) { func Verbose(msg string) {
if dj.verbose { if dj.verbose {
fmt.Printf(msg) fmt.Printf(msg + "\n")
} }
} }
@ -152,6 +153,17 @@ func isNil(a interface{}) bool {
return a == nil || reflect.ValueOf(a).IsNil() return a == nil || reflect.ValueOf(a).IsNil()
} }
func RegexpFromURL(url string, patterns []string) *regexp.Regexp {
for _, pattern := range patterns {
if re, err := regexp.Compile(pattern); err == nil {
if re.MatchString(url) {
return re
}
}
}
return nil
}
// dj variable declaration. This is done outside of main() to allow global use. // dj variable declaration. This is done outside of main() to allow global use.
var dj = mumbledj{ var dj = mumbledj{
keepAlive: make(chan bool), keepAlive: make(chan bool),
@ -160,6 +172,8 @@ var dj = mumbledj{
cache: NewSongCache(), cache: NewSongCache(),
} }
var web *WebServer
// main primarily performs startup tasks. Grabs and parses commandline // main primarily performs startup tasks. Grabs and parses commandline
// args, sets up the gumble client and its listeners, and then connects to the server. // args, sets up the gumble client and its listeners, and then connects to the server.
func main() { func main() {
@ -230,7 +244,12 @@ func main() {
os.Exit(1) os.Exit(1)
} }
Webserver() web = NewWebServer(9563)
web.makeWeb()
if isNil(web) {
Verbose("WEB IS NIL")
}
<-dj.keepAlive <-dj.keepAlive
} }

View file

@ -8,6 +8,9 @@
package main package main
import ( import (
"errors"
"fmt"
"github.com/layeh/gumble/gumble" "github.com/layeh/gumble/gumble"
) )
@ -15,7 +18,7 @@ import (
type Service interface { type Service interface {
ServiceName() string ServiceName() string
URLRegex(string) bool // Can service deal with URL URLRegex(string) bool // Can service deal with URL
NewRequest(*gumble.User, string) error // Create song/playlist and add to the queue NewRequest(*gumble.User, string) (string, error) // Create song/playlist and add to the queue
} }
// Song interface. Each service will implement these // Song interface. Each service will implement these
@ -50,3 +53,40 @@ type Playlist interface {
} }
var services = []Service{YouTube{}} var services = []Service{YouTube{}}
func findServiceAndAdd(user *gumble.User, url string) error {
var urlService Service
// Checks all services to see if any can take the URL
for _, service := range services {
if service.URLRegex(url) {
urlService = service
}
}
if urlService == nil {
return errors.New("INVALID_URL")
} else {
oldLength := dj.queue.Len()
var title string
var err error
if title, err = urlService.NewRequest(user, url); err == nil {
dj.client.Self.Channel.Send(fmt.Sprintf(SONG_ADDED_HTML, user.Name, title), false)
// Starts playing the new song if nothing else is playing
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
if err := dj.queue.CurrentSong().Download(); err == nil {
dj.queue.CurrentSong().Play()
} else {
dj.queue.CurrentSong().Delete()
dj.queue.OnSongFinished()
return errors.New("FAILED_TO_DOWNLOAD")
}
}
} else {
dj.SendPrivateMessage(user, err.Error())
}
return err
}
}

View file

@ -35,64 +35,13 @@ var youtubeVideoPatterns = []string{
`https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`, `https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
} }
// --------------- // ------
// YOUTUBE SERVICE // TYPES
// --------------- // ------
// YouTube implements the Service interface
type YouTube struct{} type YouTube struct{}
// Name of the service
func (y YouTube) ServiceName() string {
return "Youtube"
}
// Checks to see if service will accept URL
func (y YouTube) URLRegex(url string) bool {
return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil
}
func RegexpFromURL(url string, patterns []string) *regexp.Regexp {
for _, pattern := range patterns {
if re, err := regexp.Compile(pattern); err == nil {
if re.MatchString(url) {
return re
}
}
}
return nil
}
// Creates the requested song/playlist and adds to the queue
func (y YouTube) NewRequest(user *gumble.User, url string) error {
var shortURL, startOffset = "", ""
if re, err := regexp.Compile(youtubePlaylistPattern); err == nil {
if re.MatchString(url) {
if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
shortURL = re.FindStringSubmatch(url)[1]
_, err := NewYouTubePlaylist(user.Name, shortURL)
return err
} else {
return errors.New("NO_PLAYLIST_PERMISSION")
}
} else {
re = RegexpFromURL(url, youtubeVideoPatterns)
matches := re.FindAllStringSubmatch(url, -1)
shortURL = matches[0][1]
if len(matches[0]) == 3 {
startOffset = matches[0][2]
}
_, err := NewYouTubeSong(user.Name, shortURL, startOffset, nil)
return err
}
} else {
return err
}
}
// ------------
// YOUTUBE SONG
// ------------
// YouTubeSong holds the metadata for a song extracted from a YouTube video. // YouTubeSong holds the metadata for a song extracted from a YouTube video.
type YouTubeSong struct { type YouTubeSong struct {
submitter string submitter string
@ -107,14 +56,61 @@ type YouTubeSong struct {
dontSkip bool dontSkip bool
} }
// NewYouTubeSong gathers the metadata for a song extracted from a YouTube video, and returns // YouTubePlaylist holds the metadata for a YouTube playlist.
type YouTubePlaylist struct {
id string
title string
}
// ---------------
// YOUTUBE SERVICE
// ---------------
// Name of the service
func (yt YouTube) ServiceName() string {
return "Youtube"
}
// Checks to see if service will accept URL
func (yt YouTube) URLRegex(url string) bool {
return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil
}
// Creates the requested song/playlist and adds to the queue
func (yt YouTube) NewRequest(user *gumble.User, url string) (string, error) {
var shortURL, startOffset = "", ""
if re, err := regexp.Compile(youtubePlaylistPattern); err == nil {
if re.MatchString(url) {
if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
shortURL = re.FindStringSubmatch(url)[1]
playlist, err := yt.NewPlaylist(user.Name, shortURL)
return playlist.Title(), err
} else {
return "", errors.New("NO_PLAYLIST_PERMISSION")
}
} else {
re = RegexpFromURL(url, youtubeVideoPatterns)
matches := re.FindAllStringSubmatch(url, -1)
shortURL = matches[0][1]
if len(matches[0]) == 3 {
startOffset = matches[0][2]
}
song, err := yt.NewSong(user.Name, shortURL, startOffset, nil)
return song.Title(), err
}
} else {
return "", err
}
}
// NewSong gathers the metadata for a song extracted from a YouTube video, and returns
// the song. // the song.
func NewYouTubeSong(user, id, offset string, playlist *YouTubePlaylist) (*YouTubeSong, error) { func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (*YouTubeSong, error) {
var apiResponse *jsonq.JsonQuery var apiResponse *jsonq.JsonQuery
var err error var err error
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s", url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s",
id, os.Getenv("YOUTUBE_API_KEY")) id, os.Getenv("YOUTUBE_API_KEY"))
if apiResponse, err = PerformGetRequest(url); err != nil { if apiResponse, err = yt.PerformGetRequest(url); err != nil {
return nil, errors.New(INVALID_API_KEY) return nil, errors.New(INVALID_API_KEY)
} }
@ -196,29 +192,69 @@ func NewYouTubeSong(user, id, offset string, playlist *YouTubePlaylist) (*YouTub
dontSkip: false, dontSkip: false,
} }
dj.queue.AddSong(song) dj.queue.AddSong(song)
Verbose(song.Submitter() + " added track " + song.Title() + "\n") Verbose(song.Submitter() + " added track " + song.Title())
return song, nil return song, nil
} }
return nil, errors.New(VIDEO_TOO_LONG_MSG) return nil, errors.New(VIDEO_TOO_LONG_MSG)
} }
// NewPlaylist gathers the metadata for a YouTube playlist and returns it.
func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) {
var apiResponse *jsonq.JsonQuery
var err error
// Retrieve title of playlist
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s",
id, os.Getenv("YOUTUBE_API_KEY"))
if apiResponse, err = yt.PerformGetRequest(url); err != nil {
return nil, err
}
title, _ := apiResponse.String("items", "0", "snippet", "title")
playlist := &YouTubePlaylist{
id: id,
title: title,
}
// Retrieve items in playlist
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s",
id, os.Getenv("YOUTUBE_API_KEY"))
if apiResponse, err = yt.PerformGetRequest(url); err != nil {
return nil, err
}
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
if numVideos > 50 {
numVideos = 50
}
for i := 0; i < numVideos; i++ {
index := strconv.Itoa(i)
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
yt.NewSong(user, videoID, "", playlist)
}
return playlist, nil
}
// ------------
// YOUTUBE SONG
// ------------
// Download downloads the song via youtube-dl if it does not already exist on disk. // Download downloads the song via youtube-dl if it does not already exist on disk.
// All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned. // All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned.
func (s *YouTubeSong) Download() error { func (s *YouTubeSong) Download() error {
// Checks to see if song is already downloaded // Checks to see if song is already downloaded
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, s.Filename())); os.IsNotExist(err) { if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, s.Filename())); os.IsNotExist(err) {
Verbose("Downloading " + s.Title() + "\n") Verbose("Downloading " + s.Title())
cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s`, s.Filename()), "--format", "m4a", "--", s.ID()) cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s`, s.Filename()), "--format", "m4a", "--", s.ID())
if err := cmd.Run(); err == nil { if err := cmd.Run(); err == nil {
if dj.conf.Cache.Enabled { if dj.conf.Cache.Enabled {
dj.cache.CheckMaximumDirectorySize() dj.cache.CheckMaximumDirectorySize()
} }
Verbose(s.Title() + " downloaded\n") Verbose(s.Title() + " downloaded")
return nil return nil
} }
Verbose(s.Title() + " failed to download\n") Verbose(s.Title() + " failed to download")
return errors.New("Song download failed.") return errors.New("Song download failed.")
} }
return nil return nil
@ -271,7 +307,7 @@ func (s *YouTubeSong) Play() {
dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(), dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(),
s.Title(), s.Duration(), s.Submitter(), s.Playlist().Title()), false) s.Title(), s.Duration(), s.Submitter(), s.Playlist().Title()), false)
} }
Verbose("Now playing " + s.Title() + "\n") Verbose("Now playing " + s.Title())
go func() { go func() {
dj.audioStream.Wait() dj.audioStream.Wait()
@ -286,10 +322,10 @@ func (s *YouTubeSong) Delete() error {
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.ID()) filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.ID())
if _, err := os.Stat(filePath); err == nil { if _, err := os.Stat(filePath); err == nil {
if err := os.Remove(filePath); err == nil { if err := os.Remove(filePath); err == nil {
Verbose("Deleted " + s.Title() + "\n") Verbose("Deleted " + s.Title())
return nil return nil
} }
Verbose("Failed to delete " + s.Title() + "\n") Verbose("Failed to delete " + s.Title())
return errors.New("Error occurred while deleting audio file.") return errors.New("Error occurred while deleting audio file.")
} }
return nil return nil
@ -380,48 +416,6 @@ func (s *YouTubeSong) SetDontSkip(value bool) {
// YOUTUBE PLAYLIST // YOUTUBE PLAYLIST
// ---------------- // ----------------
// YouTubePlaylist holds the metadata for a YouTube playlist.
type YouTubePlaylist struct {
id string
title string
}
// NewYouTubePlaylist gathers the metadata for a YouTube playlist and returns it.
func NewYouTubePlaylist(user, id string) (*YouTubePlaylist, error) {
var apiResponse *jsonq.JsonQuery
var err error
// Retrieve title of playlist
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s",
id, os.Getenv("YOUTUBE_API_KEY"))
if apiResponse, err = PerformGetRequest(url); err != nil {
return nil, err
}
title, _ := apiResponse.String("items", "0", "snippet", "title")
playlist := &YouTubePlaylist{
id: id,
title: title,
}
// Retrieve items in playlist
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s",
id, os.Getenv("YOUTUBE_API_KEY"))
if apiResponse, err = PerformGetRequest(url); err != nil {
return nil, err
}
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
if numVideos > 50 {
numVideos = 50
}
for i := 0; i < numVideos; i++ {
index := strconv.Itoa(i)
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
NewYouTubeSong(user, videoID, "", playlist)
}
return playlist, nil
}
// AddSkip adds a skip to the playlist's skippers slice. // AddSkip adds a skip to the playlist's skippers slice.
func (p *YouTubePlaylist) AddSkip(username string) error { func (p *YouTubePlaylist) AddSkip(username string) error {
for _, user := range dj.playlistSkips[p.ID()] { for _, user := range dj.playlistSkips[p.ID()] {
@ -475,7 +469,7 @@ func (p *YouTubePlaylist) Title() string {
// ----------- // -----------
// PerformGetRequest does all the grunt work for a YouTube HTTPS GET request. // PerformGetRequest does all the grunt work for a YouTube HTTPS GET request.
func PerformGetRequest(url string) (*jsonq.JsonQuery, error) { func (yt YouTube) PerformGetRequest(url string) (*jsonq.JsonQuery, error) {
jsonString := "" jsonString := ""
if response, err := http.Get(url); err == nil { if response, err := http.Get(url); err == nil {

134
web.go
View file

@ -2,67 +2,123 @@ package main
import ( import (
"fmt" "fmt"
"html"
"html/template"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"os"
"path/filepath"
"strconv"
"strings" "strings"
"time" "time"
"github.com/layeh/gumble/gumble" "github.com/layeh/gumble/gumble"
) )
var client_token = make(map[string]string) type WebServer struct {
var token_client = make(map[string]string) port int
var external_ip = "" client_token map[*gumble.User]string
token_client map[string]*gumble.User
}
type Page struct { type Page struct {
Title string Site string
Body []byte Token string
User string
} }
func (p *Page) save() error { var external_ip = ""
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) { func NewWebServer(port int) *WebServer {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
func handler(w http.ResponseWriter, r *http.Request) {
var uname = token_client[r.URL.Path[1:]]
if uname == "" {
fmt.Fprintf(w, "I don't know you")
} else {
fmt.Fprintf(w, "Hi there, I love %s!", uname)
}
}
func Webserver() {
http.HandleFunc("/", handler)
http.ListenAndServe(":9563", nil)
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
return &WebServer{
port: port,
client_token: make(map[*gumble.User]string),
token_client: make(map[string]*gumble.User),
}
} }
func GetWebAddress(user *gumble.User) { func (web *WebServer) makeWeb() {
if client_token[user.Name] != "" { http.HandleFunc("/", web.homepage)
token_client[client_token[user.Name]] = "" http.HandleFunc("/add", web.add)
http.HandleFunc("/volume", web.volume)
http.HandleFunc("/skip", web.skip)
http.ListenAndServe(":"+strconv.Itoa(web.port), nil)
}
func (web *WebServer) homepage(w http.ResponseWriter, r *http.Request) {
var uname = web.token_client[r.URL.Path[1:]]
if uname == nil {
fmt.Fprintf(w, "Invalid Token")
} else {
cwd, _ := os.Getwd()
t, err := template.ParseFiles(filepath.Join(cwd, "./.mumbledj/web/index.html"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, Page{getIP() + ":" + strconv.Itoa(web.port), r.URL.Path[1:], uname.Name})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
func (web *WebServer) add(w http.ResponseWriter, r *http.Request) {
var uname = web.token_client[r.FormValue("token")]
if uname == nil {
fmt.Fprintf(w, "Invalid Token")
} else {
add(uname, html.UnescapeString(r.FormValue("value")))
fmt.Fprintf(w, "Success")
}
}
func (web *WebServer) volume(w http.ResponseWriter, r *http.Request) {
var uname = web.token_client[r.FormValue("token")]
if uname == nil {
fmt.Fprintf(w, "Invalid Token")
} else {
var vol = html.UnescapeString(r.FormValue("value"))
volume(uname, vol)
fmt.Fprintf(w, "Success")
}
}
func (web *WebServer) skip(w http.ResponseWriter, r *http.Request) {
var uname = web.token_client[r.FormValue("token")]
if uname == nil {
fmt.Fprintf(w, "Invalid Token")
} else {
value := html.UnescapeString(r.FormValue("value"))
playlist, err := strconv.ParseBool(value)
if err == nil {
skip(uname, false, playlist)
fmt.Fprintf(w, "Success")
} else {
fmt.Fprintf(w, "Invalid Value")
}
}
}
func (website *WebServer) GetWebAddress(user *gumble.User) {
Verbose("Port number: " + strconv.Itoa(web.port))
if web.client_token[user] != "" {
web.token_client[web.client_token[user]] = nil
} }
// dealing with collisions // dealing with collisions
var firstLoop = true var firstLoop = true
for firstLoop || token_client[client_token[user.Name]] != "" { for firstLoop || web.token_client[web.client_token[user]] != nil {
client_token[user.Name] = randSeq(10) web.client_token[user] = randSeq(10)
firstLoop = false firstLoop = false
} }
token_client[client_token[user.Name]] = user.Name web.token_client[web.client_token[user]] = user
dj.SendPrivateMessage(user, fmt.Sprintf(WEB_ADDRESS, getIP(), client_token[user.Name], getIP(), client_token[user.Name])) dj.SendPrivateMessage(user, fmt.Sprintf(WEB_ADDRESS, getIP(), web.client_token[user], getIP(), web.client_token[user]))
} }
// Gets the external ip address for the server
func getIP() string { func getIP() string {
if external_ip != "" { if external_ip != "" {
return external_ip return external_ip
@ -79,9 +135,9 @@ func getIP() string {
} }
} }
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") // Generates a pseudorandom string of characters
func randSeq(n int) string { func randSeq(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n) b := make([]rune, n)
for i := range b { for i := range b {
b[i] = letters[rand.Intn(len(letters))] b[i] = letters[rand.Intn(len(letters))]