Sync = require "sync" config = require("./config") getLogger = require("./logger") services = require("./services") sync = require "sync" request = require "request" fs = require("fs") path = require("path") qs = require "querystring" temp = require("temp").track() youtubedl = require "youtube-dl" isValidUrl = (require "valid-url").isWebUri log = getLogger "Main" # http://stackoverflow.com/a/7117336 removeBB = (str) -> str.replace /\[(\w+)[^\]]*](.*?)\[\/\1]/g, "$2" module.exports = shutdown: (cb) => ts3clientService = services.find("ts3client") if ts3clientService and ts3clientService.state == "started" await ts3clientService.stop defer(err) if err cb? new Error "Could not stop TeamSpeak3" return log.debug "Shutting down services..." await services.shutdown defer(err) if err cb? new Error "Error while shutting down rest of services." log.debug "Services shut down." cb?() shutdownSync: => Sync @shutdown # Separate our own PulseAudio from any system one by using our own custom XDG directories. process.env.XDG_RUNTIME_DIR = temp.mkdirSync "ts3bot-xdg" # Xvfb for isolated graphical interfaces! xvfbService = services.find("xvfb") await xvfbService.start defer err, vlc if err if not process.env.DISPLAY? or process.env.DISPLAY.trim() == "" log.error "Xvfb could not start up and no display is available!", err await module.exports.shutdown defer() process.exit 1 log.warn "Xvfb could not start up - will use existing display!", err # PulseAudio daemon await services.find("pulseaudio").start defer err if err log.warn "PulseAudio could not start up, audio may not act as expected!", err # VLC via WebChimera.js vlcService = services.find("vlc") await vlcService.start defer err, vlc if err log.warn "VLC could not start up!", err await module.exports.shutdown defer() process.exit 1 # This is where we keep track of the volume vlcVolume = 50 # Cached information for tracks in playlist vlcMediaInfo = {} # TeamSpeak3 ts3clientService = services.find("ts3client") ts3clientService.on "started", (ts3proc) => ts3query = ts3clientService.query ts3clientService.once "stopped", () => ts3query = undefined # VLC event handling vlc.onPlaying = () => try # TODO: Check why info is sometimes null, something must be wrong with the "add"/"play" commands here! # TODO: Do not format as URL in text message if MRL points to local file item = vlc.playlist.items[vlc.playlist.currentItem] info = vlcMediaInfo[item.mrl] url = info?.originalUrl or item.mrl title = info?.title or item.mrl ts3query?.sendtextmessage 2, 0, "Now playing [URL=#{url}]#{title}[/URL]." # Restore audio volume vlc.audio.volume = vlcVolume catch e log.warn "Error in VLC onPlaying handler", e vlc.onPaused = () => ts3query?.sendtextmessage 2, 0, "Paused." vlc.onForward = () => ts3query?.sendtextmessage 2, 0, "Fast-forwarding..." vlc.onBackward = () => ts3query?.sendtextmessage 2, 0, "Rewinding..." vlc.onEncounteredError = () => log.error "VLC has encountered an error! You will need to restart the bot.", arguments vlc.onStopped = () => ts3query?.sendtextmessage 2, 0, "Stopped." ts3query.currentScHandlerID = 1 ts3query.mydata = {} ts3query.on "open", => log.info "TS3 query now ready." attempts = 0 err = null init = true while init or err != null init = false if err attempts++ if attempts == 10 log.error "Could not register to TeamSpeak3 client events, giving up!" break else log.warn "Could not register to TeamSpeak3 client events!", err for eventName in [ "notifytalkstatuschange" "notifyconnectstatuschange" "notifytextmessage" "notifyclientupdated" "notifycliententerview" "notifyclientleftview" "notifyclientchatclosed" "notifyclientchatcomposing" "notifyclientchannelgroupchanged" "notifyclientmoved" ] await ts3query.clientnotifyregister ts3query.currentScHandlerID, eventName, defer(err) if err break ts3query.on "message.selected", (args) => if args["schandlerid"] ts3query.currentScHandlerID = parseInt args["schandlerid"] ts3query.on "message.notifytalkstatuschange", (args) => await ts3query.use args.schandlerid, defer(err, data) ts3query.on "message.notifyconnectstatuschange", (args) => await ts3query.use args.schandlerid, defer(err, data) if args.status == "disconnected" and ts3clientService.state != "stopping" log.warn "Disconnected from TeamSpeak server, reconnecting in a few seconds..." ts3clientService.stopSync() setTimeout (() => ts3clientService.restartSync()), 8000 if args.status == "connecting" log.info "Connecting to TeamSpeak server..." if args.status == "connection_established" log.info "Connected to TeamSpeak server." ts3query.on "message.notifyclientupdated", (args) => await ts3query.use args.schandlerid, defer(err, data) await ts3query.whoami defer(err, data) if not err ts3query.mydata = data ts3query.on "message.notifytextmessage", (args) => await ts3query.use args.schandlerid, defer(err, data) if not args.msg? return msg = args.msg invoker = { name: args.invokername, uid: args.invokeruid, id: args.invokerid } targetmode = args.targetmode # 1 = private, 2 = channel log.info "<#{invoker.name}> #{msg}" # cheap argument parsing here firstSpacePos = msg.indexOf " " if firstSpacePos == 0 return if firstSpacePos > 0 name = msg.substring 0, firstSpacePos paramline = msg.substring firstSpacePos + 1 params = paramline.match(/'[^']*'|"[^"]*"|[^ ]+/g) || []; else name = msg paramline = "" params = [] switch name.toLowerCase() when "current" item = vlc.playlist.items[vlc.playlist.currentItem] info = vlcMediaInfo[item.mrl] url = info?.originalUrl or item.mrl title = info?.title or item.mrl ts3query?.sendtextmessage 2, 0, "Currently playing [URL=#{url}]#{title}[/URL]." # Restore audio volume vlc.audio.volume = vlcVolume when "pause" # now we can toggle-pause playback this easily! yay! vlc.togglePause() return when "play" inputBB = paramline.trim() input = (removeBB paramline).trim() # we gonna interpret play without a url as an attempt to unpause the current song if input.length <= 0 vlc.play() return # only allow playback from file if it's a preconfigured alias if isValidUrl input log.debug "Got input URL:", input else input = config.get "aliases:#{input}" if not(isValidUrl input) and not(fs.existsSync input) log.debug "Got neither valid URL nor valid alias:", input ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to play #{inputBB} via the bot." return # TODO: permission system to check if uid is allowed to play this url or alias vlc.playlist.clear() # let's give youtube-dl a shot! await youtubedl.getInfo input, [ "--format=bestaudio" ], defer(err, info) if err or not info? log.debug "There is no audio-only download for #{inputBB}, downloading full video instead." await youtubedl.getInfo input, [ "--format=best" ], defer(err, info) if err or not info? info = url: input if not info.url? info.url = input info.title = input # URL as title info.originalUrl = input vlcMediaInfo[info.url] = info # play it in VLC vlc.play info.url when "stop-after" vlc.playlist.mode = vlc.playlist.Single ts3query.sendtextmessage args.targetmode, invoker.id, "Playback will stop after the current playlist item." when "loop" inputBB = paramline input = null switch (removeBB paramline).toLowerCase().trim() when "" # just show current mode ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is #{if vlc.playlist.mode == vlc.playlist.Loop then "on" else "off"}." when "on" # enable looping vlc.playlist.mode = vlc.playlist.Loop ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is now on." when "off" # disable looping vlc.playlist.mode = vlc.playlist.Normal ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is now off." else ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} on|off[/B] - Turns playlist looping on or off" return when "next" if vlc.playlist.items.count == 0 ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty." return if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem == vlc.playlist.items.count - 1 ts3query.sendtextmessage args.targetmode, invoker.id, "Can't jump to next playlist item, this is the last one!" return vlc.playlist.next() when "prev", "previous" if vlc.playlist.items.count == 0 ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty." return if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem <= 0 ts3query.sendtextmessage args.targetmode, invoker.id, "Can't jump to previous playlist item, this is the first one!" return vlc.playlist.prev() when "empty", "clear" vlc.playlist.clear() ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist." when "enqueue", "add", "append" inputBB = paramline.trim() input = (removeBB paramline).trim() if inputBB.length <= 0 ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} [/B] - Adds the specified URL to the current playlist" return # only allow playback from file if it's a preconfigured alias if isValidUrl input log.debug "Got input URL:", input else input = config.get "aliases:#{input}" if not(isValidUrl input) and not(fs.existsSync input) log.debug "Got neither valid URL nor valid alias:", input ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to play #{inputBB} via the bot." return # TODO: permission system to check if uid is allowed to play this url or alias # let's give youtube-dl a shot! await youtubedl.getInfo input, [ "--format=bestaudio" ], defer(err, info) if err or not info? log.debug "There is no audio-only download for #{inputBB}, downloading full video instead." await youtubedl.getInfo input, [ "--format=best" ], defer(err, info) if err or not info? info = url: input if not info.url? info.url = input info.title = input # URL as title info.originalUrl = input vlcMediaInfo[info.url] = info # add it in VLC vlc.playlist.add info.url ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist." # TODO: Do we need to make sure that vlc.playlist.mode is not set to "Single" here or is that handled automatically? when "stop" vlc.stop() when "vol" inputBB = paramline.trim() input = (removeBB paramline).trim() if inputBB.length <= 0 ts3query.sendtextmessage args.targetmode, invoker.id, "Volume is currently set to #{vlcVolume}%." return vol = parseInt input if paramline.trim().length <= 0 or isNaN(vol) or vol > 200 or vol < 0 ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol [/B] - takes a number between 0 (0%) and 200 (200%) to set the volume. 100% is 100. Defaults to 50 (50%) on startup." return vlc.audio.volume = vlcVolume = vol ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%." when "changenick" nick = paramline Sync () => try ts3query.clientupdate.sync ts3query, { client_nickname: nick } catch err log.warn "ChangeNick failed, error information:", err switch err.id when 513 then ts3query.sendtextmessage args.targetmode, invoker.id, "That nickname is already in use." when 1541 then ts3query.sendtextmessage args.targetmode, invoker.id, "That nickname is too short or too long." else ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out." await ts3clientService.start [ config.get("ts3-server") ], defer(err, ts3proc) if err log.error "TeamSpeak3 could not start, shutting down.", err await module.exports.shutdown defer() process.exit 1