2015-10-15 15:25:10 +00:00
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"
2015-11-02 11:45:27 +00:00
temp = require("temp").track()
2015-10-21 12:19:56 +00:00
youtubedl = require "youtube-dl"
isValidUrl = (require "valid-url").isWebUri
2015-10-15 15:25:10 +00:00
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
2015-11-02 11:45:27 +00:00
# Separate our own PulseAudio from any system one by using our own custom XDG directories.
process.env.XDG_RUNTIME_DIR = temp.mkdirSync "ts3bot-xdg"
2015-11-02 12:38:09 +00:00
# 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
2015-10-15 15:25:10 +00:00
# PulseAudio daemon
await services.find("pulseaudio").start defer err
if err
2015-11-02 12:26:49 +00:00
log.warn "PulseAudio could not start up, audio may not act as expected!", err
2015-10-15 15:25:10 +00:00
2015-10-27 00:14:55 +00:00
# VLC via WebChimera.js
vlcService = services.find("vlc")
await vlcService.start defer err, vlc
2015-10-21 12:19:56 +00:00
if err
2015-11-02 12:26:49 +00:00
log.warn "VLC could not start up!", err
2015-10-21 12:19:56 +00:00
await module.exports.shutdown defer()
process.exit 1
2015-10-27 00:14:55 +00:00
2015-11-03 00:28:30 +00:00
# This is where we keep track of the volume
vlcVolume = 50
2015-10-27 00:14:55 +00:00
# Cached information for tracks in playlist
vlcMediaInfo = {}
2015-10-21 12:19:56 +00:00
2015-10-15 15:25:10 +00:00
# TeamSpeak3
ts3clientService = services.find("ts3client")
ts3clientService.on "started", (ts3proc) =>
ts3query = ts3clientService.query
2015-11-03 02:59:35 +00:00
ts3clientService.once "stopped", () =>
ts3query = undefined
2015-10-27 00:14:55 +00:00
# VLC event handling
2015-10-27 21:40:39 +00:00
vlc.onPlaying = () =>
2015-11-05 02:02:03 +00:00
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
2015-11-03 00:28:30 +00:00
2015-11-03 02:59:35 +00:00
vlc.onPaused = () => ts3query?.sendtextmessage 2, 0, "Paused."
vlc.onForward = () => ts3query?.sendtextmessage 2, 0, "Fast-forwarding..."
vlc.onBackward = () => ts3query?.sendtextmessage 2, 0, "Rewinding..."
2015-10-27 21:40:39 +00:00
vlc.onEncounteredError = () => log.error "VLC has encountered an error! You will need to restart the bot.", arguments
2015-11-03 02:59:35 +00:00
vlc.onStopped = () => ts3query?.sendtextmessage 2, 0, "Stopped."
2015-10-27 00:14:55 +00:00
2015-10-15 15:25:10 +00:00
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)
2015-10-21 12:19:56 +00:00
2015-11-04 23:17:29 +00:00
if not args.msg?
return
2015-10-15 15:25:10 +00:00
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()
2015-10-21 13:37:58 +00:00
when "pause"
2015-10-27 00:14:55 +00:00
# now we can toggle-pause playback this easily! yay!
vlc.togglePause()
2015-10-21 13:37:58 +00:00
return
2015-10-15 15:25:10 +00:00
when "play"
2015-10-21 13:37:02 +00:00
inputBB = paramline.trim()
2015-10-21 18:58:36 +00:00
input = (removeBB paramline).trim()
2015-10-21 13:37:02 +00:00
# we gonna interpret play without a url as an attempt to unpause the current song
2015-10-21 18:58:36 +00:00
if input.length <= 0
2015-10-27 00:14:55 +00:00
vlc.play()
2015-10-21 13:37:02 +00:00
return
2015-10-21 12:19:56 +00:00
# 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
2015-10-27 00:14:55 +00:00
vlc.playlist.clear()
2015-10-21 12:19:56 +00:00
# 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
2015-10-27 00:14:55 +00:00
info.originalUrl = input
vlcMediaInfo[info.url] = info
2015-10-21 12:19:56 +00:00
2015-10-27 00:14:55 +00:00
# play it in VLC
vlc.play info.url
2015-10-27 22:54:36 +00:00
when "stop-after"
vlc.playlist.mode = vlc.playlist.Single
ts3query.sendtextmessage args.targetmode, invoker.id, "Playback will stop after the current playlist item."
2015-10-27 22:54:01 +00:00
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
2015-10-21 13:37:02 +00:00
when "next"
2015-10-27 23:16:10 +00:00
if vlc.playlist.items.count == 0
ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty."
return
2015-10-27 23:12:50 +00:00
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
2015-10-27 00:14:55 +00:00
vlc.playlist.next()
2015-10-27 21:59:17 +00:00
when "prev", "previous"
2015-10-27 23:16:10 +00:00
if vlc.playlist.items.count == 0
ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty."
return
2015-10-27 23:14:44 +00:00
if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem <= 0
2015-10-27 23:12:50 +00:00
ts3query.sendtextmessage args.targetmode, invoker.id, "Can't jump to previous playlist item, this is the first one!"
return
2015-10-27 21:59:17 +00:00
vlc.playlist.prev()
2015-10-27 22:01:23 +00:00
when "empty", "clear"
vlc.playlist.clear()
ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist."
2015-10-21 14:31:53 +00:00
when "enqueue", "add", "append"
2015-10-21 18:58:36 +00:00
inputBB = paramline.trim()
input = (removeBB paramline).trim()
2015-10-21 14:32:10 +00:00
if inputBB.length <= 0
2015-10-21 14:56:16 +00:00
ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} <url>[/B] - Adds the specified URL to the current playlist"
2015-10-21 14:32:10 +00:00
return
2015-10-21 13:37:02 +00:00
# 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
2015-10-27 00:14:55 +00:00
info.originalUrl = input
vlcMediaInfo[info.url] = info
2015-10-21 13:37:02 +00:00
2015-10-27 00:14:55 +00:00
# add it in VLC
vlc.playlist.add info.url
2015-10-21 14:32:58 +00:00
ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist."
2015-10-21 12:19:56 +00:00
2015-10-27 00:14:55 +00:00
# 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()
2015-10-21 13:13:53 +00:00
when "vol"
2015-10-29 01:23:53 +00:00
inputBB = paramline.trim()
input = (removeBB paramline).trim()
if inputBB.length <= 0
2015-11-03 00:28:30 +00:00
ts3query.sendtextmessage args.targetmode, invoker.id, "Volume is currently set to #{vlcVolume}%."
2015-10-29 01:23:53 +00:00
return
vol = parseInt input
2015-10-21 13:13:53 +00:00
2015-11-04 23:32:07 +00:00
if paramline.trim().length <= 0 or isNaN(vol) or vol > 200 or vol < 0
2015-10-27 00:14:55 +00:00
ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol <number>[/B] - takes a number between 0 (0%) and 200 (200%) to set the volume. 100% is 100. Defaults to 50 (50%) on startup."
2015-10-21 13:13:53 +00:00
return
2015-11-03 00:28:30 +00:00
vlc.audio.volume = vlcVolume = vol
2015-10-27 00:14:55 +00:00
ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%."
2015-10-15 15:25:10 +00:00
when "changenick"
2015-11-04 23:06:58 +00:00
nick = paramline
2015-10-15 15:25:10 +00:00
Sync () =>
try
ts3query.clientupdate.sync ts3query, { client_nickname: nick }
catch err
log.warn "ChangeNick failed, error information:", err
2015-11-04 23:07:25 +00:00
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."
2015-10-15 15:25:10 +00:00
await ts3clientService.start [ config.get("ts3-server") ], defer(err, ts3proc)
if err
2015-11-02 12:26:49 +00:00
log.error "TeamSpeak3 could not start, shutting down.", err
2015-10-15 15:25:10 +00:00
await module.exports.shutdown defer()
process.exit 1