From a0bded664ab5e66660eec72f5be1c2bd3691929f Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 15 Oct 2015 17:25:10 +0200 Subject: [PATCH 001/115] Initial commit. - TS3Bot is able to play any link that VLC supports via the "play" command, just add any URI as an argument to that command. - TS3Bot is able to stop the currently playing media via the "stop" command. - TS3Bot is able to change its own nickname via the "changenick" command. --- .gitignore | 1 + app.iced | 194 ++++++++++ app.js | 61 +++ config.iced | 38 ++ logger.iced | 50 +++ package.json | 43 +++ require_bin.iced | 18 + service_depcomparer.iced | 6 + service_template.iced | 154 ++++++++ services.iced | 70 ++++ services/api.iced | 95 +++++ services/blackbox.iced | 76 ++++ services/pulseaudio.iced | 103 ++++++ services/ts3client.iced | 264 +++++++++++++ services/vlc.iced | 87 +++++ services/xvfb.iced | 46 +++ sugar_property.iced | 13 + ts3query.iced | 781 +++++++++++++++++++++++++++++++++++++++ ts3settings.iced | 223 +++++++++++ x11.iced | 76 ++++ 20 files changed, 2399 insertions(+) create mode 100644 .gitignore create mode 100644 app.iced create mode 100644 app.js create mode 100644 config.iced create mode 100644 logger.iced create mode 100644 package.json create mode 100644 require_bin.iced create mode 100644 service_depcomparer.iced create mode 100644 service_template.iced create mode 100644 services.iced create mode 100644 services/api.iced create mode 100644 services/blackbox.iced create mode 100644 services/pulseaudio.iced create mode 100644 services/ts3client.iced create mode 100644 services/vlc.iced create mode 100644 services/xvfb.iced create mode 100644 sugar_property.iced create mode 100644 ts3query.iced create mode 100644 ts3settings.iced create mode 100644 x11.iced diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10c5b9a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +iced diff --git a/app.iced b/app.iced new file mode 100644 index 0000000..ec9f0a5 --- /dev/null +++ b/app.iced @@ -0,0 +1,194 @@ +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" + +log = getLogger "Main" + +# http://stackoverflow.com/a/7117336 +removeBB = (str) -> str.replace /\[(\w+)[^\]]*](.*?)\[\/\1]/g, "$2" + +module.exports = + shutdown: (cb) => + apiService = services.find("api") + if apiService and apiService.state == "started" + await apiService.stop defer(err) + if err + cb? new Error "Could not stop API" + return + + 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 + +# PulseAudio daemon +await services.find("pulseaudio").start defer err +if err + log.warn "PulseAudio could not start up, audio may not act as expected!" + +# TeamSpeak3 +ts3clientService = services.find("ts3client") + +ts3clientService.on "started", (ts3proc) => + ts3query = ts3clientService.query + + 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) + + 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 "play" + q = + uid: invoker.uid + input: removeBB paramline + await request "http://127.0.0.1:16444/play?#{qs.stringify q}", defer(err, response) + switch response.statusCode + when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing #{paramline}." + when 400 then ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with what you wrote. Maybe check the URL/sound name you provided?" + when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to play #{q.input} via the bot." + else + log.warn "API reported error", response.statusCode, err + ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." + when "stop" + q = + uid: invoker.uid + await request "http://127.0.0.1:16444/stop?#{qs.stringify q}", defer(err, response) + switch response.statusCode + when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback." + when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to do that." + else + log.warn "API reported error", response.statusCode, err + ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." + when "setvolume" + q = + uid: invoker.uid + volume: parseFloat paramline + await request "http://127.0.0.1:16444/setvolume?#{qs.stringify q}", defer(err, response) + switch response.statusCode + when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Set volume to #{q.volume}" + when 400 then ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with what you wrote. Maybe check the volume? It's supposed to be a floating-point number between 0 and 2." + when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to do that." + else + log.warn "API reported error", response.statusCode, err + ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." + when "changenick" + nick = if paramline.length > params[0].length then paramline else params[0] + if nick.length < 1 or nick.length > 32 + ts3query.sendtextmessage args.targetmode, invoker.id, "Invalid nickname." + return + Sync () => + try + ts3query.clientupdate.sync ts3query, { client_nickname: nick } + catch err + ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out." + log.warn "ChangeNick failed, error information:", err + +await ts3clientService.start [ config.get("ts3-server") ], defer(err, ts3proc) +if err + log.error "TeamSpeak3 could not start, shutting down." + await module.exports.shutdown defer() + process.exit 1 + +# HTTP API +await services.find("api").start defer err +if err + log.error "API could not start up, shutting down!" + await module.exports.shutdown defer() + process.exit 1 \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..0d5b6c1 --- /dev/null +++ b/app.js @@ -0,0 +1,61 @@ +require("iced-coffee-script/register"); +Sync = require("sync"); + +var services = require("./services"); + +var getLogger = require("./logger"); +var log = getLogger("app"); + +// compatibility with Windows for interrupt signal +if (process.platform === "win32") { + var rl = require("readline").createInterface({ + input: process.stdin, + output: process.stdout + }); + rl.on("SIGINT", function() { + return process.emit("SIGINT"); + }); +} + +app = require("./app.iced"); + +doShutdownAsync = function(cb) { + log.info("App shutdown starting..."); + app.shutdown(function() { + log.info("Services shutdown starting..."); + services.shutdown(function() { + if (cb && typeof cb === "function") + cb(); + }); + }); +}; + +process.on("exit", function(e) { + log.debug("Triggered exit", e); + app.shutdownSync(); +}); + +process.on("SIGTERM", function(e) { + log.debug("Caught SIGTERM signal"); + app.shutdown(); +}); + +process.on("SIGINT", function() { + log.debug("Caught SIGINT signal"); + app.shutdown(); +}); + +process.on("SIGHUP", function() { + log.debug("Caught SIGHUP signal"); + app.shutdown(); +}); + +process.on("SIGQUIT", function() { + log.debug("Caught SIGQUIT signal"); + app.shutdown(); +}); + +process.on("SIGABRT", function() { + log.debug("Caught SIGABRT signal"); + app.shutdown(); +}); diff --git a/config.iced b/config.iced new file mode 100644 index 0000000..d4bf2de --- /dev/null +++ b/config.iced @@ -0,0 +1,38 @@ +nconf = require "nconf" +path = require "path" +merge = require "merge" +pwgen = require "password-generator" + +console.log "Loading configuration..." + +# Build configuration object from input +nconf.env [ "NODE_ENV", "PULSE_BINARY" ] +nconf.argv() +nconf.file path.join(process.env["HOME"], ".ts3bot", "config.json") +nconf.defaults + # read http://stackoverflow.com/q/12252043 on why I'm using .trim here + "environment": process.env.NODE_ENV?.trim() or "development" + "log-path": "." + "vlc-host": "0.0.0.0" + "vlc-port": 8080 + "vlc-password": pwgen() + "nickname": "TS3Bot" + "ts3-install-path": path.resolve __dirname, "..", "ts3client" + "ts3-config-path": path.join process.env.HOME, ".ts3client" + "xvfb-resolution": "800x600x16" + "console-log-level": "info" + "file-log-level": "debug" + "PULSE_BINARY": "pulseaudio" + +# Validate configuration +if not nconf.get("ts3-server") + throw new Error "You need to provide a TeamSpeak3 server URL (starts with ts3server:// and can be generated from any TS3 client GUI)." + +console.log "Configuration loaded." + +if nconf.get "dump-config" + console.log nconf.get() + process.exit 0 + +module.exports = merge true, nconf, + isProduction: -> @get("environment").toUpperCase() == "PRODUCTION" \ No newline at end of file diff --git a/logger.iced b/logger.iced new file mode 100644 index 0000000..6c5003e --- /dev/null +++ b/logger.iced @@ -0,0 +1,50 @@ +winston = require "winston" +path = require "path" +config = require "./config" +merge = require "merge" +winstonCommon = require "winston/lib/winston/common" + +winston.emitErrs = true + +transports = [] + +# console logging +console.log "Minimal logging level for console is #{config.get("console-log-level")}" +transports.push new (winston.transports.Console) + colorize: not config.get("json") + silent: config.get("quiet") or config.get("silent") or false + json: config.get("json") or false + stringify: config.get("json") and config.get("json-stringify") or false + timestamp: config.get("timestamp") or false + debugStdout: not config.get("debug-stderr") + prettyPrint: not config.get("json") + level: config.get("console-log-level") + +# file logging +if not config.get("disable-file-logging") + transports.push new (winston.transports.File) + filename: path.join config.get("log-path"), "#{config.get("environment")}.log" + tailable: true + zippedArchive: config.get("zip-logs") or false + level: config.get("file-log-level") + if config.get("json") + transports.push new (winston.transports.File) + filename: path.join config.get("log-path"), "#{config.get("environment")}.json" + json: true + tailable: true + zippedArchive: config.get("zip-logs") or false + level: config.get("file-log-level") + +container = new (winston.Container) + transports: transports + +initialized_loggers = [] + +module.exports = (name, options) => + if not(name in initialized_loggers) + logger = container.add name + logger.addFilter (msg, meta, level) => "[#{name}] #{msg}" + initialized_loggers.push name + return logger + + container.get name \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e2c7e1 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "ts3bot", + "version": "0.2.0", + "description": "Allows running TeamSpeak3 as a bot for all kinds of media (local music/videos/streams/YouTube/...) without the need for a real GUI to exist.", + "main": "app.js", + "keywords": [ + "teamspeak", + "teamspeak3", + "ts3", + "bot", + "ts3bot", + "teamspeak3bot", + "music", + "playback", + "audio", + "video", + "media", + "musicbot" + ], + "author": "Carl Kittelberger ", + "license": "GPL-3.0+", + "dependencies": { + "express": "^4.13.3", + "iced-coffee-script": "^108.0.8", + "merge": "^1.2.0", + "mkdirp": "^0.5.1", + "nconf": "^0.7.2", + "npm-which": "^2.0.0", + "password-generator": "^2.0.1", + "querystring": "^0.2.0", + "request": "^2.61.0", + "simple-ini": "^1.0.3", + "sqlite3": "^3.1.0", + "stream-splitter": "^0.3.2", + "string.prototype.startswith": "^0.2.0", + "sync": "^0.2.5", + "valid-url": "^1.0.9", + "vlc-api": "0.0.0", + "which": "^1.1.2", + "winston": "^1.0.1", + "xvfb": "git://github.com/icedream/node-xvfb.git" + } +} diff --git a/require_bin.iced b/require_bin.iced new file mode 100644 index 0000000..66158f2 --- /dev/null +++ b/require_bin.iced @@ -0,0 +1,18 @@ +which = require("which").sync +path = require "path" +log = require("./logger")("RequireBin") + +module.exports = (binName) => + # check if xvfb is findable from here + if path.resolve(binName) == path.normalize(binName) + # this is an absolute path + return binName + + log.silly "Detecting #{binName}..." + try + binPath = which binName + log.debug "#{binName} detected:", binPath + return binPath + catch err + log.error "#{binName} could not be found.", err + throw new Error "#{binName} could not be found." diff --git a/service_depcomparer.iced b/service_depcomparer.iced new file mode 100644 index 0000000..18621a3 --- /dev/null +++ b/service_depcomparer.iced @@ -0,0 +1,6 @@ +module.exports = (a, b) -> + if a.dependencies.indexOf(b.name) >= 0 + return -1; # a before b + if b.dependencies.indexOf(a.name) >= 0 + return 1; # a after b + return 0; # does not matter \ No newline at end of file diff --git a/service_template.iced b/service_template.iced new file mode 100644 index 0000000..5f863d6 --- /dev/null +++ b/service_template.iced @@ -0,0 +1,154 @@ +Sync = require "sync" + +getLogger = require "./logger" +EventEmitter = require("events").EventEmitter +merge = require "merge" +services = require "./services" + +module.exports = class Service extends EventEmitter + constructor: (@name, funcs) -> + @log = getLogger @name + @_funcs = funcs + @_funcs.log = @log # for bind lovers and coffeescript fat arrow (=>) lovers + + @on "started", => @emit "_ready" + @on "stopped", => @emit "_ready" + + if not @dependencies + @dependencies = [] + + state: "stopped" + + start: () => @_start.apply @, [ false ].concat Array.prototype.slice.call(arguments) + + startSync: () => Sync () => @start.sync @ + + _start: (quiet, args...) => + if typeof quiet != "boolean" + throw new "quiet parameter must be a boolean" + + serviceArgs = args.slice 0 + cb = serviceArgs.pop() + if typeof cb != "function" + throw new Error "Callback needs to be given and needs to be a function" + + # wait until state is definite + if @state != "started" and @state != "stopped" + await @on "_ready", defer() + + if @state != "stopped" + @log.warn "Requested startup of #{@name} but it needs to be stopped, current state is #{@state}." + if @state == "started" + @_funcs.start.apply @, serviceArgs.concat [ cb ] # start should return service object and null-error to callback + else + cb? new Error "Invalid state" + return + + # make sure dependencies are running + dependencyServices = [] + for serviceName in @dependencies + service = services.find serviceName + if not service + @log.error "Could not find dependency #{serviceName}!" + cb? new Error "Dependency #{serviceName} not found" + return + dependencyServices.push service + dependencyServices.sort require("./service_depcomparer") # sort services by dependency + for service in dependencyServices + if service.state != "started" + await service.start defer err + if err + @log.error "Dependency #{service.name} failed, can't start #{@name}" + cb? new Error "Dependency #{service.name} failed" + return + + if not quiet + @log.info "Starting #{@name}" + + @state = "starting" + @emit "starting" + + await @_funcs.start.apply @, serviceArgs.concat [ defer(err, service) ] + if err + cb? err + @emit "startfail", err + @state = "stopped" + return + + if not quiet + @log.info "Started #{@name}" + + @_lastArgs = args + + @state = "started" + @emit "started", service + + cb? null, service + + stop: () => @_stop.apply @, [ false ].concat Array.prototype.slice.call(arguments) + + stopSync: () => Sync () => @stop.sync @ + + _stop: (quiet, args...) => + if typeof quiet != "boolean" + throw new "quiet parameter must be a boolean" + + serviceArgs = args.slice 0 + cb = serviceArgs.pop() + if typeof cb != "function" + throw new Error "Callback needs to be given and needs to be a function" + + # wait until state is definite + if @state != "started" and @state != "stopped" + await @on "_ready", defer() + + if @state != "started" + @log.warn "Requested shutdown of #{@name} but it needs to be started, current state is #{@state}." + cb? new Error "Invalid state" + return + + if not quiet + @log.info "Stopping #{@name}" + + @state = "stopping" + @emit "stopping" + + await @_funcs.stop.apply @, serviceArgs.concat [ defer(err, service) ] + if err + cb? err + @state = "started" + @emit "stopfail", err + return + + if not quiet + @log.info "Stopped #{@name}" + + @state = "stopped" + @emit "stopped" + + cb?() + + restart: (cb) => + # wait until state is definite + if @state != "started" and @state != "stopped" + await @on "_ready", defer() + + @log.info "Restarting #{@name}" + @emit "restarting" + + if @state == "started" + await @_stop true, defer(err) + if err + cb? err + + if @state == "stopped" + await @_start true, @_lastArgs..., defer(err) + if err + cb? err + + @log.info "Restarted #{@name}" + @emit "restarted" + + cb? err + + restartSync: () => Sync () => @restart.sync @ \ No newline at end of file diff --git a/services.iced b/services.iced new file mode 100644 index 0000000..917021b --- /dev/null +++ b/services.iced @@ -0,0 +1,70 @@ +# At this point I feel like I'm writing my own init system. Phew... + +merge = require "merge" +getLogger = require("./logger") +EventEmitter = require("events").EventEmitter +log = getLogger("ServiceMgr") +Sync = require "sync" + +getLegacyServiceName = (serviceName) -> serviceName.toLowerCase().replace(/[^A-z0-9]/g, "_") + +module.exports = + services: [] + + find: (serviceName) -> + serviceNameUpper = serviceName.toUpperCase() + for service in services + if service.name.toUpperCase() == serviceNameUpper + return service + null + + register: (service) -> + if @[service.name] + throw new Error "There is already a service registered under that name" + @services.push service + log.debug "Registered service #{service.name}" + + unregister: (serviceName) -> + for service, index in @services + if service.name == serviceName + @services.splice index, 1 + log.debug "Unregistered service #{service.name}" + throw new Error "There is no service registered under that name" + + shutdown: (cb) -> + shutdownOrder = @services.splice 0 + shutdownOrder.reverse() + + while true + for own k, v of shutdownOrder + if v.state == "stopped" + continue + await v.stop defer() + breakOut = true + for own k, v of shutdownOrder + if v.state != "stopped" + log.debug "Service #{k} in state #{v.state} after shutdown loop, relooping" + breakOut = false + if breakOut + break + cb?() + + shutdownSync: () -> Sync () => @shutdown.sync @ + +# base class for all services +module.exports.Service = require "./service_template" + +# register services +services = [ + new(require "./services/api") + new(require "./services/pulseaudio") + new(require "./services/ts3client") + new(require "./services/vlc") + new(require "./services/xvfb") + new(require "./services/blackbox") +] +services.sort require("./service_depcomparer") # sort services by dependency +for service in services + module.exports.register service + +module.exports.services = services \ No newline at end of file diff --git a/services/api.iced b/services/api.iced new file mode 100644 index 0000000..f92fd04 --- /dev/null +++ b/services/api.iced @@ -0,0 +1,95 @@ +express = require "express" +url = require "url" +path = require "path" +spawn = require("child_process").spawn +net = require "net" +Socket = net.Socket +getLogger = require "../logger" +config = require "../config" +log = getLogger "API" +#PulseAudio = require "pulseaudio" +isValidUrl = (require "valid-url").isWebUri + +services = require "../services" + +module.exports = class APIService extends services.Service + dependencies: [ + "pulseaudio" + "vlc" + "ts3client" + ] + constructor: () -> super "API", + start: (cb) -> + if @httpServer + cb? null + return + + vlc = services.find("vlc").instance + ts3query = services.find("ts3client").query + + # set up HTTP server + log.debug "Starting up HTTP API..." + app = express() + app.get "/play", (req, res) => + if not req.query.uid + log.debug "Didn't get a UID, sending forbidden" + res.status(400).send("Forbidden") + return + if not req.query.input + log.debug "Didn't get an input URI/alias, sending bad request" + res.status(400).send("Bad request") + return + + input = null + # only allow playback from file if it's a preconfigured alias + if isValidUrl req.query.input + log.debug "Got input URL:", req.query.input + input = req.query.input + else + input = config.get("aliases:#{req.query.input}") + if not(isValidUrl input) and not(fs.existsSync input) + log.debug "Got neither valid URL nor valid alias:", req.query.input + res.status(403).send("Forbidden") + return + + # TODO: permission system to check if uid is allowed to play this url or alias + + await vlc.status.empty defer(err) + if err + res.status(503).send("Something went wrong") + log.warn "VLC API returned an error when trying to empty", err + return + + await vlc.status.play input, defer(err) + if err + vlc.status.empty() + res.status(503).send("Something went wrong") + log.warn "VLC API returned an error when trying to play", err + return + + res.send("OK") + + app.get "/stop", (req, res) => + if not req.query.uid + log.debug "Didn't get a UID, sending forbidden" + res.status(403).send("Forbidden - missing UID") + return + + # TODO: permission system to check if uid is allowed to stop playback + + vlc.status.stop() + vlc.status.empty() + + res.send("OK") + + app.get "/setvolume", (req, res) => + throw new "Not implemented yet" # FIXME below, still need to implement audio + + @httpServer = app.listen 16444 + + cb? null + + stop: (cb) -> + @httpServer.close() + + cb?() \ No newline at end of file diff --git a/services/blackbox.iced b/services/blackbox.iced new file mode 100644 index 0000000..e73fdb4 --- /dev/null +++ b/services/blackbox.iced @@ -0,0 +1,76 @@ +spawn = require("child_process").spawn +log = require("../logger")("BlackBox") +services = require("../services") +StreamSplitter = require("stream-splitter") +require_bin = require("../require_bin") + +blackboxBinPath = require_bin "blackbox" + +module.exports = class BlackBoxService extends services.Service + dependencies: [ + "xvfb" + ] + constructor: -> super "BlackBox", + start: (cb) -> + if @process + cb? null, @process + return + + calledCallback = false + + proc = null + doStart = null + doStart = () => + await services.find("xvfb").start defer(err) + if err + throw new Error "Dependency xvfb failed." + + proc = spawn blackboxBinPath, [ "-rc", "/dev/null" ], + stdio: ['ignore', 'pipe', 'pipe'] + detached: true + env: + DISPLAY: process.env.DISPLAY + XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR + HOME: process.env.HOME + + # logging + stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n" + stdoutTokenizer.encoding = "utf8"; + stdoutTokenizer.on "token", (token) => + token = token.trim() # get rid of \r + @log.debug token + + stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" + stderrTokenizer.encoding = "utf8"; + stderrTokenizer.on "token", (token) => + token = token.trim() # get rid of \r + @log.warn token + + proc.on "exit", () => + if @state == "stopping" + return + if not calledCallback + calledCallback = true + @log.warn "BlackBox terminated unexpectedly during startup." + cb? new Error "BlackBox terminated unexpectedly." + @log.warn "BlackBox terminated unexpectedly, restarting." + doStart() + + @process = proc + + doStart() + + setTimeout (() => + if not calledCallback + calledCallback = true + cb? null, @process), 1500 # TODO: Use some more stable condition + + stop: (cb) -> + if not @process + cb?() + return + + @process.kill() + await @process.once "exit", defer() + + cb?() diff --git a/services/pulseaudio.iced b/services/pulseaudio.iced new file mode 100644 index 0000000..fc10f9b --- /dev/null +++ b/services/pulseaudio.iced @@ -0,0 +1,103 @@ +spawn = require("child_process").spawn +log = require("../logger")("PulseAudio") +services = require("../services") +config = require("../config") +StreamSplitter = require("stream-splitter") +require_bin = require("../require_bin") + +pulseaudioPath = require_bin config.get("PULSE_BINARY") +pacmdPath = require_bin "pacmd" + +module.exports = class PulseAudioService extends services.Service + dependencies: [ + "xvfb" + ] + constructor: -> super "PulseAudio", + start: (cb) -> + if @process + cb? null, @process + return + + # logging + forwardLog = (token) => + token = token.trim() # get rid of \r + level = token.substring(0, 1).toUpperCase() + msg = token.substring token.indexOf("]") + 2 + switch token.substring(0, 1).toUpperCase() + when "D" then log.silly msg + when "I" then log.silly msg + when "W" then log.warn msg + when "E" then log.error msg + else log.silly msg + + # spawn options + opts = + stdio: ['ignore', 'pipe', 'pipe'] + detached: true + env: + DISPLAY: process.env.DISPLAY + HOME: process.env.HOME + XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR + + # check if there is already a daemon running + proc = spawn pulseaudioPath, [ "--check" ], opts + stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" + stderrTokenizer.encoding = "utf8"; + stderrTokenizer.on "token", forwardLog + await proc.once "exit", defer(code, signal) + @log.silly "PulseAudio daemon check returned that #{if code == 0 then "a daemon is already running" else "no daemon is running"}" + if code == 0 + @log.warn "PulseAudio already running on this system" + cb? null, null + return + + proc = spawn pulseaudioPath, [ + "--start" + "--fail=true" # quit on startup failure + "--high-priority=true" + "--daemonize=false" + "-v" + ], opts + + calledCallback = false + + # logging + tokenHandler = (token) => + forwardLog token + + if not calledCallback and (token.indexOf("client.conf") >= 0 or token.indexOf("Daemon startup complete.") >= 0) + calledCallback = true + @process = proc + setTimeout (() => cb? null, @process), 1500 # TODO: Use some more stable condition + stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n" + stdoutTokenizer.encoding = "utf8" + stdoutTokenizer.on "token", tokenHandler + stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" + stderrTokenizer.encoding = "utf8" + stderrTokenizer.on "token", tokenHandler + + proc.on "exit", () => + if not calledCallback + calledCallback = true + cb? new Error "PulseAudio daemon terminated unexpectedly." + + stop: (cb) -> + if not @process + cb?() + return + + @process.kill() + await @process.once "exit", defer() + + cb?() + + findIndexForProcessId: (pid, cb) => throw new Error "Not implemented yet" + + findIndexForProcessIdSync: (pid) => Sync () => @findIndexForProcessId @, pid + + setSinkInputMute: (index, value, cb) => throw new Error "Not implemented yet" + + setSinkInputMuteSync: (index, value) => Sync () => @setSinkInputMute @, index, value + + mute: (index) => @setSinkInputMute index, 1 + unmute: (index) => @setSinkInputMute index, 0 diff --git a/services/ts3client.iced b/services/ts3client.iced new file mode 100644 index 0000000..996e6d5 --- /dev/null +++ b/services/ts3client.iced @@ -0,0 +1,264 @@ +xvfb = require("xvfb") +log = require("../logger")("TS3Client") +config = require("../config") +services = require("../services") +x11tools = require("../x11") +TS3Settings = require("../ts3settings") +TS3ClientQuery = require("../ts3query") +path = require "path" +merge = require "merge" +spawn = require("child_process").spawn +StreamSplitter = require("stream-splitter") +require_bin = require("../require_bin") + +ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3client_linux_" + (if process.arch == "x64" then "amd64" else process.arch)) + +module.exports = class TS3ClientService extends services.Service + dependencies: [ + "xvfb", + "blackbox", + "pulseaudio" + ] + constructor: -> super "TS3Client", + start: (args, cb) => + if typeof args == "function" + cb = args + args = null + + if @process + cb? null, @process + return + + if not args + args = [] + + await @_preconfigure defer() + + # spawn process + proc = null + doStart = null + forwardLog = (token) => + token = token.trim() # get rid of \r + if token.indexOf("|") > 0 + token = token.split("|") + level = token[1].toUpperCase().trim() + source = token[2].trim() + sourceStr = if source then "#{source}: " else "" + message = token[4].trim() + switch level + when "ERROR" then log.error "%s%s", sourceStr, message + when "WARN" then log.warn "%s%s", sourceStr, message + when "INFO" then log.debug "%s%s", sourceStr, message + else log.silly "%s%s", sourceStr, message + else + log.debug token + scheduleRestart = null # see below + onExit = => # autorestart + @_running = false + @process = null + if @_requestedExit + return + log.warn "TeamSpeak3 unexpectedly terminated!" + scheduleRestart() + doStart = () => + env = + HOME: process.env.HOME + DISPLAY: process.env.DISPLAY + XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR + KDEDIRS: '' + KDEDIR: '' + QTDIR: config.get("ts3-install-path") + QT_PLUGIN_PATH: config.get("ts3-install-path") + LD_LIBRARY_PATH: config.get("ts3-install-path") + if process.env.LD_LIBRARY_PATH + env.LD_LIBRARY_PATH += ":#{process.env.LD_LIBRARY_PATH}" + + @log.silly "Environment variables:", env + @log.silly "Arguments:", JSON.stringify args + + @_requestedExit = false + proc = spawn ts3client_binpath, args, + detached: true + stdio: ['ignore', 'pipe', 'pipe'] + cwd: config.get("ts3-install-path") + env: env + @_running = true + + # logging + stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n" + stdoutTokenizer.encoding = "utf8"; + stdoutTokenizer.on "token", forwardLog + + stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" + stderrTokenizer.encoding = "utf8"; + stderrTokenizer.on "token", forwardLog + + # connect to client query plugin when it's loaded + stdoutTokenizer.on "token", (token) => + if token.indexOf("Loading plugin: libclientquery_plugin") >= 0 + # client query plugin is now loading + @_queryReconnectTimer = setTimeout @query.connect.bind(@query), 250 + stderrTokenizer.on "token", (token) => + if token.indexOf("Query: bind failed") >= 0 + # without query this ts3 instance is worthless + await @_ts3GracefulShutdown defer() + scheduleRestart() + + # autorestart + proc.on "exit", onExit + + @process = proc + scheduleRestart = () => + log.warn "Restarting in 5 seconds..." + @_startTimer = setTimeout doStart.bind(@), 5000 + if @_queryReconnectTimer + clearTimeout @_queryReconnectTimer + doStart() + + # ts3 query + @query = new TS3ClientQuery "127.0.0.1", 25639 + @_queryReconnectTimer = null + @query.on "error", (err) => + if not @_requestedExit and @process and @process.connected + log.warn "Error in TS3 query connection, reconnecting..." + @_queryReconnectTimer = setTimeout @query.connect.bind(@query), 1000 + else + log.warn "TS3 query connection terminated." + @query.on "close", => log.debug "Connection to TS3 client query interface lost." + @query.on "open", => log.debug "Connected to TS3 client query interface." + @query.on "connecting", => log.debug "Connecting to TS3 client query interface..." + + cb? null, @process + + stop: (cb) -> @_ts3GracefulShutdown cb + + _ts3GracefulShutdown: (cb) -> + @_requestedExit = true + + if @_startTimer + clearTimeout @_startTimer + + if @_queryReconnectTimer + clearTimeout @_queryReconnectTimer + + if @_running + log.silly "Using xdotool to gracefully shut down TS3" + await x11tools.getWindowIdByProcessId @process.pid, defer(err, wid) + if not wid + log.debug "Can not find a window for #{@name}." + log.warn "Can not properly shut down #{@name}, it will time out on the server instead." + @process.kill() + else + log.silly "Sending keys to TS3" + await x11tools.sendKeys wid, "ctrl+q", defer(err) + if err + log.warn "Can not properly shut down #{@name}, it will time out on the server instead." + log.silly "Using SIGTERM for shutdown of #{@name}" + @process.kill() + + # wait for 10 seconds then SIGKILL if still up + log.silly "Now waiting 10 seconds for shutdown..." + killTimer = setTimeout (() => + log.silly "10 seconds gone, using SIGKILL now since we're impatient." + @process.kill("SIGKILL")), 10000 + await @process.once "exit", defer() + clearTimeout killTimer + + @_running = false + @process = null + else + log.warn "TeamSpeak3 seems to have died prematurely." + + cb?() + + _preconfigure: (cb) => + ts3settings = new TS3Settings config.get("ts3-config-path") + await ts3settings.open defer() + + # Delete bookmars to prevent auto-connect bookmarks from weirding out the client + await ts3settings.query "delete * from Bookmarks", defer() + + # Let's make sure we have an identity! + force = ts3settings.getIdentitiesSize() <= 0 or config.get("identity-path") + if force + if not config.get("identity-path") + throw new Error "Need a file to import the bot's identity from." + ts3settings.clearIdentities() + await ts3settings.importIdentity config.get("identity-path"), defer(identity) + identity.select() + + if config.get("nickname") + # Enforce nickname from configuration + identity = ts3settings.getSelectedIdentity() + identity.nickname = config.get "nickname" + identity.update() + + # Some settings to help the TS3Bot to do what it's supposed to do + await ts3settings.setMultiple [ + [ "Application", "HotkeyMode", "2" ] + [ "Chat", "MaxLines", "1" ] + [ "Chat", "LogChannelChats", "0" ] + [ "Chat", "LogClientChats", "0" ] + [ "Chat", "LogServerChats", "0" ] + [ "Chat", "ReloadChannelChats", "0" ] + [ "Chat", "ReloadClientChats", "0" ] + [ "Chat", "IndicateChannelChats", "0" ] + [ "Chat", "IndicatePrivateChats", "0" ] + [ "Chat", "IndicateServerChats", "0" ] + [ "FileTransfer", "SimultaneousDownloads", "2" ] + [ "FileTransfer", "SimultaneousUploads", "2" ] + [ "FileTransfer", "UploadBandwidth", "0" ] + [ "FileTransfer", "DownloadBandwidth", "0" ] + [ "General", "LastShownLicense", "1" ] # ugh... + [ "General", "LastShownLicenseLang", "C" ] + [ "Global", "MainWindowMaximized", "1" ] + [ "Global", "MainWindowMaximizedScreen", "1" ] + [ "Messages", "Disconnect", config.get "quit-message" ] + [ "Misc", "WarnWhenMutedInfoShown", "1" ] + [ "Misc", "LastShownNewsBrowserVersion", "4" ] + [ "News", "NewsClosed", "1" ] + [ "News", "Language", "en" ] + [ "Plugins", "teamspeak_control_plugin", "false" ] + [ "Plugins", "clientquery_plugin", "true" ] + [ "Plugins", "lua_plugin", "false" ] + [ "Plugins", "test_plugin", "false" ] + [ "Plugins", "ts3g15", "false" ] + [ "Profiles", "DefaultPlaybackProfile", "Default" ] + [ "Profiles", "Playback/Default", { + Device: '' + DeviceDisplayName: "Default" + VolumeModifier: -40 + VolumeFactorWaveDb: -17 + PlayMicClicksOnOwn: false + PlayMicClicksOnOthers: false + MonoSoundExpansion: 2 + Mode: "PulseAudio" + PlaybackMonoOverCenterSpeaker: false + } ] + [ "Profiles", "DefaultCaptureProfile", "Default" ] + [ "Profiles", "Capture/Default", { + Device: '' + DeviceDisplayName: "Default" + Mode: "PulseAudio" + } ] + [ "Profiles", "Capture/Default/PreProcessing", { + continous_transmission: "false" + vad: "true" + vad_over_ptt: "false" + delay_ptt_msecs: "250" + voiceactivation_level: "-49" + echo_reduction: false + echo_cancellation: false + denoise: false + delay_ptt: false + agc: if config.get("ts3-agc") then "true" else "false" + echo_reduction_db: 10 + } ] + [ "Notifications", "SoundPack", "nosounds" ] + [ "Statistics", "ParticipateStatistics", "0" ] + [ "Statistics", "ConfirmedParticipation", "1" ] + ], defer() + + await ts3settings.close defer() + + cb?() diff --git a/services/vlc.iced b/services/vlc.iced new file mode 100644 index 0000000..71f18d4 --- /dev/null +++ b/services/vlc.iced @@ -0,0 +1,87 @@ +spawn = require("child_process").spawn +services = require("../services") +config = require("../config") +VLCApi = require("vlc-api") +StreamSplitter = require("stream-splitter") +require_bin = require("../require_bin") + +vlcBinPath = require_bin "vlc" + +module.exports = class VLCService extends services.Service + dependencies: [ + "pulseaudio" + ] + constructor: -> super "VLC", + start: (cb) -> + if @_process + cb? null, @_process + return + + calledCallback = false + + proc = null + doStart = null + doStart = () => + await services.find("pulseaudio").start defer(err) + if err + throw new Error "Dependency pulseaudio failed." + + proc = spawn vlcBinPath, [ + "-I", "http", + "--http-host", config.get("vlc-host"), + "--http-port", config.get("vlc-port"), + "--http-password", config.get("vlc-password") + "--aout=pulse", + "--no-video" + ], + stdio: ['ignore', 'pipe', 'pipe'] + detached: true + + # logging + stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n" + stdoutTokenizer.encoding = "utf8"; + stdoutTokenizer.on "token", (token) => + token = token.trim() # get rid of \r + @log.debug token + + stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" + stderrTokenizer.encoding = "utf8"; + stderrTokenizer.on "token", (token) => + token = token.trim() # get rid of \r + @log.debug token + + proc.on "exit", () => + if @state == "stopping" + return + if not calledCallback + calledCallback = true + @log.warn "VLC terminated unexpectedly during startup." + cb? new Error "VLC terminated unexpectedly." + @log.warn "VLC terminated unexpectedly, restarting." + doStart() + + @_process = proc + + doStart() + + setTimeout (() => + if not calledCallback + calledCallback = true + + @instance = new VLCApi + host: ":#{encodeURIComponent config.get("vlc-password")}@#{config.get("vlc-host")}", + port: config.get("vlc-port") + cb? null, @instance), 1500 # TODO: Use some more stable condition + + stop: (cb) -> + if not @_process + cb?() + return + + @instance = null + + @_process.kill() + await @_process.once "exit", defer() + + cb?() + diff --git a/services/xvfb.iced b/services/xvfb.iced new file mode 100644 index 0000000..b3f1027 --- /dev/null +++ b/services/xvfb.iced @@ -0,0 +1,46 @@ +Xvfb = require("xvfb") +log = require("../logger")("Xvfb") +config = require("../config") +services = require("../services") +require_bin = require("../require_bin") + +require_bin "Xvfb" + +module.exports = class XvfbService extends services.Service + constructor: -> super "Xvfb", + start: (cb) -> + if @instance + cb? null, @instance + return + + instance = new Xvfb + detached: true + reuse: true + silent: false + timeout: 5000 + xvfb_args: [ + "-screen" + "0" + config.get("xvfb-resolution") + "-ac" + ] + await instance.start defer(err) + + if err + cb? err, null + + @instance = instance + + cb? null, @instance + + stop: (cb) -> + if not @instance + cb?() + return + + await @instance.stop defer(err) + if err + cb? err, null + + @instance = null + cb?() diff --git a/sugar_property.iced b/sugar_property.iced new file mode 100644 index 0000000..d4790f3 --- /dev/null +++ b/sugar_property.iced @@ -0,0 +1,13 @@ +# @property "prop", { desc... } +Function::property = (prop, desc) -> Object.defineProperty @prototype, prop, desc + +# defineProperty "prop", { desc... } +#Object::defineProperty = (prop, desc) -> Object.defineProperty @, prop, desc + +# propertiesof obj +#global.propertiesof = (obj) -> Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertyNames(obj.constructor.prototype or {})) + +# descriptorof obj, name +#global.descriptorof = (obj, name) -> Object.getOwnPropertyDescriptor obj, name + +module.exports = exports = {} \ No newline at end of file diff --git a/ts3query.iced b/ts3query.iced new file mode 100644 index 0000000..dafbfb4 --- /dev/null +++ b/ts3query.iced @@ -0,0 +1,781 @@ +require "string.prototype.startswith" + +net = require "net" +getLogger = require "./logger" +StringDecoder = require("string_decoder").StringDecoder +StreamSplitter = require "stream-splitter" +events = require "events" +EventEmitter = events.EventEmitter +merge = require "merge" + +parserLog = getLogger "parser" + +escape = (value) => value.toString().replace(///\\///g, "\\\\").replace(/\//g, "\\/").replace(/\|/g, "\\|").replace(///\ ///g, "\\s") + +unescape = (value) => value.toString().replace(/\\s/g, " ").replace(/\\\//g, "/").replace(/\\\|/g, "|").replace(/\\\\/g, "\\") + +buildCmd = (name, namedArgs, posArgs) => + # TODO: Add support for collected arguments (aka lists) + if not name + throw new Error "Need command name" + if name.indexOf(" ") >= 0 + throw new Error "Invalid command name" + if name.length > 0 + param = "#{name}" + if namedArgs + for k, v of namedArgs + if v == null + continue + if param.length > 0 + param += " " + param += "#{escape(k)}=#{escape(v)}" + if posArgs + for v in posArgs + if v == null + continue + if param.length > 0 + param += " " + param += "#{escape(v)}" + param + "\n\r" + +parseCmd = (str) => + params = str.split " " + + startIndex = 0 + if params[0].indexOf("=") < 0 + name = params[0] + str = str.substring name.length + 1 + + str = str.split "|" # TODO: Ignore escaped pipes + + collectedArgs = [] + + for s in str + params = s.split " " + args = {} + posArgs = [] + + for i in [0 .. params.length - 1] + value = params[i] + equalsPos = value.indexOf "=" + if equalsPos < 0 + posArgs.push value + continue + key = unescape value.substring(0, equalsPos) + value = value.substring equalsPos + 1 + value = unescape value + args[key] = value + + args._ = posArgs + + collectedArgs.push args + + if collectedArgs.length == 1 + collectedArgs = collectedArgs[0] + + { + name: name + args: collectedArgs + } + +checkError = (err) => + err.id = parseInt err.id + if err.id == 0 + return null + err + +module.exports = class TS3ClientQuery extends EventEmitter + constructor: (host, port) -> + @_log = getLogger "TS3ClientQuery" + @_id = null + @_host = host + @_port = port + + connect: (cb) => + @_tcpClient = new net.Socket + + @_tcpClient.on "close", () => @emit "close" + @_tcpClient.on "error", (err) => + @_log.warn "Connection error", err + @_tcpClient = null + @emit "error", err + + @emit "connecting" + @_tcpClient.connect @_port, @_host, () => + @emit "open" + await @once "message.selected", defer(selectedArgs) + cb?() + + splitterStream = StreamSplitter("\n\r") + splitterStream.encoding = "utf8" + + @_tcpTokenizer = @_tcpClient.pipe splitterStream + + @_tcpTokenizer.on "token", (token) => + token = token.trim() + + if token.startsWith "TS3 Client" or token.startsWith "Welcome" + return # this is just helper text for normal users + + response = parseCmd token + + @_log.silly "Recv:", response + + if response.name + @emit "message.#{response.name}", response.args + else + @emit "vars", response.args + + close: (cb) => + if not @_tcpClient + cb?() + return + @_tcpClient.destroy() + cb?() + + send: (cmd, namedArgs, positionalArgs, cb) => + if not cmd + throw new Error "Need command name" + + if Array.isArray namedArgs + cb = positionalArgs + positionalArgs = namedArgs + namedArgs = {} + + if typeof positionalArgs == "function" + cb = positionalArgs + positionalArgs = [] + + text = buildCmd(cmd, namedArgs, positionalArgs) + + @_log.silly "Send:", { cmd: cmd, namedArgs: namedArgs, positionalArgs: positionalArgs } + + @_tcpClient.write text, "utf8", () => cb?() + + banadd: (cb) => + throw new Error "Not implemented yet" + + banclient: (cb) => + throw new Error "Not implemented yet" + + bandel: (cb) => + throw new Error "Not implemented yet" + + bandelall: (cb) => + throw new Error "Not implemented yet" + + banlist: (cb) => + throw new Error "Not implemented yet" + + channeladdperm: (cb) => + throw new Error "Not implemented yet" + + channelclientaddperm: (cb) => + throw new Error "Not implemented yet" + + channelclientdelperm: (cb) => + throw new Error "Not implemented yet" + + channelclientlist: (cb) => + throw new Error "Not implemented yet" + + channelclientpermlist: (cb) => + throw new Error "Not implemented yet" + + ### + Get channel connection information for specified channelid from the currently + selected server connection handler. If no channelid is provided, information + for the current channel will be received. + ### + channelconnectinfo: (cid, cb) => + if not cb and typeof cid == "function" + cb = cid + cid = null + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "channelconnectinfo", + cid: cid + + ### + Creates a new channel using the given properties and displays its ID. + + N.B. The channel_password property needs a hashed password as a value. + The hash is a sha1 hash of the password, encoded in base64. You can + use the "hashpassword" command to get the correct value. + ### + channelcreate: (channel_name, channel_properties, cb) => + if not cb and typeof channel_properties == "function" + cb = channel_properties + channel_properties = {} + if not channel_properties + channel_properties = {} + channel_properties["channel_name"] = channel_name + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "channelcreate", channel_properties + + channeldelete: (cb) => + throw new Error "Not implemented yet" + + channeldelperm: (cb) => + throw new Error "Not implemented yet" + + ### + Changes a channels configuration using given properties. + ### + channeledit: (cid, channel_properties, cb) => + @once "message.error", (args) => cb? checkError(args) + @send "channeledit", merge true, channel_properties, + cid: cid + + channelgroupadd: (cb) => + throw new Error "Not implemented yet" + + channelgroupaddperm: (cb) => + throw new Error "Not implemented yet" + + channelgroupclientlist: (cb) => + throw new Error "Not implemented yet" + + channelgroupdel: (cb) => + throw new Error "Not implemented yet" + + channelgroupdelperm: (cb) => + throw new Error "Not implemented yet" + + channelgrouplist: (cb) => + throw new Error "Not implemented yet" + + channelgrouppermlist: (cb) => + throw new Error "Not implemented yet" + + channellist: (cb) => + throw new Error "Not implemented yet" + + channelmove: (cb) => + throw new Error "Not implemented yet" + + channelpermlist: (cb) => + throw new Error "Not implemented yet" + + channelvariable: (cb) => + throw new Error "Not implemented yet" + + clientaddperm: (cb) => + throw new Error "Not implemented yet" + + clientdbdelete: (cb) => + throw new Error "Not implemented yet" + + clientdbedit: (cb) => + throw new Error "Not implemented yet" + + clientdblist: (cb) => + throw new Error "Not implemented yet" + + clientdelperm: (cb) => + throw new Error "Not implemented yet" + + ### + Displays the database ID matching the unique identifier specified by cluid. + ### + clientgetdbidfromuid: (cluid, cb) => + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "clientgetdbidfromuid", + cluid: cluid + + ### + Displays all client IDs matching the unique identifier specified by cluid. + ### + clientgetids: (cb) => + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "clientgetids", + cluid: cluid + + ### + Displays the unique identifier and nickname matching the database ID specified + by cldbid. + ### + clientgetnamefromdbid: (cldbid, cb) => + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "clientgetnamefromdbid", + cldbid: cldbid + + ### + Displays the database ID and nickname matching the unique identifier specified + by cluid. + ### + clientgetnamefromuid: (cluid, cb) => + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "clientgetnamefromuid", + cluid: cluid + + ### + Displays the unique identifier and nickname associated with the client + specified by the clid parameter. + ### + clientgetuidfromclid: (clid, cb) => + retval = { } + @once "notifyclientuidfromclid", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "clientgetuidfromclid", + clid: clid + + ### + Kicks one or more clients specified with clid from their currently joined + channel or from the server, depending on reasonid. The reasonmsg parameter + specifies a text message sent to the kicked clients. This parameter is optional + and may only have a maximum of 40 characters. + + Available reasonid values are: + 4: Kick the client from his current channel into the default channel + 5: Kick the client from the server + ### + clientkick: (reasonid, reasonmsg, clid, cb) => + if not cb and not clid + cb = clid + clid = reasonmsg + reasonmsg = null + if typeof clid == "function" + cb = clid + clid = null + @once "message.error", (args) => cb? checkError(args), retval + @send "clientkick", + reasonid: reasonid + reasonmsg: reasonmsg + clid: clid + + ### + Displays a list of clients that are known. Included information is the + clientID, nickname, client database id, channelID and client type. + Please take note that the output will only contain clients which are in + channels you are currently subscribed to. Using the optional modifier + parameters you can enable additional information per client. + + Here is a list of the additional display paramters you will receive for + each of the possible modifier parameters. + + -uid: + client_unique_identifier + + -away: + client_away + client_away_message + + -voice: + client_flag_talking + client_input_muted + client_output_muted + client_input_hardware + client_output_hardware + client_talk_power + client_is_talker + client_is_priority_speaker + client_is_recording + client_is_channel_commander + client_is_muted + + -groups: + client_servergroups + client_channel_group_id + + -icon: + client_icon_id + + -country: + client_country + ### + clientlist: (modifiers, cb) => + if not cb + cb = modifiers + modifiers = null + + cleanedModifiers = [] + for v, index in modifiers + if not v.startsWith "-" + v = "-#{v}" + cleanedModifiers.push v + + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "clientlist", cleanedModifiers + + clientmove: (cb) => + throw new Error "Not implemented yet" + + clientmute: (cb) => + throw new Error "Not implemented yet" + + ### + This command allows you to listen to events that the client encounters. Events + are things like people starting or stopping to talk, people joining or leaving, + new channels being created and many more. + It registers for client notifications for the specified + serverConnectionHandlerID. If the serverConnectionHandlerID is set to zero it + applies to all server connection handlers. Possible event values are listed + below, additionally the special string "any" can be used to subscribe to all + events. + + Possible values for event: + notifytalkstatuschange + notifymessage + notifymessagelist + notifycomplainlist + notifybanlist + notifyclientmoved + notifyclientleftview + notifycliententerview + notifyclientpoke + notifyclientchatclosed + notifyclientchatcomposing + notifyclientupdated + notifyclientids + notifyclientdbidfromuid + notifyclientnamefromuid + notifyclientnamefromdbid + notifyclientuidfromclid + notifyconnectioninfo + notifychannelcreated + notifychanneledited + notifychanneldeleted + notifychannelmoved + notifyserveredited + notifyserverupdated + channellist + channellistfinished + notifytextmessage + notifycurrentserverconnectionchanged + notifyconnectstatuschange + ### + clientnotifyregister: (schandlerid, event, cb) => + @once "message.error", (args) => cb? checkError(args) + @send "clientnotifyregister", + schandlerid: schandlerid + event: event + + ### + Unregisters from all previously registered client notifications. + ### + clientnotifyunregister: (cb) => + @once "message.error", (args) => cb? checkError(args) + @send "clientnotifyunregister" + + ### + Displays a list of permissions defined for a client. + ### + clientpermlist: (cldbid, cb) => + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "clientpermlist", + cldbid: cldbid + + ### + Sends a poke message to the client specified with clid. + ### + clientpoke: (clid, msg, cb) => + if typeof msg == "function" + cb = msg + msg = null + @once "message.error", (args) => cb? checkError(args) + @send "clientpoke", + msg: msg + clid: clid + + clientunmute: (cb) => + throw new Error "Not implemented yet" + + ### + Sets one or more values concerning your own client, and makes them available + to other clients through the server where applicable. Available idents are: + + client_nickname: set a new nickname + client_away: 0 or 1, set us away or back available + client_away_message: what away message to display when away + client_input_muted: 0 or 1, mutes or unmutes microphone + client_output_muted: 0 or 1, mutes or unmutes speakers/headphones + client_input_deactivated: 0 or 1, same as input_muted, but invisible to + other clients + client_is_channel_commander: 0 or 1, sets or removes channel commander + client_nickname_phonetic: set your phonetic nickname + client_flag_avatar: set your avatar + client_meta_data: any string that is passed to all clients that + have vision of you. + client_default_token: privilege key to be used for the next server + connect + ### + clientupdate: (idents, cb) => + @once "message.error", (args) => cb? checkError(args) + @send "clientupdate", idents + + ### + Retrieves client variables from the client (no network usage). For each client + you can specify one or more properties that should be queried, and this whole + block of clientID and properties can be repeated to get information about + multiple clients with one call of clientvariable. + + Available properties are: + client_unique_identifier + client_nickname + client_input_muted + client_output_muted + client_outputonly_muted + client_input_hardware + client_output_hardware + client_meta_data + client_is_recording + client_database_id + client_channel_group_id + client_servergroups + client_away + client_away_message + client_type + client_flag_avatar + client_talk_power + client_talk_request + client_talk_request_msg + client_description + client_is_talker + client_is_priority_speaker + client_unread_messages + client_nickname_phonetic + client_needed_serverquery_view_power + client_icon_id + client_is_channel_commander + client_country + client_channel_group_inherited_channel_id + client_flag_talking + client_is_muted + client_volume_modificator + + These properties are always available for yourself, but need to be requested + for other clients. Currently you cannot request these variables via + clientquery: + client_version + client_platform + client_login_name + client_created + client_lastconnected + client_totalconnections + client_month_bytes_uploaded + client_month_bytes_downloaded + client_total_bytes_uploaded + client_total_bytes_downloaded + + These properties are available only for yourself: + client_input_deactivated + ### + clientvariable: (clid, variables, cb) => + if not clid + throw new Error "Need client ID" + if not Array.isArray variables + throw new Error "variables needs to be an array of requested client variables." + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "clientvariable", { clid: clid }, variables + + complainadd: (cb) => + throw new Error "Not implemented yet" + + complaindel: (cb) => + throw new Error "Not implemented yet" + + complaindelall: (cb) => + throw new Error "Not implemented yet" + + complainlist: (cb) => + throw new Error "Not implemented yet" + + ### + Get server connection handler ID of current server tab. + ### + currentschandlerid: (cb) => + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "currentschandlerid" + + disconnect: (cb) => close(cb) + + exam: (cb) => + throw new Error "Not implemented yet" + + ftcreatedir: (cb) => + throw new Error "Not implemented yet" + + ftdeletefile: (cb) => + throw new Error "Not implemented yet" + + ftgetfileinfo: (cb) => + throw new Error "Not implemented yet" + + ftgetfilelist: (cb) => + throw new Error "Not implemented yet" + + ftinitdownload: (cb) => + throw new Error "Not implemented yet" + + ftinitupload: (cb) => + throw new Error "Not implemented yet" + + ftlist: (cb) => + throw new Error "Not implemented yet" + + ftrenamefile: (cb) => + throw new Error "Not implemented yet" + + ftstop: (cb) => + throw new Error "Not implemented yet" + + hashpassword: (cb) => + throw new Error "Not implemented yet" + + help: (cb) => + throw new Error "Not implemented yet" + + messageadd: (cb) => + throw new Error "Not implemented yet" + + messagedel: (cb) => + throw new Error "Not implemented yet" + + messageget: (cb) => + throw new Error "Not implemented yet" + + messagelist: (cb) => + throw new Error "Not implemented yet" + + messageupdateflag: (cb) => + throw new Error "Not implemented yet" + + permoverview: (cb) => + throw new Error "Not implemented yet" + + quit: (cb) => close(cb) + + ### + Sends a text message a specified target. The type of the target is determined + by targetmode. + Available targetmodes are: + 1: Send private text message to a client. You must specify the target parameter + 2: Send message to the channel you are currently in. Target is ignored. + 3: Send message to the entire server. Target is ignored. + ### + sendtextmessage: (targetmode, target, msg, cb) => + if targetmode != 1 and (not msg or typeof msg == "function") + cb = msg + msg = target + target = null + @once "message.error", (args) => cb? checkError(args) + @send "sendtextmessage", + targetmode: targetmode + target: target + msg: msg + + serverconnectinfo: (cb) => + throw new Error "Not implemented yet" + + serverconnectionhandlerlist: (cb) => + throw new Error "Not implemented yet" + + servergroupadd: (cb) => + throw new Error "Not implemented yet" + + servergroupaddclient: (cb) => + throw new Error "Not implemented yet" + + servergroupaddperm: (cb) => + throw new Error "Not implemented yet" + + servergroupclientlist: (cb) => + throw new Error "Not implemented yet" + + servergroupdel: (cb) => + throw new Error "Not implemented yet" + + servergroupdelclient: (cb) => + throw new Error "Not implemented yet" + + servergroupdelperm: (cb) => + throw new Error "Not implemented yet" + + servergrouplist: (cb) => + throw new Error "Not implemented yet" + + servergrouppermlist: (cb) => + throw new Error "Not implemented yet" + + servergroupsbyclientid: (cb) => + throw new Error "Not implemented yet" + + servervariable: (cb) => + throw new Error "Not implemented yet" + + setclientchannelgroup: (cb) => + throw new Error "Not implemented yet" + + tokenadd: (cb) => + throw new Error "Not implemented yet" + + tokendelete: (cb) => + throw new Error "Not implemented yet" + + tokenlist: (cb) => + throw new Error "Not implemented yet" + + ### + Use a token key gain access to a server or channel group. Please note that the + server will automatically delete the token after it has been used. + ### + tokenuse: (token, cb) => + @once "message.error", (args) => cb? checkError(args) + @send "tokenuse", + token: token + + ### + Selects the server connection handler scHandlerID or, if no parameter is given, + the currently active server connection handler is selected. + ### + use: (schandlerid, cb) => + retval = { } + @once "message.selected", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "use", + schandlerid: schandlerid + + verifychannelpassword: (cb) => + throw new Error "Not implemented yet" + + ### + Verifies the server password and will return an error if the password is + incorrect. + ### + verifyserverpassword: (password, cb) => + @once "message.error", (args) => cb? checkError(args) + @send "verifyserverpassword", + password: password + + ### + Retrieves information about ourself: + - ClientID (if connected) + - ChannelID of the channel we are in (if connected) + + If not connected, an error is returned. + ### + whoami: (cb) => + retval = { } + @once "vars", (args) => merge retval, args + @once "message.error", (args) => cb? checkError(args), retval + @send "whoami" diff --git a/ts3settings.iced b/ts3settings.iced new file mode 100644 index 0000000..c673aac --- /dev/null +++ b/ts3settings.iced @@ -0,0 +1,223 @@ +sqlite3 = require("sqlite3") #.verbose() +SQLite3Database = sqlite3.Database +path = require "path" +mkdirp = require "mkdirp" +SimpleIni = require "simple-ini" +fs = require "fs" +merge = require "merge" +getLogger = require "./logger" + +# some properties sugar from http://bl.ocks.org/joyrexus/65cb3780a24ecd50f6df +Function::getter = (prop, get) -> + Object.defineProperty @prototype, prop, {get, configurable: yes} +Function::setter = (prop, set) -> + Object.defineProperty @prototype, prop, {set, configurable: yes} + +module.exports = class SettingsFile + db: null + identities: null + defaultIdentity: null + + constructor: (@configPath) -> + @log = getLogger "TS3Settings" + + try + mkdirp.sync @configPath + catch err + throw new Error "Could not create TS3 config directory." + + @getter "isInitialized", -> () => fs.existsSync(path.join(@configPath, "settings.db")) and fs.existsSync(path.join(@configPath, "ts3clientui_qt.secrets.conf")) + @getter "isReady", -> () => @db != null + + open: (cb) => + # settings database + @db = new SQLite3Database path.join(@configPath, "settings.db") + await @db.serialize defer() + await @query "CREATE TABLE IF NOT EXISTS TS3Tables (key varchar NOT NULL UNIQUE,timestamp integer unsigned NOT NULL)", defer() + + # secrets file + @identities = [] + @defaultIdentity = null + secretsPath = path.join(@configPath, "ts3clientui_qt.secrets.conf") + if fs.existsSync(secretsPath) + secrets = new SimpleIni (() => fs.readFileSync(secretsPath, "utf-8")), + quotedValues: false + for i in [1 .. secrets.Identities.size] + @identities.push + id: secrets.Identities["#{i}/id"] + identity: secrets.Identities["#{i}/identity"] + nickname: secrets.Identities["#{i}/nickname"] + @defaultIdentity = secrets.Identities.SelectedIdentity + cb?() + + close: (cb) => + if not @isReady + @log.warn "Tried to close TS3 settings when already closed" + return + + await @db.close defer() + + # Build secrets INI structure + secrets = new SimpleIni null, + quotedValues: false + secrets.General = {} + secrets.Bookmarks = + size: 0 + secrets.Identities = + size: @identities.length + index = 1 + for identity in @identities + for key, value of identity + secrets.Identities["#{index}/#{key}"] = value + index++ + if @defaultIdentity + secrets.Identities.SelectedIdentity = @defaultIdentity + + # Generate INI content + await secrets.save defer(iniText) + fs.writeFileSync path.join(@configPath, "ts3clientui_qt.secrets.conf"), iniText + + @identities = null + @defaultIdentity = null + @db = null + + cb?() + + setMultiple: (sets, cb) => + for set in sets + await @set set[0], set[1], set[2], defer(err) + if err + throw err + cb?() + + set: (table, key, value, cb) => + if not @isReady + throw new Error "You need to run open on this instance of TS3Settings first" + return + + if not table + throw new Error "Need table" + + await @query "create table if not exists #{table} (timestamp integer unsigned NOT NULL, key varchar NOT NULL UNIQUE, value varchar)", defer() + + if not key + return + + if not (typeof value == "string" || value instanceof String) + # serialize from object to ts3 dict text + strval = "" + for own k of value + strval += k + "=" + value[k] + "\n" + value = strval + + timestamp = Math.round (new Date).getTime() / 1000 + + stmt = @db.prepare "insert or replace into TS3Tables (key, timestamp) values (?, ?)" + stmt.run table, timestamp + await stmt.finalize defer() + + stmt = @db.prepare "insert or replace into #{table} (timestamp, key, value) values (?, ?, ?)" + stmt.run timestamp, key, value + await stmt.finalize defer() + + cb?() + + query: (stmt, cb) => + if not @isReady + throw new Error "You need to run open on this instance of TS3Settings first" + return + + await @db.run stmt, defer() + cb?() + + importIdentity: (identityFilePath, cb) => + if not @isReady + throw new Error "You need to run open on this instance of TS3Settings first" + return + + if not identityFilePath + throw new Error "Need identity file path" + + @log.info "Importing identity from #{identityFilePath}..." + + # open identity file + idFile = new SimpleIni (() => fs.readFileSync(identityFilePath, "utf-8")), + quotedValues: true + importedIdentity = {} + for own k, v of idFile.Identity + importedIdentity[k] = v + + for identity in @identities + if identity.id == importedIdentity.id + throw new Error "Identity with same ID already exists" + + @identities.push importedIdentity + @log.info "Identity #{importedIdentity.id} imported successfully!" + + cb? @constructIdentityObject importedIdentity + + importIdentitySync: (identityFilePath) => + await @importIdentity identityFilePath, defer retval + return retval + + getIdentities: (cb) => + if not @isReady + throw new Error "You need to run open on this instance of TS3Settings first" + return + + identities = [] + + for identity in @identities + identities.push @constructIdentityObject identity + + cb? identities + + getIdentitiesSync: () => + await @getIdentities defer retval + return retval + + getIdentitiesSize: () => + if not @isReady + throw new Error "You need to run open on this instance of TS3Settings first" + return + + @identities.length + + getSelectedIdentity: () => + if not @isReady + throw new Error "You need to run open on this instance of TS3Settings first" + return + + if not @defaultIdentity + return null + + for own index, identity of @identities + if identity.id == @defaultIdentity + return @constructIdentityObject identity + + clearIdentities: () => + if not @isReady + throw new Error "You need to run open on this instance of TS3Settings first" + return + + @log.debug "Clearing all identities" + @identities.length = 0 + return + + constructIdentityObject: (id) => + settingsObj = @ + clonedId = merge(true, id) + return merge clonedId, # true causes object to be cloned + select: () -> + settingsObj.defaultIdentity = @id + update: () -> + for own index, identity of settingsObj.identities + if identity.id == id.id + settingsObj.identities[index] = merge identity, id + return + remove: () -> + for own index, identity of settingsObj.identities + if identity.id == id.id + delete settingsObj.identities[index] + break + # TODO: Select another identity as default \ No newline at end of file diff --git a/x11.iced b/x11.iced new file mode 100644 index 0000000..c679dca --- /dev/null +++ b/x11.iced @@ -0,0 +1,76 @@ +Sync = require "sync" + +log = require("./logger")("X11tools") +spawn = require("child_process").spawn +services = require("./services") +StreamSplitter = require("stream-splitter") +require_bin = require("./require_bin") + +xdotoolBinPath = require_bin "xdotool" + +# Just some tools to work with the X11 windows +module.exports = + getWindowIdByProcessId: (pid, cb) => + wid = null + + # We provide --name due to the bug mentioned at https://github.com/jordansissel/xdotool/issues/14 + xdoproc = spawn xdotoolBinPath, [ "search", "--any", "--pid", pid, "--name", "xdosearch" ], + env: + DISPLAY: process.env.DISPLAY + XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR + stdoutTokenizer = xdoproc.stdout.pipe StreamSplitter "\n" + stdoutTokenizer.encoding = "utf8"; + stdoutTokenizer.on "token", (token) => + token = token.trim() # get rid of \r + newWid = parseInt(token) + if newWid != 0 and wid == null + wid = newWid + stderrTokenizer = xdoproc.stderr.pipe StreamSplitter "\n" + stderrTokenizer.encoding = "utf8"; + stderrTokenizer.on "token", (token) => + token = token.trim() # get rid of \r + log.warn token + await xdoproc.on "exit", defer(e) + + if e.code + log.error "Failed to find window ID, error code #{e.code}" + err = new Error "Failed to find window ID." + cb? err + return + + cb? null, parseInt(wid) + + getWindowIdByProcessIdSync: (pid) => Sync() => @getWindowIdByProcessId.sync @, pid + + sendKeys: (wid, keys, cb) => + # blackbox needs to be running for windowactivate to work + blackboxService = services.find("BlackBox") + if blackboxService.state != "started" + await blackboxService.start defer(err) + if err + cb? new Error "Could not start compatible window manager." + return + + xdoproc = spawn xdotoolBinPath, [ "windowactivate", "--sync", wid, "key", "--clearmodifiers", "--delay", "100" ].concat(keys), + env: + DISPLAY: process.env.DISPLAY + XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR + stdoutTokenizer = xdoproc.stdout.pipe StreamSplitter "\n" + stdoutTokenizer.encoding = "utf8"; + stdoutTokenizer.on "token", (token) => + token = token.trim() # get rid of \r + log.debug token + stderrTokenizer = xdoproc.stderr.pipe StreamSplitter "\n" + stderrTokenizer.encoding = "utf8"; + stderrTokenizer.on "token", (token) => + token = token.trim() # get rid of \r + log.warn token + await xdoproc.on "exit", defer(e) + + err = null + if e.code + log.error "Failed to send keys, error code #{e.code}" + err = new Error "Failed to send keys." + cb? err + + sendKeysSync: (keys) => Sync () => @sendKeys.sync @, keys \ No newline at end of file From 6ed562e27917a9913012aad3f681f66f47785d01 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 08:21:55 +0200 Subject: [PATCH 002/115] Try a synchronized shutdown when an uncaught exception occurs. --- app.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app.js b/app.js index 0d5b6c1..2451a8b 100644 --- a/app.js +++ b/app.js @@ -30,6 +30,11 @@ doShutdownAsync = function(cb) { }); }; +process.on("uncaughtException", function(err) { + log.fatal("Shutting down due to an uncaught exception!", err); + app.shutdownSync(); +}); + process.on("exit", function(e) { log.debug("Triggered exit", e); app.shutdownSync(); From cb42c3afd2373d87a70e18799547617f920e081b Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 08:22:20 +0200 Subject: [PATCH 003/115] Check for access to TeamSpeak3 binary on startup. --- services/ts3client.iced | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/ts3client.iced b/services/ts3client.iced index 996e6d5..9f94a66 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -7,11 +7,17 @@ TS3Settings = require("../ts3settings") TS3ClientQuery = require("../ts3query") path = require "path" merge = require "merge" +fs = require "fs" spawn = require("child_process").spawn StreamSplitter = require("stream-splitter") require_bin = require("../require_bin") ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3client_linux_" + (if process.arch == "x64" then "amd64" else process.arch)) +if not path.exists + await fs.access ts3client_binpath, fs.R_OK | fs.X_OK, defer err + if err + log.fatal "Can't access TeamSpeak3 client binary at #{ts3client_binpath}, does the binary exist and have you given correct access?" + throw new Error "Access to TeamSpeak3 binary failed." module.exports = class TS3ClientService extends services.Service dependencies: [ From 4b258b8ff48c82d7cee8e099e4c14147aa850cea Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 08:22:34 +0200 Subject: [PATCH 004/115] Disable client query plugin as it's not used currently. --- services/ts3client.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 9f94a66..826a6a2 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -225,7 +225,7 @@ module.exports = class TS3ClientService extends services.Service [ "News", "NewsClosed", "1" ] [ "News", "Language", "en" ] [ "Plugins", "teamspeak_control_plugin", "false" ] - [ "Plugins", "clientquery_plugin", "true" ] + [ "Plugins", "clientquery_plugin", "false" ] [ "Plugins", "lua_plugin", "false" ] [ "Plugins", "test_plugin", "false" ] [ "Plugins", "ts3g15", "false" ] From 47fa3e798c9e336712f6d882b8d9badc7adf5089 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 08:23:15 +0200 Subject: [PATCH 005/115] Try to hide news in GUI using time conditions. Might save on a tiny little bit of CPU time... I guess? --- services/ts3client.iced | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 826a6a2..af1688f 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -224,6 +224,9 @@ module.exports = class TS3ClientService extends services.Service [ "Misc", "LastShownNewsBrowserVersion", "4" ] [ "News", "NewsClosed", "1" ] [ "News", "Language", "en" ] + [ "News", "LastModified", (new Date()).toISOString() ] + [ "News", "NextCheck", (new Date() + 1000 * 60 * 60 * 24 * 360).toISOString() ] + [ "Notifications", "SoundPack", "nosounds" ] [ "Plugins", "teamspeak_control_plugin", "false" ] [ "Plugins", "clientquery_plugin", "false" ] [ "Plugins", "lua_plugin", "false" ] @@ -260,7 +263,6 @@ module.exports = class TS3ClientService extends services.Service agc: if config.get("ts3-agc") then "true" else "false" echo_reduction_db: 10 } ] - [ "Notifications", "SoundPack", "nosounds" ] [ "Statistics", "ParticipateStatistics", "0" ] [ "Statistics", "ConfirmedParticipation", "1" ] ], defer() From be478ae5044ab616a7112405428781678b8826f7 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 08:23:37 +0200 Subject: [PATCH 006/115] Another small CPU save by turning off logging for (almost) everything TS3. --- services/ts3client.iced | 1 + 1 file changed, 1 insertion(+) diff --git a/services/ts3client.iced b/services/ts3client.iced index af1688f..1292384 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -211,6 +211,7 @@ module.exports = class TS3ClientService extends services.Service [ "Chat", "IndicateChannelChats", "0" ] [ "Chat", "IndicatePrivateChats", "0" ] [ "Chat", "IndicateServerChats", "0" ] + [ "ClientLogView", "LogLevel", "000001" ] [ "FileTransfer", "SimultaneousDownloads", "2" ] [ "FileTransfer", "SimultaneousUploads", "2" ] [ "FileTransfer", "UploadBandwidth", "0" ] From fe293ace7c6aa02ae4a3e6e6cfd415261b1a7c83 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 09:21:58 +0200 Subject: [PATCH 007/115] Oops! --- services/ts3client.iced | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 1292384..04d8d5c 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -13,11 +13,10 @@ StreamSplitter = require("stream-splitter") require_bin = require("../require_bin") ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3client_linux_" + (if process.arch == "x64" then "amd64" else process.arch)) -if not path.exists - await fs.access ts3client_binpath, fs.R_OK | fs.X_OK, defer err - if err - log.fatal "Can't access TeamSpeak3 client binary at #{ts3client_binpath}, does the binary exist and have you given correct access?" - throw new Error "Access to TeamSpeak3 binary failed." +await fs.access ts3client_binpath, fs.R_OK | fs.X_OK, defer err +if err + log.fatal "Can't access TeamSpeak3 client binary at #{ts3client_binpath}, does the binary exist and have you given correct access?" + throw new Error "Access to TeamSpeak3 binary failed." module.exports = class TS3ClientService extends services.Service dependencies: [ From 5cd0c0ee1967d9a0e8e6658d4fae6442546f724c Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 09:36:52 +0200 Subject: [PATCH 008/115] Asynchronous code at module startup never goes well with NodeJS. --- services/ts3client.iced | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 04d8d5c..dd26a1c 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -13,10 +13,6 @@ StreamSplitter = require("stream-splitter") require_bin = require("../require_bin") ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3client_linux_" + (if process.arch == "x64" then "amd64" else process.arch)) -await fs.access ts3client_binpath, fs.R_OK | fs.X_OK, defer err -if err - log.fatal "Can't access TeamSpeak3 client binary at #{ts3client_binpath}, does the binary exist and have you given correct access?" - throw new Error "Access to TeamSpeak3 binary failed." module.exports = class TS3ClientService extends services.Service dependencies: [ @@ -37,6 +33,12 @@ module.exports = class TS3ClientService extends services.Service if not args args = [] + await fs.access ts3client_binpath, fs.R_OK | fs.X_OK, defer err + if err + log.fatal "Can't access TeamSpeak3 client binary at #{ts3client_binpath}, does the binary exist and have you given correct access?" + cb? new Error "Access to TeamSpeak3 binary failed." + return + await @_preconfigure defer() # spawn process From 3624c69d70483227643fac600d61ad842913ef39 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 09:43:32 +0200 Subject: [PATCH 009/115] log.fatal does - in fact - not exist. --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 2451a8b..325d330 100644 --- a/app.js +++ b/app.js @@ -31,7 +31,7 @@ doShutdownAsync = function(cb) { }; process.on("uncaughtException", function(err) { - log.fatal("Shutting down due to an uncaught exception!", err); + log.error("Shutting down due to an uncaught exception!", err); app.shutdownSync(); }); From 2b1942f79cd4a6d4ba61a0cfba54cb895df7616b Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 09:47:58 +0200 Subject: [PATCH 010/115] Here again. --- services/ts3client.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index dd26a1c..2e615df 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -35,7 +35,7 @@ module.exports = class TS3ClientService extends services.Service await fs.access ts3client_binpath, fs.R_OK | fs.X_OK, defer err if err - log.fatal "Can't access TeamSpeak3 client binary at #{ts3client_binpath}, does the binary exist and have you given correct access?" + log.error "Can't access TeamSpeak3 client binary at #{ts3client_binpath}, does the binary exist and have you given correct access?" cb? new Error "Access to TeamSpeak3 binary failed." return From 19198f0dc42ed1b3cceec9000a3fec1ac2b7a66b Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 10:11:38 +0200 Subject: [PATCH 011/115] Fixed news time calculation. --- services/ts3client.iced | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 2e615df..be922db 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -201,6 +201,7 @@ module.exports = class TS3ClientService extends services.Service identity.update() # Some settings to help the TS3Bot to do what it's supposed to do + now = new Date() await ts3settings.setMultiple [ [ "Application", "HotkeyMode", "2" ] [ "Chat", "MaxLines", "1" ] @@ -226,8 +227,8 @@ module.exports = class TS3ClientService extends services.Service [ "Misc", "LastShownNewsBrowserVersion", "4" ] [ "News", "NewsClosed", "1" ] [ "News", "Language", "en" ] - [ "News", "LastModified", (new Date()).toISOString() ] - [ "News", "NextCheck", (new Date() + 1000 * 60 * 60 * 24 * 360).toISOString() ] + [ "News", "LastModified", now.toISOString() ] + [ "News", "NextCheck", new Date(now.getTime() + 1000 * 60 * 60 * 24 * 365).toISOString() ] [ "Notifications", "SoundPack", "nosounds" ] [ "Plugins", "teamspeak_control_plugin", "false" ] [ "Plugins", "clientquery_plugin", "false" ] From 44ba3b7526d3824758e94949b0ccbe1bedfe4089 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 10:25:19 +0200 Subject: [PATCH 012/115] Fixing TS3 query reconnection. If the first connection to the query interface fails, the app didn't try to connect again afterwards because it assumed that the process has been aborted intentionally. --- services/ts3client.iced | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index be922db..7337ba0 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -126,12 +126,13 @@ module.exports = class TS3ClientService extends services.Service @query = new TS3ClientQuery "127.0.0.1", 25639 @_queryReconnectTimer = null @query.on "error", (err) => - if not @_requestedExit and @process and @process.connected - log.warn "Error in TS3 query connection, reconnecting..." + log.warn "Error in TS3 query connection", err + @query.on "close", => + if not @_requestedExit + log.warn "Connection to TS3 client query interface lost, reconnecting..." @_queryReconnectTimer = setTimeout @query.connect.bind(@query), 1000 else - log.warn "TS3 query connection terminated." - @query.on "close", => log.debug "Connection to TS3 client query interface lost." + log.debug "Connection to TS3 client query interface lost." @query.on "open", => log.debug "Connected to TS3 client query interface." @query.on "connecting", => log.debug "Connecting to TS3 client query interface..." From ab3e66ae8f62cac5ecd9a752637085c1e3f597ae Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 10:37:19 +0200 Subject: [PATCH 013/115] How did that end up here!? --- services/ts3client.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 7337ba0..ca662c4 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -232,7 +232,7 @@ module.exports = class TS3ClientService extends services.Service [ "News", "NextCheck", new Date(now.getTime() + 1000 * 60 * 60 * 24 * 365).toISOString() ] [ "Notifications", "SoundPack", "nosounds" ] [ "Plugins", "teamspeak_control_plugin", "false" ] - [ "Plugins", "clientquery_plugin", "false" ] + [ "Plugins", "clientquery_plugin", "true" ] [ "Plugins", "lua_plugin", "false" ] [ "Plugins", "test_plugin", "false" ] [ "Plugins", "ts3g15", "false" ] From 84797f6814877d713f6f189ae49eb3235c3ef692 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 14:42:03 +0200 Subject: [PATCH 014/115] Replace .gitignore with a more complete copy. --- .gitignore | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 10c5b9a..dd55084 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,28 @@ -iced +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules \ No newline at end of file From a5dbbb631be593e22266023cc35d870956ee7a04 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 15:25:46 +0200 Subject: [PATCH 015/115] Use youtube-dl to resolve URLs if possible. First try --format=bestaudio, if that fails try --format=best and if that fails as well, just pass the URL as-is to VLC. Very fault tolerant and silently ignores errors (except for letting it show up in the logs)... --- package.json | 83 ++++++++++++++++++++++++----------------------- services/api.iced | 20 ++++++++++-- 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 2e2c7e1..1c8aa6e 100644 --- a/package.json +++ b/package.json @@ -1,43 +1,44 @@ { - "name": "ts3bot", - "version": "0.2.0", - "description": "Allows running TeamSpeak3 as a bot for all kinds of media (local music/videos/streams/YouTube/...) without the need for a real GUI to exist.", - "main": "app.js", - "keywords": [ - "teamspeak", - "teamspeak3", - "ts3", - "bot", - "ts3bot", - "teamspeak3bot", - "music", - "playback", - "audio", - "video", - "media", - "musicbot" - ], - "author": "Carl Kittelberger ", - "license": "GPL-3.0+", - "dependencies": { - "express": "^4.13.3", - "iced-coffee-script": "^108.0.8", - "merge": "^1.2.0", - "mkdirp": "^0.5.1", - "nconf": "^0.7.2", - "npm-which": "^2.0.0", - "password-generator": "^2.0.1", - "querystring": "^0.2.0", - "request": "^2.61.0", - "simple-ini": "^1.0.3", - "sqlite3": "^3.1.0", - "stream-splitter": "^0.3.2", - "string.prototype.startswith": "^0.2.0", - "sync": "^0.2.5", - "valid-url": "^1.0.9", - "vlc-api": "0.0.0", - "which": "^1.1.2", - "winston": "^1.0.1", - "xvfb": "git://github.com/icedream/node-xvfb.git" - } + "name": "ts3bot", + "version": "0.2.0", + "description": "Allows running TeamSpeak3 as a bot for all kinds of media (local music/videos/streams/YouTube/...) without the need for a real GUI to exist.", + "main": "app.js", + "keywords": [ + "teamspeak", + "teamspeak3", + "ts3", + "bot", + "ts3bot", + "teamspeak3bot", + "music", + "playback", + "audio", + "video", + "media", + "musicbot" + ], + "author": "Carl Kittelberger ", + "license": "GPL-3.0+", + "dependencies": { + "express": "^4.13.3", + "iced-coffee-script": "^108.0.8", + "merge": "^1.2.0", + "mkdirp": "^0.5.1", + "nconf": "^0.7.2", + "npm-which": "^2.0.0", + "password-generator": "^2.0.1", + "querystring": "^0.2.0", + "request": "^2.61.0", + "simple-ini": "^1.0.3", + "sqlite3": "^3.1.0", + "stream-splitter": "^0.3.2", + "string.prototype.startswith": "^0.2.0", + "sync": "^0.2.5", + "valid-url": "^1.0.9", + "vlc-api": "0.0.0", + "which": "^1.1.2", + "winston": "^1.0.1", + "xvfb": "git://github.com/icedream/node-xvfb.git", + "youtube-dl": "^1.10.5" + } } diff --git a/services/api.iced b/services/api.iced index f92fd04..3d18085 100644 --- a/services/api.iced +++ b/services/api.iced @@ -7,6 +7,7 @@ Socket = net.Socket getLogger = require "../logger" config = require "../config" log = getLogger "API" +youtubedl = require "youtube-dl" #PulseAudio = require "pulseaudio" isValidUrl = (require "valid-url").isWebUri @@ -60,14 +61,29 @@ module.exports = class APIService extends services.Service log.warn "VLC API returned an error when trying to empty", err return - await vlc.status.play input, defer(err) + # let's give youtube-dl a shot! + await youtubedl.getInfo input, [ + "--format=bestaudio" + ], defer(err, info) + if err or not info? + 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 + + await vlc.status.play info.url, defer(err) if err vlc.status.empty() res.status(503).send("Something went wrong") log.warn "VLC API returned an error when trying to play", err return - res.send("OK") + res.send JSON.stringify info app.get "/stop", (req, res) => if not req.query.uid From 9fa980d2bc8ea697a6ee846652874dc3fc20c457 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 19:40:47 +0200 Subject: [PATCH 016/115] Don't log JSON representation of the TeamSpeak3 queries. --- ts3query.iced | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts3query.iced b/ts3query.iced index dafbfb4..a9505e6 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -119,7 +119,7 @@ module.exports = class TS3ClientQuery extends EventEmitter response = parseCmd token - @_log.silly "Recv:", response + @_log.silly "Recv:", token if response.name @emit "message.#{response.name}", response.args @@ -148,7 +148,7 @@ module.exports = class TS3ClientQuery extends EventEmitter text = buildCmd(cmd, namedArgs, positionalArgs) - @_log.silly "Send:", { cmd: cmd, namedArgs: namedArgs, positionalArgs: positionalArgs } + @_log.silly "Send:", text.trim() @_tcpClient.write text, "utf8", () => cb?() From 2dd995b2d77f5330b261e58967fa9176ff95f02f Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 19:41:21 +0200 Subject: [PATCH 017/115] Separate parameter name and value for "aout" parameter like we did with the others. --- services/vlc.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/vlc.iced b/services/vlc.iced index 71f18d4..7a3757c 100644 --- a/services/vlc.iced +++ b/services/vlc.iced @@ -31,7 +31,7 @@ module.exports = class VLCService extends services.Service "--http-host", config.get("vlc-host"), "--http-port", config.get("vlc-port"), "--http-password", config.get("vlc-password") - "--aout=pulse", + "--aout", "pulse", "--no-video" ], stdio: ['ignore', 'pipe', 'pipe'] From b62a1c46a62be2cd5b48699736a32c0ee5111a6e Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 19:46:58 +0200 Subject: [PATCH 018/115] Bump version to 0.2.1. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c8aa6e..274766c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts3bot", - "version": "0.2.0", + "version": "0.2.1", "description": "Allows running TeamSpeak3 as a bot for all kinds of media (local music/videos/streams/YouTube/...) without the need for a real GUI to exist.", "main": "app.js", "keywords": [ From 6d49bdfdaa1232b66df5ff70e884f137544a8a97 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 16 Oct 2015 19:49:09 +0200 Subject: [PATCH 019/115] Add "repository" field to package.json. --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 274766c..bc41172 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ ], "author": "Carl Kittelberger ", "license": "GPL-3.0+", + "repository": { + "type": "git", + "url": "https://github.com/icedream/ts3bot-control.git" + }, "dependencies": { "express": "^4.13.3", "iced-coffee-script": "^108.0.8", From 7890e2272300fb523b7b9f8be24049ef3fba3061 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 14:19:56 +0200 Subject: [PATCH 020/115] Remove the HTTP API server and do everything directly. --- app.iced | 100 +++++++++++++++++++++++++---------------- services.iced | 1 - services/api.iced | 111 ---------------------------------------------- 3 files changed, 61 insertions(+), 151 deletions(-) delete mode 100644 services/api.iced diff --git a/app.iced b/app.iced index ec9f0a5..6f25ac5 100644 --- a/app.iced +++ b/app.iced @@ -8,6 +8,8 @@ request = require "request" fs = require("fs") path = require("path") qs = require "querystring" +youtubedl = require "youtube-dl" +isValidUrl = (require "valid-url").isWebUri log = getLogger "Main" @@ -44,6 +46,14 @@ await services.find("pulseaudio").start defer err if err log.warn "PulseAudio could not start up, audio may not act as expected!" +# VLC HTTP API +await services.find("vlc").start defer err +if err + log.warn "VLC could not start up!" + await module.exports.shutdown defer() + process.exit 1 +vlc = services.find("vlc").instance + # TeamSpeak3 ts3clientService = services.find("ts3client") @@ -113,7 +123,7 @@ ts3clientService.on "started", (ts3proc) => ts3query.on "message.notifytextmessage", (args) => await ts3query.use args.schandlerid, defer(err, data) - + msg = args.msg invoker = { name: args.invokername, uid: args.invokeruid, id: args.invokerid } targetmode = args.targetmode # 1 = private, 2 = channel @@ -135,39 +145,58 @@ ts3clientService.on "started", (ts3proc) => switch name.toLowerCase() when "play" - q = - uid: invoker.uid - input: removeBB paramline - await request "http://127.0.0.1:16444/play?#{qs.stringify q}", defer(err, response) - switch response.statusCode - when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing #{paramline}." - when 400 then ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with what you wrote. Maybe check the URL/sound name you provided?" - when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to play #{q.input} via the bot." - else - log.warn "API reported error", response.statusCode, err - ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." + inputBB = paramline + input = removeBB paramline + + # 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 + + await vlc.status.empty defer(err) + if err + log.warn "Couldn't empty VLC playlist", err + ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." + return + + # 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 + + await vlc.status.play info.url, defer(err) + if err + vlc.status.empty() + log.warn "VLC API returned an error when trying to play", err + ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with that media. Maybe check the URL/sound name you provided?" + return + + ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]." when "stop" - q = - uid: invoker.uid - await request "http://127.0.0.1:16444/stop?#{qs.stringify q}", defer(err, response) - switch response.statusCode - when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback." - when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to do that." - else - log.warn "API reported error", response.statusCode, err - ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." + vlc.status.stop() + vlc.status.empty() + + ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback." when "setvolume" - q = - uid: invoker.uid - volume: parseFloat paramline - await request "http://127.0.0.1:16444/setvolume?#{qs.stringify q}", defer(err, response) - switch response.statusCode - when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Set volume to #{q.volume}" - when 400 then ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with what you wrote. Maybe check the volume? It's supposed to be a floating-point number between 0 and 2." - when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to do that." - else - log.warn "API reported error", response.statusCode, err - ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." + ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, that's not implemented yet." when "changenick" nick = if paramline.length > params[0].length then paramline else params[0] if nick.length < 1 or nick.length > 32 @@ -185,10 +214,3 @@ if err log.error "TeamSpeak3 could not start, shutting down." await module.exports.shutdown defer() process.exit 1 - -# HTTP API -await services.find("api").start defer err -if err - log.error "API could not start up, shutting down!" - await module.exports.shutdown defer() - process.exit 1 \ No newline at end of file diff --git a/services.iced b/services.iced index 917021b..2d6168a 100644 --- a/services.iced +++ b/services.iced @@ -56,7 +56,6 @@ module.exports.Service = require "./service_template" # register services services = [ - new(require "./services/api") new(require "./services/pulseaudio") new(require "./services/ts3client") new(require "./services/vlc") diff --git a/services/api.iced b/services/api.iced deleted file mode 100644 index 3d18085..0000000 --- a/services/api.iced +++ /dev/null @@ -1,111 +0,0 @@ -express = require "express" -url = require "url" -path = require "path" -spawn = require("child_process").spawn -net = require "net" -Socket = net.Socket -getLogger = require "../logger" -config = require "../config" -log = getLogger "API" -youtubedl = require "youtube-dl" -#PulseAudio = require "pulseaudio" -isValidUrl = (require "valid-url").isWebUri - -services = require "../services" - -module.exports = class APIService extends services.Service - dependencies: [ - "pulseaudio" - "vlc" - "ts3client" - ] - constructor: () -> super "API", - start: (cb) -> - if @httpServer - cb? null - return - - vlc = services.find("vlc").instance - ts3query = services.find("ts3client").query - - # set up HTTP server - log.debug "Starting up HTTP API..." - app = express() - app.get "/play", (req, res) => - if not req.query.uid - log.debug "Didn't get a UID, sending forbidden" - res.status(400).send("Forbidden") - return - if not req.query.input - log.debug "Didn't get an input URI/alias, sending bad request" - res.status(400).send("Bad request") - return - - input = null - # only allow playback from file if it's a preconfigured alias - if isValidUrl req.query.input - log.debug "Got input URL:", req.query.input - input = req.query.input - else - input = config.get("aliases:#{req.query.input}") - if not(isValidUrl input) and not(fs.existsSync input) - log.debug "Got neither valid URL nor valid alias:", req.query.input - res.status(403).send("Forbidden") - return - - # TODO: permission system to check if uid is allowed to play this url or alias - - await vlc.status.empty defer(err) - if err - res.status(503).send("Something went wrong") - log.warn "VLC API returned an error when trying to empty", err - return - - # let's give youtube-dl a shot! - await youtubedl.getInfo input, [ - "--format=bestaudio" - ], defer(err, info) - if err or not info? - 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 - - await vlc.status.play info.url, defer(err) - if err - vlc.status.empty() - res.status(503).send("Something went wrong") - log.warn "VLC API returned an error when trying to play", err - return - - res.send JSON.stringify info - - app.get "/stop", (req, res) => - if not req.query.uid - log.debug "Didn't get a UID, sending forbidden" - res.status(403).send("Forbidden - missing UID") - return - - # TODO: permission system to check if uid is allowed to stop playback - - vlc.status.stop() - vlc.status.empty() - - res.send("OK") - - app.get "/setvolume", (req, res) => - throw new "Not implemented yet" # FIXME below, still need to implement audio - - @httpServer = app.listen 16444 - - cb? null - - stop: (cb) -> - @httpServer.close() - - cb?() \ No newline at end of file From 4bfc0cabf49f28b95e17907c0c3423c7a15fe4ab Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 15:10:28 +0200 Subject: [PATCH 021/115] Wait for VLC to stop playback on demand. --- app.iced | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.iced b/app.iced index 6f25ac5..9f44b77 100644 --- a/app.iced +++ b/app.iced @@ -191,7 +191,8 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]." when "stop" - vlc.status.stop() + await vlc.status.stop defer(err) + vlc.status.empty() ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback." From e0379895d77c17a7eac1bd36b7b819f34b102970 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 15:13:53 +0200 Subject: [PATCH 022/115] Implemented "vol" command to set volume. By default it's 50% (127). The range is as-is in VLC, something between 0 (0%) and 511 (200%). You get this info as well if you just type "vol". --- app.iced | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app.iced b/app.iced index 9f44b77..a349f8f 100644 --- a/app.iced +++ b/app.iced @@ -53,6 +53,7 @@ if err await module.exports.shutdown defer() process.exit 1 vlc = services.find("vlc").instance +vlc.status.volume 127 # that's 50% (about half of 0xFF) # TeamSpeak3 ts3clientService = services.find("ts3client") @@ -196,8 +197,21 @@ ts3clientService.on "started", (ts3proc) => vlc.status.empty() ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback." - when "setvolume" - ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, that's not implemented yet." + when "vol" + vol = parseInt paramline + + if paramline.trim().length <= 0 or vol > 511 or vol < 0 + ts3query.sendtextmessage args.targetmode, invoker.id, "The [b]vol[/b] command takes a number between 0 (0%) and 511 (200%) to set the volume. 100% is 127." + return + + await vlc.status.volume paramline, defer(err) + + if err + log.warn "Failed to set volume", err + ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out." + return + + ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set." when "changenick" nick = if paramline.length > params[0].length then paramline else params[0] if nick.length < 1 or nick.length > 32 From f368221b3b5a3864212561361e5d3f3018a9b699 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 15:37:02 +0200 Subject: [PATCH 023/115] Implement "enqueue", "next" and a URL-less "play" to resume from paused tracks. --- app.iced | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/app.iced b/app.iced index a349f8f..3818f98 100644 --- a/app.iced +++ b/app.iced @@ -146,8 +146,14 @@ ts3clientService.on "started", (ts3proc) => switch name.toLowerCase() when "play" - inputBB = paramline - input = removeBB paramline + inputBB = paramline.trim() + + # we gonna interpret play without a url as an attempt to unpause the current song + if inputBB.length <= 0 + vlc.status.resume() + return + + input = removeBB inputBB # only allow playback from file if it's a preconfigured alias if isValidUrl input @@ -190,6 +196,55 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with that media. Maybe check the URL/sound name you provided?" return + ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]." + when "next" + await vlc.status.next defer(err) + if err + vlc.status.empty() + log.warn "VLC API returned an error when trying to skip current song", err + ts3query.sendtextmessage args.targetmode, invoker.id, "This unfortunately didn't work out, I'm sorry." + return + + ts3query.sendtextmessage args.targetmode, invoker.id, "Going to the next playlist entry." + when "enqueue" + inputBB = paramline + input = removeBB paramline + + # 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 + + await vlc.status.enqueue info.url, defer(err) + if err + vlc.status.empty() + log.warn "VLC API returned an error when trying to play", err + ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with that media. Maybe check the URL/sound name you provided?" + return + ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]." when "stop" await vlc.status.stop defer(err) From 69b6deb9be2c1ca9f390059630c366d37a05e1d4 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 15:37:58 +0200 Subject: [PATCH 024/115] Implement "pause" command to pause current track. --- app.iced | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.iced b/app.iced index 3818f98..5ec734f 100644 --- a/app.iced +++ b/app.iced @@ -145,6 +145,9 @@ ts3clientService.on "started", (ts3proc) => params = [] switch name.toLowerCase() + when "pause" + vlc.status.pause() + return when "play" inputBB = paramline.trim() From 8964db5afa6445ed375b75af66cedd3690e480b7 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 16:31:53 +0200 Subject: [PATCH 025/115] Command aliases for "add" and "append" -> "enqueue". --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index 5ec734f..c865c3a 100644 --- a/app.iced +++ b/app.iced @@ -209,7 +209,7 @@ ts3clientService.on "started", (ts3proc) => return ts3query.sendtextmessage args.targetmode, invoker.id, "Going to the next playlist entry." - when "enqueue" + when "enqueue", "add", "append" inputBB = paramline input = removeBB paramline From e4f3e4f2c2cfa23697197be5db5afdca8c8f4bc8 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 16:32:10 +0200 Subject: [PATCH 026/115] Prevent running the "enqueue" command without a URL. --- app.iced | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app.iced b/app.iced index c865c3a..9a8a5d8 100644 --- a/app.iced +++ b/app.iced @@ -211,6 +211,11 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Going to the next playlist entry." when "enqueue", "add", "append" inputBB = paramline + + if inputBB.length <= 0 + ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name}[/B] takes a URL as a parameter that should be appended to the playlist." + return + input = removeBB paramline # only allow playback from file if it's a preconfigured alias From 2dd16830ecce9b7b1c3b4cb7dd2be7d38abe7d7c Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 16:32:31 +0200 Subject: [PATCH 027/115] Fix default volume. --- app.iced | 1 - services/vlc.iced | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index 9a8a5d8..99d4f4f 100644 --- a/app.iced +++ b/app.iced @@ -53,7 +53,6 @@ if err await module.exports.shutdown defer() process.exit 1 vlc = services.find("vlc").instance -vlc.status.volume 127 # that's 50% (about half of 0xFF) # TeamSpeak3 ts3clientService = services.find("ts3client") diff --git a/services/vlc.iced b/services/vlc.iced index 7a3757c..d0be6c4 100644 --- a/services/vlc.iced +++ b/services/vlc.iced @@ -32,6 +32,7 @@ module.exports = class VLCService extends services.Service "--http-port", config.get("vlc-port"), "--http-password", config.get("vlc-password") "--aout", "pulse", + "--volume", "128", # 50% volume "--no-video" ], stdio: ['ignore', 'pipe', 'pipe'] From 1b57bd36ce7c65874b85485c582b131d605a1fa2 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 16:32:58 +0200 Subject: [PATCH 028/115] Fix response messages for volume and adding to playlist. --- app.iced | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.iced b/app.iced index 99d4f4f..587fbef 100644 --- a/app.iced +++ b/app.iced @@ -252,7 +252,7 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with that media. Maybe check the URL/sound name you provided?" return - ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]." + ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist." when "stop" await vlc.status.stop defer(err) @@ -263,7 +263,7 @@ ts3clientService.on "started", (ts3proc) => vol = parseInt paramline if paramline.trim().length <= 0 or vol > 511 or vol < 0 - ts3query.sendtextmessage args.targetmode, invoker.id, "The [b]vol[/b] command takes a number between 0 (0%) and 511 (200%) to set the volume. 100% is 127." + ts3query.sendtextmessage args.targetmode, invoker.id, "The [b]vol[/b] command takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup." return await vlc.status.volume paramline, defer(err) From a2c4406f09724c7d43ddfbdd93e1e8b67201dfc4 Mon Sep 17 00:00:00 2001 From: avail Date: Wed, 21 Oct 2015 16:50:36 +0200 Subject: [PATCH 029/115] string iprovements --- app.iced | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app.iced b/app.iced index 587fbef..8da20c4 100644 --- a/app.iced +++ b/app.iced @@ -195,7 +195,7 @@ ts3clientService.on "started", (ts3proc) => if err vlc.status.empty() log.warn "VLC API returned an error when trying to play", err - ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with that media. Maybe check the URL/sound name you provided?" + ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?" return ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]." @@ -204,15 +204,15 @@ ts3clientService.on "started", (ts3proc) => if err vlc.status.empty() log.warn "VLC API returned an error when trying to skip current song", err - ts3query.sendtextmessage args.targetmode, invoker.id, "This unfortunately didn't work out, I'm sorry." + ts3query.sendtextmessage args.targetmode, invoker.id, "This didn't work. Does the playlist have multiple songs?" return - ts3query.sendtextmessage args.targetmode, invoker.id, "Going to the next playlist entry." + ts3query.sendtextmessage args.targetmode, invoker.id, "Playing next song in the playlist." when "enqueue", "add", "append" inputBB = paramline if inputBB.length <= 0 - ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name}[/B] takes a URL as a parameter that should be appended to the playlist." + ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} *url*[/B] - adds the specified URL to the current playlist" return input = removeBB paramline @@ -249,7 +249,7 @@ ts3clientService.on "started", (ts3proc) => if err vlc.status.empty() log.warn "VLC API returned an error when trying to play", err - ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with that media. Maybe check the URL/sound name you provided?" + ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?" return ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist." @@ -263,7 +263,7 @@ ts3clientService.on "started", (ts3proc) => vol = parseInt paramline if paramline.trim().length <= 0 or vol > 511 or vol < 0 - ts3query.sendtextmessage args.targetmode, invoker.id, "The [b]vol[/b] command takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup." + ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol *number*[/B] - takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup." return await vlc.status.volume paramline, defer(err) From 1846e96806fe59fdaa3a4d02d46adaee74c4ebb8 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 16:56:16 +0200 Subject: [PATCH 030/115] Use <> instead of ** for marking parameters in the text. --- app.iced | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.iced b/app.iced index 8da20c4..2970b0c 100644 --- a/app.iced +++ b/app.iced @@ -212,7 +212,7 @@ ts3clientService.on "started", (ts3proc) => inputBB = paramline if inputBB.length <= 0 - ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} *url*[/B] - adds the specified URL to the current playlist" + ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} [/B] - Adds the specified URL to the current playlist" return input = removeBB paramline @@ -263,7 +263,7 @@ ts3clientService.on "started", (ts3proc) => vol = parseInt paramline if paramline.trim().length <= 0 or vol > 511 or vol < 0 - ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol *number*[/B] - takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup." + ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol [/B] - takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup." return await vlc.status.volume paramline, defer(err) From 6568e1e5ebddd79fda199602d41f18beb84faf78 Mon Sep 17 00:00:00 2001 From: avail Date: Wed, 21 Oct 2015 19:38:13 +0200 Subject: [PATCH 031/115] fix nickname letter limit error message --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index 2970b0c..fc5f057 100644 --- a/app.iced +++ b/app.iced @@ -276,7 +276,7 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set." when "changenick" nick = if paramline.length > params[0].length then paramline else params[0] - if nick.length < 1 or nick.length > 32 + if nick.length < 3 or nick.length > 30 ts3query.sendtextmessage args.targetmode, invoker.id, "Invalid nickname." return Sync () => From 0de22eb9e1629a758314b4aafb0f2259a4ecb8b9 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 20:58:36 +0200 Subject: [PATCH 032/115] Trim parameters so whitespace doesn't invalidate URLs. --- app.iced | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app.iced b/app.iced index fc5f057..02782f8 100644 --- a/app.iced +++ b/app.iced @@ -149,14 +149,13 @@ ts3clientService.on "started", (ts3proc) => 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 inputBB.length <= 0 + if input.length <= 0 vlc.status.resume() return - input = removeBB inputBB - # only allow playback from file if it's a preconfigured alias if isValidUrl input log.debug "Got input URL:", input @@ -209,14 +208,13 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Playing next song in the playlist." when "enqueue", "add", "append" - inputBB = paramline + 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 - input = removeBB paramline - # only allow playback from file if it's a preconfigured alias if isValidUrl input log.debug "Got input URL:", input From 6695fbd83ee07677e6ef920d7687de6a24011d90 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 21 Oct 2015 20:59:25 +0200 Subject: [PATCH 033/115] Log updates on identities. --- ts3settings.iced | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ts3settings.iced b/ts3settings.iced index c673aac..1364464 100644 --- a/ts3settings.iced +++ b/ts3settings.iced @@ -211,8 +211,10 @@ module.exports = class SettingsFile select: () -> settingsObj.defaultIdentity = @id update: () -> + @log.silly "Requested update of #{id.id}" for own index, identity of settingsObj.identities if identity.id == id.id + @log.silly "Updating identity #{id.id}" settingsObj.identities[index] = merge identity, id return remove: () -> From 1892b56e4cffc583e7faa1ca7fe7cf2534b30c1b Mon Sep 17 00:00:00 2001 From: icedream Date: Sat, 24 Oct 2015 10:26:01 +0200 Subject: [PATCH 034/115] @log -> settingsObj. --- ts3settings.iced | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts3settings.iced b/ts3settings.iced index 1364464..f9d2472 100644 --- a/ts3settings.iced +++ b/ts3settings.iced @@ -211,10 +211,10 @@ module.exports = class SettingsFile select: () -> settingsObj.defaultIdentity = @id update: () -> - @log.silly "Requested update of #{id.id}" + settingsObj.silly "Requested update of #{id.id}" for own index, identity of settingsObj.identities if identity.id == id.id - @log.silly "Updating identity #{id.id}" + settingsObj.silly "Updating identity #{id.id}" settingsObj.identities[index] = merge identity, id return remove: () -> From ed10e875b3bf07f1ddfc3cca2d324fb64ec6d9ed Mon Sep 17 00:00:00 2001 From: icedream Date: Sat, 24 Oct 2015 11:28:44 +0200 Subject: [PATCH 035/115] I'm on a roll with this today. --- ts3settings.iced | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts3settings.iced b/ts3settings.iced index f9d2472..81f9e3b 100644 --- a/ts3settings.iced +++ b/ts3settings.iced @@ -211,10 +211,10 @@ module.exports = class SettingsFile select: () -> settingsObj.defaultIdentity = @id update: () -> - settingsObj.silly "Requested update of #{id.id}" + settingsObj.log.silly "Requested update of #{id.id}" for own index, identity of settingsObj.identities if identity.id == id.id - settingsObj.silly "Updating identity #{id.id}" + settingsObj.log.silly "Updating identity #{id.id}" settingsObj.identities[index] = merge identity, id return remove: () -> From ac3531a8753599c1fd2e712807df29fa187ff213 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 00:13:10 +0100 Subject: [PATCH 036/115] Add webchimera.js to dependencies - we're now switching! See issue #17. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index bc41172..6e5a85e 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "sync": "^0.2.5", "valid-url": "^1.0.9", "vlc-api": "0.0.0", + "webchimera.js": "^0.1.38", "which": "^1.1.2", "winston": "^1.0.1", "xvfb": "git://github.com/icedream/node-xvfb.git", From 394b16ab5eee9780d71961f6c3a0674f5ca69861 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 01:14:55 +0100 Subject: [PATCH 037/115] We're now completely getting rid of vlc-api. - Hopefully fixes audio volume not being set to 50% at startup. - Volume now is in the range of 0 to 200 (percentage, so goes from 0% to 200% for those numbers). - Made message displaying volume change display the actual level set. - We're no longer providing an HTTP interface. - Now stores metadata info in a separate variable for later retrieval by input MRL. - Now prints when player reaches end of playlist. - Now supports toggle-pausing using "pause" command. - Should fix playlist not being able to kick off on "play" if stopped. - Removed vlc-api dependency. Relates to issues #17, #10 and #7 (partially). --- app.iced | 85 +++++++++++++++++++++-------------------------- package.json | 1 - services/vlc.iced | 83 +++++++++++---------------------------------- 3 files changed, 56 insertions(+), 113 deletions(-) diff --git a/app.iced b/app.iced index 02782f8..7c48de4 100644 --- a/app.iced +++ b/app.iced @@ -46,13 +46,16 @@ await services.find("pulseaudio").start defer err if err log.warn "PulseAudio could not start up, audio may not act as expected!" -# VLC HTTP API -await services.find("vlc").start defer err +# VLC via WebChimera.js +vlcService = services.find("vlc") +await vlcService.start defer err, vlc if err log.warn "VLC could not start up!" await module.exports.shutdown defer() process.exit 1 -vlc = services.find("vlc").instance + +# Cached information for tracks in playlist +vlcMediaInfo = {} # TeamSpeak3 ts3clientService = services.find("ts3client") @@ -60,6 +63,17 @@ ts3clientService = services.find("ts3client") ts3clientService.on "started", (ts3proc) => ts3query = ts3clientService.query + # VLC event handling + vlc.onPlaying () => + info = vlcMediaInfo[vlc.playlist.items[vlc.playlist.currentItem].mrl] + ts3query.sendtextmessage 2, 0, "Now playing [URL=#{info.originalUrl}]#{info.title}[/URL]." + 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.onEndReached () => ts3query.sendtextmessage 2, 0, "End of playlist reached." + vlc.onStopped () => ts3query.sendtextmessage 2, 0, "Stopped." + ts3query.currentScHandlerID = 1 ts3query.mydata = {} @@ -145,7 +159,8 @@ ts3clientService.on "started", (ts3proc) => switch name.toLowerCase() when "pause" - vlc.status.pause() + # now we can toggle-pause playback this easily! yay! + vlc.togglePause() return when "play" inputBB = paramline.trim() @@ -153,7 +168,7 @@ ts3clientService.on "started", (ts3proc) => # we gonna interpret play without a url as an attempt to unpause the current song if input.length <= 0 - vlc.status.resume() + vlc.play() return # only allow playback from file if it's a preconfigured alias @@ -168,11 +183,7 @@ ts3clientService.on "started", (ts3proc) => # TODO: permission system to check if uid is allowed to play this url or alias - await vlc.status.empty defer(err) - if err - log.warn "Couldn't empty VLC playlist", err - ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." - return + vlc.playlist.clear() # let's give youtube-dl a shot! await youtubedl.getInfo input, [ @@ -189,24 +200,13 @@ ts3clientService.on "started", (ts3proc) => if not info.url? info.url = input info.title = input # URL as title + info.originalUrl = input + vlcMediaInfo[info.url] = info - await vlc.status.play info.url, defer(err) - if err - vlc.status.empty() - log.warn "VLC API returned an error when trying to play", err - ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?" - return - - ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]." + # play it in VLC + vlc.play info.url when "next" - await vlc.status.next defer(err) - if err - vlc.status.empty() - log.warn "VLC API returned an error when trying to skip current song", err - ts3query.sendtextmessage args.targetmode, invoker.id, "This didn't work. Does the playlist have multiple songs?" - return - - ts3query.sendtextmessage args.targetmode, invoker.id, "Playing next song in the playlist." + vlc.playlist.next() when "enqueue", "add", "append" inputBB = paramline.trim() input = (removeBB paramline).trim() @@ -242,36 +242,25 @@ ts3clientService.on "started", (ts3proc) => if not info.url? info.url = input info.title = input # URL as title + info.originalUrl = input + vlcMediaInfo[info.url] = info - await vlc.status.enqueue info.url, defer(err) - if err - vlc.status.empty() - log.warn "VLC API returned an error when trying to play", err - ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?" - return - + # 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" - await vlc.status.stop defer(err) - - vlc.status.empty() - - ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback." + vlc.stop() when "vol" vol = parseInt paramline - if paramline.trim().length <= 0 or vol > 511 or vol < 0 - ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol [/B] - takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup." + if paramline.trim().length <= 0 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 - await vlc.status.volume paramline, defer(err) - - if err - log.warn "Failed to set volume", err - ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out." - return - - ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set." + vlc.audio.volume = vol + ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%." when "changenick" nick = if paramline.length > params[0].length then paramline else params[0] if nick.length < 3 or nick.length > 30 diff --git a/package.json b/package.json index 6e5a85e..78ac299 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "string.prototype.startswith": "^0.2.0", "sync": "^0.2.5", "valid-url": "^1.0.9", - "vlc-api": "0.0.0", "webchimera.js": "^0.1.38", "which": "^1.1.2", "winston": "^1.0.1", diff --git a/services/vlc.iced b/services/vlc.iced index d0be6c4..b52ee83 100644 --- a/services/vlc.iced +++ b/services/vlc.iced @@ -1,88 +1,43 @@ spawn = require("child_process").spawn services = require("../services") config = require("../config") -VLCApi = require("vlc-api") +wc = require("webchimera.js") StreamSplitter = require("stream-splitter") -require_bin = require("../require_bin") - -vlcBinPath = require_bin "vlc" module.exports = class VLCService extends services.Service dependencies: [ "pulseaudio" ] constructor: -> super "VLC", + ### + # Starts an instance of VLC and keeps it ready for service. + ### start: (cb) -> - if @_process - cb? null, @_process + if @_instance + cb? null, @_instance return calledCallback = false - proc = null - doStart = null - doStart = () => - await services.find("pulseaudio").start defer(err) - if err - throw new Error "Dependency pulseaudio failed." + instance = wc.createPlayer [ + "--aout", "pulse", + "--no-video" + ] + instance.audio.volume = 50 - proc = spawn vlcBinPath, [ - "-I", "http", - "--http-host", config.get("vlc-host"), - "--http-port", config.get("vlc-port"), - "--http-password", config.get("vlc-password") - "--aout", "pulse", - "--volume", "128", # 50% volume - "--no-video" - ], - stdio: ['ignore', 'pipe', 'pipe'] - detached: true - - # logging - stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n" - stdoutTokenizer.encoding = "utf8"; - stdoutTokenizer.on "token", (token) => - token = token.trim() # get rid of \r - @log.debug token - - stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" - stderrTokenizer.encoding = "utf8"; - stderrTokenizer.on "token", (token) => - token = token.trim() # get rid of \r - @log.debug token - - proc.on "exit", () => - if @state == "stopping" - return - if not calledCallback - calledCallback = true - @log.warn "VLC terminated unexpectedly during startup." - cb? new Error "VLC terminated unexpectedly." - @log.warn "VLC terminated unexpectedly, restarting." - doStart() - - @_process = proc - - doStart() - - setTimeout (() => - if not calledCallback - calledCallback = true - - @instance = new VLCApi - host: ":#{encodeURIComponent config.get("vlc-password")}@#{config.get("vlc-host")}", - port: config.get("vlc-port") - cb? null, @instance), 1500 # TODO: Use some more stable condition + @_instance = instance + cb? null, @_instance + ### + # Shuts down the VLC instance. + ### stop: (cb) -> - if not @_process + if not @_instance cb?() return - @instance = null - - @_process.kill() - await @_process.once "exit", defer() + # TODO: Is there even a proper way to shut this down? + @_instance = null cb?() From 852052388df2c469e26a9455d5fd21f5db78c065 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 22:40:39 +0100 Subject: [PATCH 038/115] VLC event members are actually properties, not functions. Got the documentation wrong here, I was looking up examples and it looked as if calling the on* fields as functions was the right way to go but it was actually not. https://github.com/RSATom/WebChimera.js/blob/64bf4ffc362898e309c8a444c8888ad7b4bfae92/src/JsVlcPlayer.cpp#L90-L102 clearly defines as set-properties here. --- app.iced | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app.iced b/app.iced index 7c48de4..21826f9 100644 --- a/app.iced +++ b/app.iced @@ -64,15 +64,15 @@ ts3clientService.on "started", (ts3proc) => ts3query = ts3clientService.query # VLC event handling - vlc.onPlaying () => + vlc.onPlaying = () => info = vlcMediaInfo[vlc.playlist.items[vlc.playlist.currentItem].mrl] ts3query.sendtextmessage 2, 0, "Now playing [URL=#{info.originalUrl}]#{info.title}[/URL]." - 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.onEndReached () => ts3query.sendtextmessage 2, 0, "End of playlist reached." - vlc.onStopped () => ts3query.sendtextmessage 2, 0, "Stopped." + 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.onEndReached = () => ts3query.sendtextmessage 2, 0, "End of playlist reached." + vlc.onStopped = () => ts3query.sendtextmessage 2, 0, "Stopped." ts3query.currentScHandlerID = 1 ts3query.mydata = {} From 8ab524467bb56e90e17b9c9d3da887dd743c8c76 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 22:47:29 +0100 Subject: [PATCH 039/115] Fix non-numeric volume value being passed through to VLC as NaN. Those values are not supposed to be accepted in the first place... --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index 21826f9..95c94f1 100644 --- a/app.iced +++ b/app.iced @@ -255,7 +255,7 @@ ts3clientService.on "started", (ts3proc) => when "vol" vol = parseInt paramline - if paramline.trim().length <= 0 or vol > 200 or vol < 0 + if paramline.trim().length <= 0 or vol == NaN 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 From 824b4b94bd1dfbe12df6fefebe83342b33c27721 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 22:59:17 +0100 Subject: [PATCH 040/115] Implement "prev"/"previous" commands. These commands allow going to the previous entry in the playlist. --- app.iced | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.iced b/app.iced index 95c94f1..4d94018 100644 --- a/app.iced +++ b/app.iced @@ -207,6 +207,8 @@ ts3clientService.on "started", (ts3proc) => vlc.play info.url when "next" vlc.playlist.next() + when "prev", "previous" + vlc.playlist.prev() when "enqueue", "add", "append" inputBB = paramline.trim() input = (removeBB paramline).trim() From 4eed972a235325b26276ad4a6f040b79081019f1 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:01:23 +0100 Subject: [PATCH 041/115] Implement "empty"/"clear" commands. These commands allow emptying the current playlist. Here another reminder to implement a proper permission system soon! --- app.iced | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.iced b/app.iced index 4d94018..89e5a8d 100644 --- a/app.iced +++ b/app.iced @@ -209,6 +209,9 @@ ts3clientService.on "started", (ts3proc) => vlc.playlist.next() when "prev", "previous" 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() From cc783d0bc0db4a0b1d69463607a3f0751cb4dd64 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:09:28 +0100 Subject: [PATCH 042/115] Implement "list"/"playlist" commands. These commands make the bot print out the current playlist in the channel chat. The current track being played back is printed in bold green font. --- app.iced | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app.iced b/app.iced index 89e5a8d..a4cf592 100644 --- a/app.iced +++ b/app.iced @@ -212,6 +212,16 @@ ts3clientService.on "started", (ts3proc) => when "empty", "clear" vlc.playlist.clear() ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist." + when "list", "playlist" + message = "Currently these tracks are in the playlist:\n" + for i in [ 0 .. vlc.playlist.items.count ] + if vlc.playlist.currentItem == i + message += "[COLOR=green][B]" + info = vlcMediaInfo[vlc.playlist.items[i].mrl] + message += "#{i + 1}. [URL=#{info.originalUrl}]#{info.title}[/URL]" + if vlc.playlist.currentItem == i + message += "[/B][/COLOR]" + ts3query.sendtextmessage args.targetmode, invoker.id, message when "enqueue", "add", "append" inputBB = paramline.trim() input = (removeBB paramline).trim() From ab18c3cd8f2824f94f818550ac77a9e0702dc76d Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:12:47 +0100 Subject: [PATCH 043/115] According to the WebChimera documentation this is not zero-based... --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index a4cf592..da79b65 100644 --- a/app.iced +++ b/app.iced @@ -214,7 +214,7 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist." when "list", "playlist" message = "Currently these tracks are in the playlist:\n" - for i in [ 0 .. vlc.playlist.items.count ] + for i in [ 1 .. vlc.playlist.items.count ] if vlc.playlist.currentItem == i message += "[COLOR=green][B]" info = vlcMediaInfo[vlc.playlist.items[i].mrl] From d61d85e020cfaab777ad7aef978b204df776485f Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:18:13 +0100 Subject: [PATCH 044/115] Got the right CoffeeScript syntax for this loop now. --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index da79b65..ac0a8b1 100644 --- a/app.iced +++ b/app.iced @@ -214,7 +214,7 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist." when "list", "playlist" message = "Currently these tracks are in the playlist:\n" - for i in [ 1 .. vlc.playlist.items.count ] + for i in [ 0 ... vlc.playlist.items.count ] if vlc.playlist.currentItem == i message += "[COLOR=green][B]" info = vlcMediaInfo[vlc.playlist.items[i].mrl] From eb00dc2f8795d15710d368f2fdb2f0df56509c93 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:19:40 +0100 Subject: [PATCH 045/115] While we're on it, fine-tuning the playlist message. --- app.iced | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.iced b/app.iced index ac0a8b1..3f6cd34 100644 --- a/app.iced +++ b/app.iced @@ -213,7 +213,8 @@ ts3clientService.on "started", (ts3proc) => vlc.playlist.clear() ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist." when "list", "playlist" - message = "Currently these tracks are in the playlist:\n" + message = "Currently there are #{vlc.playlist.items.count} tracks are in the playlist" + message += if vlc.playlist.items.count > 0 then ":\n" else "." for i in [ 0 ... vlc.playlist.items.count ] if vlc.playlist.currentItem == i message += "[COLOR=green][B]" From 0762d6d0bdd182a2b0949ab74202df7c3cea9222 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:38:14 +0100 Subject: [PATCH 046/115] Fix line breaks and tabs not being properly escaped in ts3query.iced. --- ts3query.iced | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ts3query.iced b/ts3query.iced index a9505e6..3b1b076 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -10,9 +10,23 @@ merge = require "merge" parserLog = getLogger "parser" -escape = (value) => value.toString().replace(///\\///g, "\\\\").replace(/\//g, "\\/").replace(/\|/g, "\\|").replace(///\ ///g, "\\s") +escape = (value) => value.toString()\ + .replace(/\\/g, "\\\\")\ + .replace(/\//g, "\\/")\ + .replace(/\|/g, "\\|")\ + .replace(/\n/g, "\\n")\ + .replace(/\r/g, "\\r")\ + .replace(/\t/g, "\\t")\ + .replace(/\ /g, "\\s") -unescape = (value) => value.toString().replace(/\\s/g, " ").replace(/\\\//g, "/").replace(/\\\|/g, "|").replace(/\\\\/g, "\\") +unescape = (value) => value.toString()\ + .replace(/\\s/g, " ")\ + .replace(/\\t/g, "\t")\ + .replace(/\\r/g, "\r")\ + .replace(/\\n/g, "\n")\ + .replace(/\\\|/g, "|")\ + .replace(/\\\//g, "/")\ + .replace(/\\\\/g, "\\") buildCmd = (name, namedArgs, posArgs) => # TODO: Add support for collected arguments (aka lists) From d82721128f6319d4382daf0ee9adf9cf010137c1 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:54:01 +0100 Subject: [PATCH 047/115] Implement "loop" command. This command allows looping the playlist by just passing the command "loop on". Respectively, "loop off" turns off looping again. --- app.iced | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app.iced b/app.iced index 3f6cd34..e009421 100644 --- a/app.iced +++ b/app.iced @@ -205,6 +205,24 @@ ts3clientService.on "started", (ts3proc) => # play it in VLC vlc.play info.url + 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" vlc.playlist.next() when "prev", "previous" From 18b7d8296447e6dbfd8dfa82e7810e602294e40d Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:54:36 +0100 Subject: [PATCH 048/115] Implement "stop-after" command. This command allows stopping the playlist after the current playlist item is finished. --- app.iced | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.iced b/app.iced index e009421..b8e8fb1 100644 --- a/app.iced +++ b/app.iced @@ -205,6 +205,9 @@ ts3clientService.on "started", (ts3proc) => # 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 From 7966bfb0562403214fa673f113fc1664ae30ee3e Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:55:54 +0100 Subject: [PATCH 049/115] Playlist display (command "list"/"playlist") can generate too long messages, commenting out for now. --- app.iced | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.iced b/app.iced index b8e8fb1..1a751ca 100644 --- a/app.iced +++ b/app.iced @@ -233,6 +233,7 @@ ts3clientService.on "started", (ts3proc) => when "empty", "clear" vlc.playlist.clear() ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist." + ### when "list", "playlist" message = "Currently there are #{vlc.playlist.items.count} tracks are in the playlist" message += if vlc.playlist.items.count > 0 then ":\n" else "." @@ -244,6 +245,7 @@ ts3clientService.on "started", (ts3proc) => if vlc.playlist.currentItem == i message += "[/B][/COLOR]" ts3query.sendtextmessage args.targetmode, invoker.id, message + ### when "enqueue", "add", "append" inputBB = paramline.trim() input = (removeBB paramline).trim() From b238a655b9b13b1504cdb0f8a0f1823d5904c4f1 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 27 Oct 2015 23:57:50 +0100 Subject: [PATCH 050/115] This indenting stuff still sometimes doesn't really want to work out I guess... --- app.iced | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app.iced b/app.iced index 1a751ca..f56ee7f 100644 --- a/app.iced +++ b/app.iced @@ -233,19 +233,6 @@ ts3clientService.on "started", (ts3proc) => when "empty", "clear" vlc.playlist.clear() ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist." - ### - when "list", "playlist" - message = "Currently there are #{vlc.playlist.items.count} tracks are in the playlist" - message += if vlc.playlist.items.count > 0 then ":\n" else "." - for i in [ 0 ... vlc.playlist.items.count ] - if vlc.playlist.currentItem == i - message += "[COLOR=green][B]" - info = vlcMediaInfo[vlc.playlist.items[i].mrl] - message += "#{i + 1}. [URL=#{info.originalUrl}]#{info.title}[/URL]" - if vlc.playlist.currentItem == i - message += "[/B][/COLOR]" - ts3query.sendtextmessage args.targetmode, invoker.id, message - ### when "enqueue", "add", "append" inputBB = paramline.trim() input = (removeBB paramline).trim() From a5a8ae1046b453a24c54af816e3e7e66795ef8ec Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 28 Oct 2015 00:12:50 +0100 Subject: [PATCH 051/115] Implement some easy checks for "next" and "prev" commands. --- app.iced | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app.iced b/app.iced index f56ee7f..834b5f2 100644 --- a/app.iced +++ b/app.iced @@ -227,8 +227,14 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} on|off[/B] - Turns playlist looping on or off" return when "next" + 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.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() From 9417e337087c360b9a32243960d98cbc8d3d6283 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 28 Oct 2015 00:14:44 +0100 Subject: [PATCH 052/115] Small fix for "prev" not giving an error when in empty playlist. --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index 834b5f2..cf5006f 100644 --- a/app.iced +++ b/app.iced @@ -232,7 +232,7 @@ ts3clientService.on "started", (ts3proc) => return vlc.playlist.next() when "prev", "previous" - if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem == 0 + 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() From 4d25c2b822d1660546a48b89db34645285408dd7 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 28 Oct 2015 00:16:10 +0100 Subject: [PATCH 053/115] Small fixes for empty playlists. --- app.iced | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app.iced b/app.iced index cf5006f..e71ef00 100644 --- a/app.iced +++ b/app.iced @@ -227,11 +227,17 @@ ts3clientService.on "started", (ts3proc) => 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 From f3157857c488621ca2d172bf505af47e3cf1249b Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 28 Oct 2015 00:21:54 +0100 Subject: [PATCH 054/115] Completely remove handler for vlc.onEndReached. onEndReached does not just trigger the callback at the end of the playlist but actually at the end of each track. This should have been better documented. --- app.iced | 1 - 1 file changed, 1 deletion(-) diff --git a/app.iced b/app.iced index e71ef00..a05428b 100644 --- a/app.iced +++ b/app.iced @@ -71,7 +71,6 @@ ts3clientService.on "started", (ts3proc) => 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.onEndReached = () => ts3query.sendtextmessage 2, 0, "End of playlist reached." vlc.onStopped = () => ts3query.sendtextmessage 2, 0, "Stopped." ts3query.currentScHandlerID = 1 From adeccf6d57da68d61f98190d4a102f7a68486198 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 29 Oct 2015 02:23:53 +0100 Subject: [PATCH 055/115] Allow "vol" to display current volume if no arguments were given. --- app.iced | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app.iced b/app.iced index a05428b..b08eea6 100644 --- a/app.iced +++ b/app.iced @@ -290,7 +290,14 @@ ts3clientService.on "started", (ts3proc) => when "stop" vlc.stop() when "vol" - vol = parseInt paramline + inputBB = paramline.trim() + input = (removeBB paramline).trim() + + if inputBB.length <= 0 + ts3query.sendtextmessage args.targetmode, invoker.id, "Volume is currently set to #{vlc.audio.volume}%." + return + + vol = parseInt input if paramline.trim().length <= 0 or vol == NaN 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." From c20462272f370ed1e88a5227860583fe3e0559a3 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 29 Oct 2015 02:24:11 +0100 Subject: [PATCH 056/115] Stop audio playback when shutting down TS3Bot. --- services/vlc.iced | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/vlc.iced b/services/vlc.iced index b52ee83..e5e1762 100644 --- a/services/vlc.iced +++ b/services/vlc.iced @@ -36,6 +36,8 @@ module.exports = class VLCService extends services.Service cb?() return + @_instance.stop() + # TODO: Is there even a proper way to shut this down? @_instance = null From 82c19a2196770c463d8c94fc9e5842dfe8697c8d Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 29 Oct 2015 03:08:44 +0100 Subject: [PATCH 057/115] Added explicit process.exit after app shutdown. Since introducing WebChimera.js the bot is no longer shutting down by itself after all services are shut down since the instances that WebChimera.js generates would still be alive after deletion. There is no way to get around this except if WebChimera.js reveals an explicit release function which it doesn't. The only release function gets called before the VM gets killed. --- app.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app.js b/app.js index 325d330..ec938f7 100644 --- a/app.js +++ b/app.js @@ -33,6 +33,7 @@ doShutdownAsync = function(cb) { process.on("uncaughtException", function(err) { log.error("Shutting down due to an uncaught exception!", err); app.shutdownSync(); + process.exit(0xFF); }); process.on("exit", function(e) { @@ -43,24 +44,29 @@ process.on("exit", function(e) { process.on("SIGTERM", function(e) { log.debug("Caught SIGTERM signal"); app.shutdown(); + process.exit(0); }); process.on("SIGINT", function() { log.debug("Caught SIGINT signal"); app.shutdown(); + process.exit(0); }); process.on("SIGHUP", function() { log.debug("Caught SIGHUP signal"); app.shutdown(); + process.exit(0); }); process.on("SIGQUIT", function() { log.debug("Caught SIGQUIT signal"); app.shutdown(); + process.exit(0); }); process.on("SIGABRT", function() { log.debug("Caught SIGABRT signal"); app.shutdown(); + process.exit(0); }); From f650c3296b1ffdcf538cc5ffa690408282c5868c Mon Sep 17 00:00:00 2001 From: Icedream Jenkins Date: Mon, 2 Nov 2015 12:18:36 +0100 Subject: [PATCH 058/115] Make xdotool optional. --- require_bin.iced | 12 +++++++++--- x11.iced | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/require_bin.iced b/require_bin.iced index 66158f2..1a6a77f 100644 --- a/require_bin.iced +++ b/require_bin.iced @@ -2,7 +2,9 @@ which = require("which").sync path = require "path" log = require("./logger")("RequireBin") -module.exports = (binName) => +module.exports = (binName, doErrorIfNotFound) => + doErrorIfNotFound = true unless doErrorIfNotFound? + # check if xvfb is findable from here if path.resolve(binName) == path.normalize(binName) # this is an absolute path @@ -14,5 +16,9 @@ module.exports = (binName) => log.debug "#{binName} detected:", binPath return binPath catch err - log.error "#{binName} could not be found.", err - throw new Error "#{binName} could not be found." + if doErrorIfNotFound + log.error "#{binName} could not be found." + throw new Error "#{binName} could not be found." + else + log.warn "#{binName} could not be found." + return null diff --git a/x11.iced b/x11.iced index c679dca..05ce1f1 100644 --- a/x11.iced +++ b/x11.iced @@ -6,13 +6,18 @@ services = require("./services") StreamSplitter = require("stream-splitter") require_bin = require("./require_bin") -xdotoolBinPath = require_bin "xdotool" +xdotoolBinPath = require_bin "xdotool", false # Just some tools to work with the X11 windows module.exports = getWindowIdByProcessId: (pid, cb) => wid = null + # Return null instantly if xdotool is not available + if not xdotoolBinPath? + cb? new Error "xdotool is not available" + return + # We provide --name due to the bug mentioned at https://github.com/jordansissel/xdotool/issues/14 xdoproc = spawn xdotoolBinPath, [ "search", "--any", "--pid", pid, "--name", "xdosearch" ], env: @@ -43,6 +48,11 @@ module.exports = getWindowIdByProcessIdSync: (pid) => Sync() => @getWindowIdByProcessId.sync @, pid sendKeys: (wid, keys, cb) => + # Do not bother trying if xdotool is not available + if not xdotoolBinPath? + cb? new Error "xdotool not available." + return + # blackbox needs to be running for windowactivate to work blackboxService = services.find("BlackBox") if blackboxService.state != "started" @@ -73,4 +83,4 @@ module.exports = err = new Error "Failed to send keys." cb? err - sendKeysSync: (keys) => Sync () => @sendKeys.sync @, keys \ No newline at end of file + sendKeysSync: (keys) => Sync () => @sendKeys.sync @, keys From b6a70ebdc5af5c6a48220fa5c45a03ba4d798940 Mon Sep 17 00:00:00 2001 From: Icedream Jenkins Date: Mon, 2 Nov 2015 12:20:40 +0100 Subject: [PATCH 059/115] Delay error that Xvfb is not available so main code can handle it instead. --- services/xvfb.iced | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/xvfb.iced b/services/xvfb.iced index b3f1027..4edc696 100644 --- a/services/xvfb.iced +++ b/services/xvfb.iced @@ -4,11 +4,15 @@ config = require("../config") services = require("../services") require_bin = require("../require_bin") -require_bin "Xvfb" +xvfbPath = require_bin "Xvfb", false module.exports = class XvfbService extends services.Service constructor: -> super "Xvfb", start: (cb) -> + if not xvfbPath? + cb? new Error "Xvfb is not available." + return + if @instance cb? null, @instance return From ae4288d44a22c0fc31c0ced8fe03b05b4c516141 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Mon, 2 Nov 2015 12:42:37 +0100 Subject: [PATCH 060/115] Remove left-over explicit API shutdown code. The code removed in this commit was not removed in the process of migrating to the new HTTP-server-less code. It causes additional errors and is generally useless now. --- app.iced | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app.iced b/app.iced index b08eea6..d4e6c52 100644 --- a/app.iced +++ b/app.iced @@ -18,13 +18,6 @@ removeBB = (str) -> str.replace /\[(\w+)[^\]]*](.*?)\[\/\1]/g, "$2" module.exports = shutdown: (cb) => - apiService = services.find("api") - if apiService and apiService.state == "started" - await apiService.stop defer(err) - if err - cb? new Error "Could not stop API" - return - ts3clientService = services.find("ts3client") if ts3clientService and ts3clientService.state == "started" await ts3clientService.stop defer(err) From ee9bd9c0fc1f8fe2d0b446567a4695ce5de0ec1e Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Mon, 2 Nov 2015 12:44:12 +0100 Subject: [PATCH 061/115] Delay Blackbox availability handling. Allow the main code to handle this case instead. --- services/blackbox.iced | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/blackbox.iced b/services/blackbox.iced index e73fdb4..a645200 100644 --- a/services/blackbox.iced +++ b/services/blackbox.iced @@ -4,7 +4,7 @@ services = require("../services") StreamSplitter = require("stream-splitter") require_bin = require("../require_bin") -blackboxBinPath = require_bin "blackbox" +blackboxBinPath = require_bin "blackbox", false module.exports = class BlackBoxService extends services.Service dependencies: [ @@ -12,6 +12,10 @@ module.exports = class BlackBoxService extends services.Service ] constructor: -> super "BlackBox", start: (cb) -> + if not blackboxBinPath? + cb? new Error "Blackbox not available." + return + if @process cb? null, @process return From ce801fecbb3490ceafa9f833495fa8b8ddcafc05 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Mon, 2 Nov 2015 12:45:27 +0100 Subject: [PATCH 062/115] Use custom XDG runtime dirs for better isolation. --- app.iced | 4 ++++ package.json | 1 + services/ts3client.iced | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/app.iced b/app.iced index d4e6c52..fc4fe4d 100644 --- a/app.iced +++ b/app.iced @@ -8,6 +8,7 @@ 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 @@ -34,6 +35,9 @@ module.exports = 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" + # PulseAudio daemon await services.find("pulseaudio").start defer err if err diff --git a/package.json b/package.json index 78ac299..9f31387 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "stream-splitter": "^0.3.2", "string.prototype.startswith": "^0.2.0", "sync": "^0.2.5", + "temp": "^0.8.3", "valid-url": "^1.0.9", "webchimera.js": "^0.1.38", "which": "^1.1.2", diff --git a/services/ts3client.iced b/services/ts3client.iced index ca662c4..093ba57 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -22,6 +22,10 @@ module.exports = class TS3ClientService extends services.Service ] constructor: -> super "TS3Client", start: (args, cb) => + if not process.env.XDG_RUNTIME_DIR? or process.env.XDG_RUNTIME_DIR.trim() == "" + cb? new Error "XDG runtime directory needs to be set." + return + if typeof args == "function" cb = args args = null From 2e246a1a9888bd9318dbb3f5c9f800e0c964c330 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Mon, 2 Nov 2015 12:46:30 +0100 Subject: [PATCH 063/115] Do not require an isolated GUI anymore. This commit allows the TS3Bot to boot in an environment where there is no way to boot an isolated graphical environment. In that case TS3Bot will try to use the already existing display and the desktop manager that runs on it already. This adds some undefined error cases which still need to be found and fixed, so be warned: This commit is very, VERY experimental! --- services/pulseaudio.iced | 1 - services/ts3client.iced | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/pulseaudio.iced b/services/pulseaudio.iced index fc10f9b..3f1c1eb 100644 --- a/services/pulseaudio.iced +++ b/services/pulseaudio.iced @@ -10,7 +10,6 @@ pacmdPath = require_bin "pacmd" module.exports = class PulseAudioService extends services.Service dependencies: [ - "xvfb" ] constructor: -> super "PulseAudio", start: (cb) -> diff --git a/services/ts3client.iced b/services/ts3client.iced index 093ba57..a179b09 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -16,8 +16,6 @@ ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3cl module.exports = class TS3ClientService extends services.Service dependencies: [ - "xvfb", - "blackbox", "pulseaudio" ] constructor: -> super "TS3Client", @@ -26,6 +24,10 @@ module.exports = class TS3ClientService extends services.Service cb? new Error "XDG runtime directory needs to be set." return + if not process.env.DISPLAY? or process.env.DISPLAY.trim() == "" + cb? new Error "There is no display to run TeamSpeak3 on." + return + if typeof args == "function" cb = args args = null From 8afc967796c19400555cb37fc73daf4566a8aee7 Mon Sep 17 00:00:00 2001 From: icedream Date: Mon, 2 Nov 2015 13:26:49 +0100 Subject: [PATCH 064/115] Print more details on service startup failures. --- app.iced | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app.iced b/app.iced index fc4fe4d..eba9cd2 100644 --- a/app.iced +++ b/app.iced @@ -41,13 +41,13 @@ process.env.XDG_RUNTIME_DIR = temp.mkdirSync "ts3bot-xdg" # PulseAudio daemon await services.find("pulseaudio").start defer err if err - log.warn "PulseAudio could not start up, audio may not act as expected!" + 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!" + log.warn "VLC could not start up!", err await module.exports.shutdown defer() process.exit 1 @@ -316,6 +316,6 @@ ts3clientService.on "started", (ts3proc) => await ts3clientService.start [ config.get("ts3-server") ], defer(err, ts3proc) if err - log.error "TeamSpeak3 could not start, shutting down." + log.error "TeamSpeak3 could not start, shutting down.", err await module.exports.shutdown defer() process.exit 1 From 02e6f0c4891474fb0497405e3223ae9ad91531a5 Mon Sep 17 00:00:00 2001 From: icedream Date: Mon, 2 Nov 2015 13:38:09 +0100 Subject: [PATCH 065/115] Boot Xvfb via app.iced. --- app.iced | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app.iced b/app.iced index eba9cd2..b4396cb 100644 --- a/app.iced +++ b/app.iced @@ -38,6 +38,16 @@ module.exports = # 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 From b1cc9f6255d8130e861d8f7b901a5d45df3ed47a Mon Sep 17 00:00:00 2001 From: icedream Date: Mon, 2 Nov 2015 15:42:37 +0100 Subject: [PATCH 066/115] Fix pipes from TS3 query not being decoded, fixes #22. --- ts3query.iced | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts3query.iced b/ts3query.iced index 3b1b076..4ff1230 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -13,7 +13,7 @@ parserLog = getLogger "parser" escape = (value) => value.toString()\ .replace(/\\/g, "\\\\")\ .replace(/\//g, "\\/")\ - .replace(/\|/g, "\\|")\ + .replace(/\|/g, "\\p")\ .replace(/\n/g, "\\n")\ .replace(/\r/g, "\\r")\ .replace(/\t/g, "\\t")\ @@ -24,7 +24,7 @@ unescape = (value) => value.toString()\ .replace(/\\t/g, "\t")\ .replace(/\\r/g, "\r")\ .replace(/\\n/g, "\n")\ - .replace(/\\\|/g, "|")\ + .replace(/\\p/g, "|")\ .replace(/\\\//g, "/")\ .replace(/\\\\/g, "\\") From 2ee4dc32c30e61e25f13a7062850f8481dfd86a1 Mon Sep 17 00:00:00 2001 From: icedream Date: Mon, 2 Nov 2015 16:06:34 +0100 Subject: [PATCH 067/115] Quick fix for the no-metadata crash, closes #23. --- app.iced | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.iced b/app.iced index b4396cb..828b76d 100644 --- a/app.iced +++ b/app.iced @@ -72,8 +72,9 @@ ts3clientService.on "started", (ts3proc) => # VLC event handling vlc.onPlaying = () => + # TODO: Check why info is sometimes null, something must be wrong with the "add"/"play" commands here! info = vlcMediaInfo[vlc.playlist.items[vlc.playlist.currentItem].mrl] - ts3query.sendtextmessage 2, 0, "Now playing [URL=#{info.originalUrl}]#{info.title}[/URL]." + ts3query.sendtextmessage 2, 0, "Now playing [URL=#{info?.originalUrl or vlc.playlist.items[vlc.playlist.currentItem].mrl}]#{info?.title or vlc.playlist.items[vlc.playlist.currentItem].mrl}[/URL]." vlc.onPaused = () => ts3query.sendtextmessage 2, 0, "Paused." vlc.onForward = () => ts3query.sendtextmessage 2, 0, "Fast-forwarding..." vlc.onBackward = () => ts3query.sendtextmessage 2, 0, "Rewinding..." From 4d7550865e59a95e7bae73f2c1fd7f99ecdfb8a0 Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 3 Nov 2015 01:28:30 +0100 Subject: [PATCH 068/115] Store wanted volume in a variable and set on VLC when playback starts. This gets rid of a configuration issue in the Docker image where VLC would reset the volume to 100% on each new track. --- app.iced | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app.iced b/app.iced index 828b76d..905cd55 100644 --- a/app.iced +++ b/app.iced @@ -61,6 +61,9 @@ if 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 = {} @@ -72,6 +75,9 @@ ts3clientService.on "started", (ts3proc) => # VLC event handling vlc.onPlaying = () => + # Restore audio volume + vlc.audio.volume = vlcVolume + # TODO: Check why info is sometimes null, something must be wrong with the "add"/"play" commands here! info = vlcMediaInfo[vlc.playlist.items[vlc.playlist.currentItem].mrl] ts3query.sendtextmessage 2, 0, "Now playing [URL=#{info?.originalUrl or vlc.playlist.items[vlc.playlist.currentItem].mrl}]#{info?.title or vlc.playlist.items[vlc.playlist.currentItem].mrl}[/URL]." @@ -302,7 +308,7 @@ ts3clientService.on "started", (ts3proc) => input = (removeBB paramline).trim() if inputBB.length <= 0 - ts3query.sendtextmessage args.targetmode, invoker.id, "Volume is currently set to #{vlc.audio.volume}%." + ts3query.sendtextmessage args.targetmode, invoker.id, "Volume is currently set to #{vlcVolume}%." return vol = parseInt input @@ -311,7 +317,7 @@ ts3clientService.on "started", (ts3proc) => 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 = vol + vlc.audio.volume = vlcVolume = vol ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%." when "changenick" nick = if paramline.length > params[0].length then paramline else params[0] From cbb6204b527bec9b04ac41204728753c153851fc Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 3 Nov 2015 03:55:08 +0100 Subject: [PATCH 069/115] Fix process exiting before all services shut down. Fixes #24 and potentially #25. --- app.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/app.js b/app.js index ec938f7..8e7cd8f 100644 --- a/app.js +++ b/app.js @@ -43,30 +43,35 @@ process.on("exit", function(e) { process.on("SIGTERM", function(e) { log.debug("Caught SIGTERM signal"); - app.shutdown(); - process.exit(0); + app.shutdown(function() { + process.exit(0); + }); }); process.on("SIGINT", function() { log.debug("Caught SIGINT signal"); - app.shutdown(); - process.exit(0); + app.shutdown(function() { + process.exit(0); + }); }); process.on("SIGHUP", function() { log.debug("Caught SIGHUP signal"); - app.shutdown(); - process.exit(0); + app.shutdown(function() { + process.exit(0); + }); }); process.on("SIGQUIT", function() { log.debug("Caught SIGQUIT signal"); - app.shutdown(); - process.exit(0); + app.shutdown(function() { + process.exit(0); + }); }); process.on("SIGABRT", function() { log.debug("Caught SIGABRT signal"); - app.shutdown(); - process.exit(0); + app.shutdown(function() { + process.exit(0); + }); }); From 655659e254940082a3629631fbf8e698bf4689da Mon Sep 17 00:00:00 2001 From: icedream Date: Tue, 3 Nov 2015 03:59:35 +0100 Subject: [PATCH 070/115] Prevent crash on shutdown caused by VLC stop. The code tried to send a "Stopped." message to TeamSpeak3 when VLC shut down. However VLC gets shut down after TeamSpeak3 which causes a null reference on the query interface. --- app.iced | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app.iced b/app.iced index 905cd55..e5aa742 100644 --- a/app.iced +++ b/app.iced @@ -73,6 +73,9 @@ ts3clientService = services.find("ts3client") ts3clientService.on "started", (ts3proc) => ts3query = ts3clientService.query + ts3clientService.once "stopped", () => + ts3query = undefined + # VLC event handling vlc.onPlaying = () => # Restore audio volume @@ -80,12 +83,12 @@ ts3clientService.on "started", (ts3proc) => # TODO: Check why info is sometimes null, something must be wrong with the "add"/"play" commands here! info = vlcMediaInfo[vlc.playlist.items[vlc.playlist.currentItem].mrl] - ts3query.sendtextmessage 2, 0, "Now playing [URL=#{info?.originalUrl or vlc.playlist.items[vlc.playlist.currentItem].mrl}]#{info?.title or vlc.playlist.items[vlc.playlist.currentItem].mrl}[/URL]." - vlc.onPaused = () => ts3query.sendtextmessage 2, 0, "Paused." - vlc.onForward = () => ts3query.sendtextmessage 2, 0, "Fast-forwarding..." - vlc.onBackward = () => ts3query.sendtextmessage 2, 0, "Rewinding..." + ts3query?.sendtextmessage 2, 0, "Now playing [URL=#{info?.originalUrl or vlc.playlist.items[vlc.playlist.currentItem].mrl}]#{info?.title or vlc.playlist.items[vlc.playlist.currentItem].mrl}[/URL]." + 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." + vlc.onStopped = () => ts3query?.sendtextmessage 2, 0, "Stopped." ts3query.currentScHandlerID = 1 ts3query.mydata = {} From 59bdee92296c49090c298b0193d250394c36bbbc Mon Sep 17 00:00:00 2001 From: Icedream Date: Tue, 3 Nov 2015 09:28:15 +0100 Subject: [PATCH 071/115] Fix update routine in ts3settings.iced. Fixes #30. Signed-off-by: icedream --- ts3settings.iced | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ts3settings.iced b/ts3settings.iced index 81f9e3b..db03705 100644 --- a/ts3settings.iced +++ b/ts3settings.iced @@ -214,12 +214,19 @@ module.exports = class SettingsFile settingsObj.log.silly "Requested update of #{id.id}" for own index, identity of settingsObj.identities if identity.id == id.id + # remove functions from this object + cleanIdentity = merge @ + for own k, v of cleanIdentity + if typeof v == "function" + delete cleanIdentity[k] + + # now this is our new identity object! settingsObj.log.silly "Updating identity #{id.id}" - settingsObj.identities[index] = merge identity, id + settingsObj.identities[index] = cleanIdentity return remove: () -> for own index, identity of settingsObj.identities if identity.id == id.id delete settingsObj.identities[index] break - # TODO: Select another identity as default \ No newline at end of file + # TODO: Select another identity as default From d403f93e76ed25fadc2f2a34016e2e3858f73082 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Tue, 3 Nov 2015 10:36:06 +0100 Subject: [PATCH 072/115] Fix wrong saving of last used service arguments. Affects #31. --- service_template.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service_template.iced b/service_template.iced index 5f863d6..b155c32 100644 --- a/service_template.iced +++ b/service_template.iced @@ -78,7 +78,7 @@ module.exports = class Service extends EventEmitter if not quiet @log.info "Started #{@name}" - @_lastArgs = args + @_lastArgs = serviceArgs @state = "started" @emit "started", service From 95f939de53793a9722b8f26be2f6bfa0021bcaf2 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Tue, 3 Nov 2015 10:37:05 +0100 Subject: [PATCH 073/115] Rewrite "apply" calls with easier syntax. --- service_template.iced | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service_template.iced b/service_template.iced index b155c32..5ad2630 100644 --- a/service_template.iced +++ b/service_template.iced @@ -19,7 +19,7 @@ module.exports = class Service extends EventEmitter state: "stopped" - start: () => @_start.apply @, [ false ].concat Array.prototype.slice.call(arguments) + start: () => @_start false, arguments... startSync: () => Sync () => @start.sync @ @@ -85,7 +85,7 @@ module.exports = class Service extends EventEmitter cb? null, service - stop: () => @_stop.apply @, [ false ].concat Array.prototype.slice.call(arguments) + stop: () => @_stop false, arguments... stopSync: () => Sync () => @stop.sync @ @@ -151,4 +151,4 @@ module.exports = class Service extends EventEmitter cb? err - restartSync: () => Sync () => @restart.sync @ \ No newline at end of file + restartSync: () => Sync () => @restart.sync @ From eaa0d0076710758a31793c3df404ec2b64650a90 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Tue, 3 Nov 2015 11:23:20 +0100 Subject: [PATCH 074/115] Create README. Closes #29. README describes a bit about this repository and how to run this code outside of Docker for testing purposes. More information will be added in the future and as requested. Ideas and suggestions are warmly appreciated! --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2f953a --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# TS3Bot + +A new and easy way to set up your own TeamSpeak 3 bot! + +This repository contains the Node.js/IcedCoffeeScript code needed for the TS3Bot Docker image. + +## Running TS3Bot without Docker + +Basically these instructions are derived from [the Docker build files](https://github.com/icedream/ts3bot-docker). +You can adopt these depending on your OS (the Docker image uses Debian Jessie, so the instructions below are for +that OS). + +We assume that we want to run the "develop" branch of TS3Bot here. You can easily replace "develop" with another branch you want to run, like "master" for the stable code. + +Commands being run as your own user are marked with `$` and commands being run as root are marked with `#`. + +- Install the dependencies: + + $ apt-get install blackbox xvfb xdotool pulseaudio pulseaudio-utils cmake libvlc-dev vlc-plugin-pulse + +- Create a new user to run TS3Bot on - do not run this on your own user if you use TeamSpeak3 on it as that will overwrite your configuration! + + # adduser --system --disabled-login --disabled-password ts3bot + +- Download and unpack TeamSpeak3 client for your platform into a folder accessible by the TS3Bot user. Only read access is required. Replace `3.0.18.2` with whatever version of TeamSpeak3 you prefer to install, usually that is the most recent one. Accept the license that shows up in the process. + + # wget -Ots3client.run http://dl.4players.de/ts/releases/3.0.18.2/TeamSpeak3-Client-linux_$(uname -p)-3.0.18.2.run + # chmod +x ts3client.run + # ./ts3client.run --target ~ts3bot/ts3client + # rm ts3client.run + +- Download the TS3Bot control application into your TS3Bot user's home folder. The TS3Bot user itself only needs read access to the code. You can do this in two ways: + o By downloading the tar.gz archive from GitHub and unpacking it. + + # wget -q -O- https://github.com/icedream/ts3bot-control/archive/develop.tar.gz | tar xz -C ~ts3bot + + o By cloning the Git repository from GitHub. + + # git clone https://github.com/icedream/ts3bot-control -b develop ~ts3bot/ts3bot-control-develop + +- Install the Node.js dependencies using `npm`. Note how a simple `npm install` will install the wrong version of WebChimera.js and you need to provide it with correct Node.js information (environment variables `npm_config_wcjs_runtime` and `npm_config_wcjs_runtime_version`) like this: + + # cd ~ts3bot/ts3bot-control-develop + # npm_config_wcjs_runtime="node" npm_config_wcjs_runtime_version="$(node --version | tr -d 'v')" npm install + +- Now set up your TS3Bot configuration files in your TS3Bot user's home folder. For this create a folder `.ts3bot` in the home directory and put a `config.json` with your configuration there. The most minimal configuration consists of: + o `identity-path` - The path to your identity INI file, export a newly generated identity from your own TeamSpeak3 client for that. + o `ts3-install-path` - The path where you installed the TeamSpeak3 client to, you can skip this if you used exactly the same path as in the instructions above. + o `ts3-server` - The URL to the server/channel you want the bot to connect to, you can get from your own TS3 client via "Extras" > "Invite friend", select the checkbox "Channel" and select "ts3server link" as invitation type. + +Running the bot can finally be done like this: + + $ sudo -u ts3bot -H node ~ts3bot/ts3bot-control-develop + +You can provide your configuration as command line arguments instead if you want, that can be useful for just temporary configuration you want to test. For that just append the configuration options to the command line above and prefix every command line option with `--`, for example for `ts3-install-path` you would write `--ts3-install-path=/your/path/to/ts3client` From 8fe17abdf8cec65c69cd5d4c479f910301cfe053 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 4 Nov 2015 19:39:25 +0100 Subject: [PATCH 075/115] Add compatibility with other window managers using x-window-manager. --- services.iced | 2 +- services/{blackbox.iced => xwm.iced} | 20 ++++++++++---------- x11.iced | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) rename services/{blackbox.iced => xwm.iced} (73%) diff --git a/services.iced b/services.iced index 2d6168a..f3e8399 100644 --- a/services.iced +++ b/services.iced @@ -60,7 +60,7 @@ services = [ new(require "./services/ts3client") new(require "./services/vlc") new(require "./services/xvfb") - new(require "./services/blackbox") + new(require "./services/xwm") ] services.sort require("./service_depcomparer") # sort services by dependency for service in services diff --git a/services/blackbox.iced b/services/xwm.iced similarity index 73% rename from services/blackbox.iced rename to services/xwm.iced index a645200..757a135 100644 --- a/services/blackbox.iced +++ b/services/xwm.iced @@ -1,19 +1,19 @@ spawn = require("child_process").spawn -log = require("../logger")("BlackBox") +log = require("../logger")("XWindowManager") services = require("../services") StreamSplitter = require("stream-splitter") require_bin = require("../require_bin") -blackboxBinPath = require_bin "blackbox", false +xwmBinPath = require_bin "x-window-manager", false -module.exports = class BlackBoxService extends services.Service +module.exports = class XWindowManagerService extends services.Service dependencies: [ "xvfb" ] - constructor: -> super "BlackBox", + constructor: -> super "XWindowManager", start: (cb) -> - if not blackboxBinPath? - cb? new Error "Blackbox not available." + if not xwmBinPath? + cb? new Error "A window manager not available." return if @process @@ -29,7 +29,7 @@ module.exports = class BlackBoxService extends services.Service if err throw new Error "Dependency xvfb failed." - proc = spawn blackboxBinPath, [ "-rc", "/dev/null" ], + proc = spawn xwmBinPath, [ "-rc", "/dev/null" ], stdio: ['ignore', 'pipe', 'pipe'] detached: true env: @@ -55,9 +55,9 @@ module.exports = class BlackBoxService extends services.Service return if not calledCallback calledCallback = true - @log.warn "BlackBox terminated unexpectedly during startup." - cb? new Error "BlackBox terminated unexpectedly." - @log.warn "BlackBox terminated unexpectedly, restarting." + @log.warn "Window manager terminated unexpectedly during startup." + cb? new Error "Window manager terminated unexpectedly." + @log.warn "Window manager terminated unexpectedly, restarting." doStart() @process = proc diff --git a/x11.iced b/x11.iced index 05ce1f1..948916f 100644 --- a/x11.iced +++ b/x11.iced @@ -53,12 +53,12 @@ module.exports = cb? new Error "xdotool not available." return - # blackbox needs to be running for windowactivate to work - blackboxService = services.find("BlackBox") - if blackboxService.state != "started" - await blackboxService.start defer(err) + # a window manager needs to be running for windowactivate to work + xwmService = services.find("XWindowManager") + if xwmService.state != "started" + await xwmService.start defer(err) if err - cb? new Error "Could not start compatible window manager." + cb? new Error "Could not start a window manager." return xdoproc = spawn xdotoolBinPath, [ "windowactivate", "--sync", wid, "key", "--clearmodifiers", "--delay", "100" ].concat(keys), From 39f5ec1ec0725634c46a64c5f0cb1a3ea6a9fd91 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 4 Nov 2015 22:08:52 +0100 Subject: [PATCH 076/115] Also check vol against null. --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index e5aa742..b2f947c 100644 --- a/app.iced +++ b/app.iced @@ -316,7 +316,7 @@ ts3clientService.on "started", (ts3proc) => vol = parseInt input - if paramline.trim().length <= 0 or vol == NaN or vol > 200 or vol < 0 + if paramline.trim().length <= 0 or vol == NaN or vol == null 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 From bfb0ff154b6db35a40b0915be06c2cd925c51414 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 4 Nov 2015 23:23:34 +0100 Subject: [PATCH 077/115] Remove left-over parameters for blackbox for compatibility with other WMs. --- services/xwm.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/xwm.iced b/services/xwm.iced index 757a135..6be4219 100644 --- a/services/xwm.iced +++ b/services/xwm.iced @@ -29,7 +29,7 @@ module.exports = class XWindowManagerService extends services.Service if err throw new Error "Dependency xvfb failed." - proc = spawn xwmBinPath, [ "-rc", "/dev/null" ], + proc = spawn xwmBinPath, [], stdio: ['ignore', 'pipe', 'pipe'] detached: true env: From 2b7da8705f1fbb9125f6aa4c1ac84fdca9cb0113 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 4 Nov 2015 23:33:44 +0100 Subject: [PATCH 078/115] Require a DISPLAY to be set instead of Xvfb to be running for the window manager. --- services/xwm.iced | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/services/xwm.iced b/services/xwm.iced index 6be4219..6114e2b 100644 --- a/services/xwm.iced +++ b/services/xwm.iced @@ -8,7 +8,6 @@ xwmBinPath = require_bin "x-window-manager", false module.exports = class XWindowManagerService extends services.Service dependencies: [ - "xvfb" ] constructor: -> super "XWindowManager", start: (cb) -> @@ -16,6 +15,14 @@ module.exports = class XWindowManagerService extends services.Service cb? new Error "A window manager not available." return + if not process.env.XDG_RUNTIME_DIR? or process.env.XDG_RUNTIME_DIR.trim() == "" + cb? new Error "XDG runtime directory needs to be set." + return + + if not process.env.DISPLAY? or process.env.DISPLAY.trim() == "" + cb? new Error "There is no display to run TeamSpeak3 on." + return + if @process cb? null, @process return @@ -25,10 +32,6 @@ module.exports = class XWindowManagerService extends services.Service proc = null doStart = null doStart = () => - await services.find("xvfb").start defer(err) - if err - throw new Error "Dependency xvfb failed." - proc = spawn xwmBinPath, [], stdio: ['ignore', 'pipe', 'pipe'] detached: true From 091f77f352d4b0ca40ee8ee8f802c0630b7a8141 Mon Sep 17 00:00:00 2001 From: icedream Date: Wed, 4 Nov 2015 23:39:25 +0100 Subject: [PATCH 079/115] Remove code checking the nickname length. --- app.iced | 3 --- 1 file changed, 3 deletions(-) diff --git a/app.iced b/app.iced index b2f947c..fc93a2f 100644 --- a/app.iced +++ b/app.iced @@ -324,9 +324,6 @@ ts3clientService.on "started", (ts3proc) => ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%." when "changenick" nick = if paramline.length > params[0].length then paramline else params[0] - if nick.length < 3 or nick.length > 30 - ts3query.sendtextmessage args.targetmode, invoker.id, "Invalid nickname." - return Sync () => try ts3query.clientupdate.sync ts3query, { client_nickname: nick } From d40ffdc76b91fd056946f78d2a04bcb01f6744e8 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 00:06:58 +0100 Subject: [PATCH 080/115] Fix #34 by removing useless length check for "changenick" command. --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index fc93a2f..bbf599f 100644 --- a/app.iced +++ b/app.iced @@ -323,7 +323,7 @@ ts3clientService.on "started", (ts3proc) => vlc.audio.volume = vlcVolume = vol ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%." when "changenick" - nick = if paramline.length > params[0].length then paramline else params[0] + nick = paramline Sync () => try ts3query.clientupdate.sync ts3query, { client_nickname: nick } From 9929807cd35653fffb5038c7f8ca097c7eb4da08 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 00:07:25 +0100 Subject: [PATCH 081/115] Proper error message for nicknames that fail TS3Client checks. --- app.iced | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app.iced b/app.iced index bbf599f..e23010d 100644 --- a/app.iced +++ b/app.iced @@ -328,8 +328,11 @@ ts3clientService.on "started", (ts3proc) => try ts3query.clientupdate.sync ts3query, { client_nickname: nick } catch err - ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out." 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 From 7077c8425de69f5fd9af52c1f446e1daa999c483 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 00:17:29 +0100 Subject: [PATCH 082/115] Do not handle messages that don't have a proper text. Fixes #34. --- app.iced | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.iced b/app.iced index e23010d..4d4f4a4 100644 --- a/app.iced +++ b/app.iced @@ -154,6 +154,9 @@ ts3clientService.on "started", (ts3proc) => 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 From b71c2be88ff76445d2859047dfbdd5c117e74523 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 00:32:07 +0100 Subject: [PATCH 083/115] Fix NaN check for "vol" command. --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index 4d4f4a4..af3d2ba 100644 --- a/app.iced +++ b/app.iced @@ -319,7 +319,7 @@ ts3clientService.on "started", (ts3proc) => vol = parseInt input - if paramline.trim().length <= 0 or vol == NaN or vol == null or vol > 200 or vol < 0 + 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 From 38c248e6433b0328efcb07ca0639490d5ad6a45f Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 02:49:31 +0100 Subject: [PATCH 084/115] Bumping version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f31387..8d3fe8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts3bot", - "version": "0.2.1", + "version": "0.3.0", "description": "Allows running TeamSpeak3 as a bot for all kinds of media (local music/videos/streams/YouTube/...) without the need for a real GUI to exist.", "main": "app.js", "keywords": [ From 1b1f4c0f373f25f322d458c2ac35999cadb1bff4 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 03:02:03 +0100 Subject: [PATCH 085/115] Rewrite vlc.onPlaying handler, fixes #35. --- app.iced | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app.iced b/app.iced index af3d2ba..3e6c834 100644 --- a/app.iced +++ b/app.iced @@ -78,12 +78,21 @@ ts3clientService.on "started", (ts3proc) => # VLC event handling vlc.onPlaying = () => - # Restore audio volume - vlc.audio.volume = vlcVolume + 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 - # TODO: Check why info is sometimes null, something must be wrong with the "add"/"play" commands here! - info = vlcMediaInfo[vlc.playlist.items[vlc.playlist.currentItem].mrl] - ts3query?.sendtextmessage 2, 0, "Now playing [URL=#{info?.originalUrl or vlc.playlist.items[vlc.playlist.currentItem].mrl}]#{info?.title or vlc.playlist.items[vlc.playlist.currentItem].mrl}[/URL]." vlc.onPaused = () => ts3query?.sendtextmessage 2, 0, "Paused." vlc.onForward = () => ts3query?.sendtextmessage 2, 0, "Fast-forwarding..." vlc.onBackward = () => ts3query?.sendtextmessage 2, 0, "Rewinding..." From 003ec4876352cf70f471d286543a88a3210df7a3 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 03:02:57 +0100 Subject: [PATCH 086/115] Provide "ts3bot" global executable. --- app.js | 2 ++ package.json | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app.js b/app.js index 8e7cd8f..26af737 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + require("iced-coffee-script/register"); Sync = require("sync"); diff --git a/package.json b/package.json index 8d3fe8a..f2fba59 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.3.0", "description": "Allows running TeamSpeak3 as a bot for all kinds of media (local music/videos/streams/YouTube/...) without the need for a real GUI to exist.", "main": "app.js", + "bin": { + "ts3bot": "app.js" + }, "keywords": [ "teamspeak", "teamspeak3", From e8da814e5946996a8f2670fc5575c89b3a2bd42f Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 18:40:49 +0100 Subject: [PATCH 087/115] Implement "current" command, see #7. --- app.iced | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app.iced b/app.iced index 3e6c834..94c4f4c 100644 --- a/app.iced +++ b/app.iced @@ -186,6 +186,15 @@ ts3clientService.on "started", (ts3proc) => 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() From ed9e3605a76c225c59d950d2b4f7d7df0ef93d39 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 19:04:23 +0100 Subject: [PATCH 088/115] Validate "nickname" configuration option. Fixes #14. --- config.iced | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.iced b/config.iced index d4bf2de..92f9554 100644 --- a/config.iced +++ b/config.iced @@ -28,6 +28,9 @@ nconf.defaults if not nconf.get("ts3-server") throw new Error "You need to provide a TeamSpeak3 server URL (starts with ts3server:// and can be generated from any TS3 client GUI)." +if nconf.get("nickname")? and (nconf.get("nickname").length < 3 or nconf.get("nickname").length > 30 + throw new Error "Nickname must be between 3 and 30 characters long." + console.log "Configuration loaded." if nconf.get "dump-config" From e93b3f5fff953176ab26b9da360fbddea8c33721 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 19:28:32 +0100 Subject: [PATCH 089/115] Missing newline in config.iced. --- config.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.iced b/config.iced index 92f9554..db31045 100644 --- a/config.iced +++ b/config.iced @@ -38,4 +38,4 @@ if nconf.get "dump-config" process.exit 0 module.exports = merge true, nconf, - isProduction: -> @get("environment").toUpperCase() == "PRODUCTION" \ No newline at end of file + isProduction: -> @get("environment").toUpperCase() == "PRODUCTION" From 95fe3249cc0858b2fff2c6c7843b1320a1bdc846 Mon Sep 17 00:00:00 2001 From: icedream Date: Thu, 5 Nov 2015 19:41:48 +0100 Subject: [PATCH 090/115] Missing ) in config.iced. --- config.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.iced b/config.iced index db31045..5cddbfd 100644 --- a/config.iced +++ b/config.iced @@ -28,7 +28,7 @@ nconf.defaults if not nconf.get("ts3-server") throw new Error "You need to provide a TeamSpeak3 server URL (starts with ts3server:// and can be generated from any TS3 client GUI)." -if nconf.get("nickname")? and (nconf.get("nickname").length < 3 or nconf.get("nickname").length > 30 +if nconf.get("nickname")? and (nconf.get("nickname").length < 3 or nconf.get("nickname").length > 30) throw new Error "Nickname must be between 3 and 30 characters long." console.log "Configuration loaded." From ffbd2f98fed96113aa88e8764d40511cbfd88507 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Fri, 6 Nov 2015 22:39:58 +0100 Subject: [PATCH 091/115] Update README with sane install instructions. The original text has probably been written by a sleepy me from the past. --- README.md | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d2f953a..68ea475 100644 --- a/README.md +++ b/README.md @@ -12,36 +12,41 @@ that OS). We assume that we want to run the "develop" branch of TS3Bot here. You can easily replace "develop" with another branch you want to run, like "master" for the stable code. -Commands being run as your own user are marked with `$` and commands being run as root are marked with `#`. +We create a separate user "ts3bot" for this bot using the command below - do not run this on your own user if you use TeamSpeak3 on it as the bot will overwrite the configuration of the client later! -- Install the dependencies: + # adduser --disabled-login --disabled-password ts3bot - $ apt-get install blackbox xvfb xdotool pulseaudio pulseaudio-utils cmake libvlc-dev vlc-plugin-pulse +And we access the user's shell usually via: -- Create a new user to run TS3Bot on - do not run this on your own user if you use TeamSpeak3 on it as that will overwrite your configuration! + # sudo -u ts3bot -s -H - # adduser --system --disabled-login --disabled-password ts3bot +Commands being run as your bot user (`ts3bot`) are marked with `$` and commands being run as root are marked with `#`. -- Download and unpack TeamSpeak3 client for your platform into a folder accessible by the TS3Bot user. Only read access is required. Replace `3.0.18.2` with whatever version of TeamSpeak3 you prefer to install, usually that is the most recent one. Accept the license that shows up in the process. +- Install the dependencies, optionally add `git` if you are going to use the git client for cloning the source code later: - # wget -Ots3client.run http://dl.4players.de/ts/releases/3.0.18.2/TeamSpeak3-Client-linux_$(uname -p)-3.0.18.2.run - # chmod +x ts3client.run - # ./ts3client.run --target ~ts3bot/ts3client - # rm ts3client.run + # apt-get install node-dev blackbox xvfb xdotool pulseaudio pulseaudio-utils cmake libvlc-dev vlc-plugin-pulse + +- Download and unpack TeamSpeak3 client for your platform into a folder accessible by the TS3Bot user. Only read access is required. Replace `3.0.18.2` with whatever version of TeamSpeak3 you prefer to install, usually that is the most recent one. Accept the license that shows up in the process. Also replace `amd64` with `x86` if you're on a 32-bit system. + + $ cd ~ + $ wget -Ots3client.run http://dl.4players.de/ts/releases/3.0.18.2/TeamSpeak3-Client-linux_amd64-3.0.18.2.run + $ chmod +x ts3client.run + $ ./ts3client.run --target ~ts3bot/ts3client + $ rm ts3client.run - Download the TS3Bot control application into your TS3Bot user's home folder. The TS3Bot user itself only needs read access to the code. You can do this in two ways: o By downloading the tar.gz archive from GitHub and unpacking it. - # wget -q -O- https://github.com/icedream/ts3bot-control/archive/develop.tar.gz | tar xz -C ~ts3bot + $ wget -q -O- https://github.com/icedream/ts3bot-control/archive/develop.tar.gz | tar xz -C ~ o By cloning the Git repository from GitHub. - # git clone https://github.com/icedream/ts3bot-control -b develop ~ts3bot/ts3bot-control-develop + $ git clone https://github.com/icedream/ts3bot-control -b develop ~/ts3bot-control-develop - Install the Node.js dependencies using `npm`. Note how a simple `npm install` will install the wrong version of WebChimera.js and you need to provide it with correct Node.js information (environment variables `npm_config_wcjs_runtime` and `npm_config_wcjs_runtime_version`) like this: - # cd ~ts3bot/ts3bot-control-develop - # npm_config_wcjs_runtime="node" npm_config_wcjs_runtime_version="$(node --version | tr -d 'v')" npm install + $ cd ~ts3bot/ts3bot-control-develop + $ npm_config_wcjs_runtime="node" npm_config_wcjs_runtime_version="$(node --version | tr -d 'v')" npm install - Now set up your TS3Bot configuration files in your TS3Bot user's home folder. For this create a folder `.ts3bot` in the home directory and put a `config.json` with your configuration there. The most minimal configuration consists of: o `identity-path` - The path to your identity INI file, export a newly generated identity from your own TeamSpeak3 client for that. @@ -50,6 +55,6 @@ Commands being run as your own user are marked with `$` and commands being run a Running the bot can finally be done like this: - $ sudo -u ts3bot -H node ~ts3bot/ts3bot-control-develop + $ node ~/ts3bot-control-develop You can provide your configuration as command line arguments instead if you want, that can be useful for just temporary configuration you want to test. For that just append the configuration options to the command line above and prefix every command line option with `--`, for example for `ts3-install-path` you would write `--ts3-install-path=/your/path/to/ts3client` From 529bf84fcd9313e62df850f6c1fbf1f42e9f1bcb Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Fri, 6 Nov 2015 22:41:17 +0100 Subject: [PATCH 092/115] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 68ea475..63e8bce 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Commands being run as your bot user (`ts3bot`) are marked with `$` and commands $ rm ts3client.run - Download the TS3Bot control application into your TS3Bot user's home folder. The TS3Bot user itself only needs read access to the code. You can do this in two ways: + o By downloading the tar.gz archive from GitHub and unpacking it. $ wget -q -O- https://github.com/icedream/ts3bot-control/archive/develop.tar.gz | tar xz -C ~ @@ -49,6 +50,7 @@ Commands being run as your bot user (`ts3bot`) are marked with `$` and commands $ npm_config_wcjs_runtime="node" npm_config_wcjs_runtime_version="$(node --version | tr -d 'v')" npm install - Now set up your TS3Bot configuration files in your TS3Bot user's home folder. For this create a folder `.ts3bot` in the home directory and put a `config.json` with your configuration there. The most minimal configuration consists of: + o `identity-path` - The path to your identity INI file, export a newly generated identity from your own TeamSpeak3 client for that. o `ts3-install-path` - The path where you installed the TeamSpeak3 client to, you can skip this if you used exactly the same path as in the instructions above. o `ts3-server` - The URL to the server/channel you want the bot to connect to, you can get from your own TS3 client via "Extras" > "Invite friend", select the checkbox "Channel" and select "ts3server link" as invitation type. From 5abb132faef7b2d228bcc417add99a16fcaf4545 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Fri, 6 Nov 2015 22:41:39 +0100 Subject: [PATCH 093/115] Update README.md Fix formatting. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 63e8bce..46a0302 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,9 @@ Commands being run as your bot user (`ts3bot`) are marked with `$` and commands - Now set up your TS3Bot configuration files in your TS3Bot user's home folder. For this create a folder `.ts3bot` in the home directory and put a `config.json` with your configuration there. The most minimal configuration consists of: o `identity-path` - The path to your identity INI file, export a newly generated identity from your own TeamSpeak3 client for that. + o `ts3-install-path` - The path where you installed the TeamSpeak3 client to, you can skip this if you used exactly the same path as in the instructions above. + o `ts3-server` - The URL to the server/channel you want the bot to connect to, you can get from your own TS3 client via "Extras" > "Invite friend", select the checkbox "Channel" and select "ts3server link" as invitation type. Running the bot can finally be done like this: From d1fe5f4d3649f486c0ee22f69dc447c90bbbd029 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 6 Nov 2015 23:18:22 +0100 Subject: [PATCH 094/115] Respond to "current" command properly, not just via channel chat. --- app.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.iced b/app.iced index 94c4f4c..b18f3f2 100644 --- a/app.iced +++ b/app.iced @@ -191,7 +191,7 @@ ts3clientService.on "started", (ts3proc) => 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]." + ts3query?.sendtextmessage args.targetmode, invoker.id, "Currently playing [URL=#{url}]#{title}[/URL]." # Restore audio volume vlc.audio.volume = vlcVolume From a74b923de0aacd9c93aa00eda9f706e58a488d32 Mon Sep 17 00:00:00 2001 From: icedream Date: Fri, 6 Nov 2015 23:27:31 +0100 Subject: [PATCH 095/115] Fix crash in "current" command when nothing is playing. --- app.iced | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app.iced b/app.iced index b18f3f2..24d1bd2 100644 --- a/app.iced +++ b/app.iced @@ -188,6 +188,10 @@ ts3clientService.on "started", (ts3proc) => switch name.toLowerCase() when "current" item = vlc.playlist.items[vlc.playlist.currentItem] + if not item? + ts3query?.sendtextmessage args.targetmode, invoker.id, "Not playing anything at the moment." + return + info = vlcMediaInfo[item.mrl] url = info?.originalUrl or item.mrl title = info?.title or item.mrl From 592872f97d28052da14ac05a3c8b24ea44c2e2a2 Mon Sep 17 00:00:00 2001 From: icedream Date: Sun, 8 Nov 2015 01:08:33 +0100 Subject: [PATCH 096/115] Implement "time" command. Aliases: "seek", "pos", "position" This command sets the current position in the playing item, time can be given as an argument of any sensible form like "2m 3s", "2m03s", "2:03" or "2m". This command returns the current position in the playing item if given no arguments. --- app.iced | 15 +++++++++++++++ package.json | 3 +++ parse_duration.iced | 12 ++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 parse_duration.iced diff --git a/app.iced b/app.iced index 24d1bd2..4ab6673 100644 --- a/app.iced +++ b/app.iced @@ -11,6 +11,8 @@ qs = require "querystring" temp = require("temp").track() youtubedl = require "youtube-dl" isValidUrl = (require "valid-url").isWebUri +parseDuration = require "./parse_duration.iced" +prettyMs = require "pretty-ms" log = getLogger "Main" @@ -246,6 +248,19 @@ ts3clientService.on "started", (ts3proc) => # play it in VLC vlc.play info.url + when "time", "seek", "pos", "position" + inputBB = paramline.trim() + input = (removeBB paramline).trim() + + # we gonna interpret no argument as us needing to return the current position + if input.length <= 0 + ts3query.sendtextmessage args.targetmode, invoker.id, "Currently position is #{prettyMs vlc.input.time}." + return + + ts3query.sendtextmessage args.targetmode, invoker.id, "Seeking to #{prettyMs vlc.input.time}." + vlc.input.time = parseDuration input + + return when "stop-after" vlc.playlist.mode = vlc.playlist.Single ts3query.sendtextmessage args.targetmode, invoker.id, "Playback will stop after the current playlist item." diff --git a/package.json b/package.json index f2fba59..71d560f 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,12 @@ "iced-coffee-script": "^108.0.8", "merge": "^1.2.0", "mkdirp": "^0.5.1", + "named-regexp": "^0.1.1", "nconf": "^0.7.2", "npm-which": "^2.0.0", + "parse-duration": "^0.1.1", "password-generator": "^2.0.1", + "pretty-ms": "^2.1.0", "querystring": "^0.2.0", "request": "^2.61.0", "simple-ini": "^1.0.3", diff --git a/parse_duration.iced b/parse_duration.iced new file mode 100644 index 0000000..bf4b563 --- /dev/null +++ b/parse_duration.iced @@ -0,0 +1,12 @@ +parseDuration = require "parse-duration" +namedRegex = require("named-regexp").named + +durationRegex = namedRegex /^(((:[0-9]{0,2}):)?(:[0-9]{0,2}):)?(:[0-9]{0,2})(:\.[0-9]*)?$/ + +module.exports = (str) -> + # check if this is in the colon-separated format + if str.indexOf(":") > -1 and str.match durationRegex + m = durationRegex.exec(str).matches + return m["ms"] + m["s"]*60 + m["m"]*(60*60) + m["h"]*(60*60*60) + + parseDuration str \ No newline at end of file From 0a0a3875e58cbf720a51fe887061b0606753e671 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Tue, 16 Feb 2016 17:30:42 +0100 Subject: [PATCH 097/115] ts3query: Send TCP keep-alives every minute. Fixes #36. --- ts3query.iced | 1 + 1 file changed, 1 insertion(+) diff --git a/ts3query.iced b/ts3query.iced index 4ff1230..a26743c 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -107,6 +107,7 @@ module.exports = class TS3ClientQuery extends EventEmitter connect: (cb) => @_tcpClient = new net.Socket + @_tcpClient.setKeepAlive true, 60000 @_tcpClient.on "close", () => @emit "close" @_tcpClient.on "error", (err) => From c48ea91682dbde8999147946166a9848da6f30cf Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Tue, 16 Feb 2016 18:13:53 +0100 Subject: [PATCH 098/115] Revert "ts3query: Send TCP keep-alives every minute." This reverts commit 0a0a3875e58cbf720a51fe887061b0606753e671. --- ts3query.iced | 1 - 1 file changed, 1 deletion(-) diff --git a/ts3query.iced b/ts3query.iced index a26743c..4ff1230 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -107,7 +107,6 @@ module.exports = class TS3ClientQuery extends EventEmitter connect: (cb) => @_tcpClient = new net.Socket - @_tcpClient.setKeepAlive true, 60000 @_tcpClient.on "close", () => @emit "close" @_tcpClient.on "error", (err) => From 92c66a6bbc07f5aa9d0d467458305bd657ca0b8f Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Tue, 16 Feb 2016 18:23:59 +0100 Subject: [PATCH 099/115] Send empty command as keep-alive. Fixes #36. Inspired from http://py-ts3.readthedocs.org/en/latest/_modules/ts3/query.html. --- ts3query.iced | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ts3query.iced b/ts3query.iced index 4ff1230..56e1da6 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -140,6 +140,22 @@ module.exports = class TS3ClientQuery extends EventEmitter else @emit "vars", response.args + # send keepalives to avoid connection timeout + @_resetKeepalive() + + _sendKeepalive: (cb) => + @_log.silly "Send: " + + @_tcpClient.write "\n\r", "utf8", () => cb?() + + _stopKeepalive: () => + if @_keepaliveInt is not undefined + clearInterval @_keepaliveInt + + _resetKeepalive: () => + @_stopKeepalive() + @_keepaliveInt = setInterval @_sendKeepalive, 60000 + close: (cb) => if not @_tcpClient cb?() @@ -165,6 +181,7 @@ module.exports = class TS3ClientQuery extends EventEmitter @_log.silly "Send:", text.trim() @_tcpClient.write text, "utf8", () => cb?() + @_resetKeepalive() banadd: (cb) => throw new Error "Not implemented yet" From b4e88ad54b3c3a492953c8500c2aca692ec841ef Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Fri, 8 Apr 2016 19:43:19 +0200 Subject: [PATCH 100/115] ts3query: Stop keepalive thread when closing connection. --- ts3query.iced | 1 + 1 file changed, 1 insertion(+) diff --git a/ts3query.iced b/ts3query.iced index 56e1da6..001f2c1 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -157,6 +157,7 @@ module.exports = class TS3ClientQuery extends EventEmitter @_keepaliveInt = setInterval @_sendKeepalive, 60000 close: (cb) => + @_stopKeepalive() if not @_tcpClient cb?() return From 3948c767e91df5581b5589c7d7e882439f1aa8b7 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Fri, 8 Apr 2016 19:44:59 +0200 Subject: [PATCH 101/115] ts3query: Stop sending keepalives on connection errors. --- ts3query.iced | 1 + 1 file changed, 1 insertion(+) diff --git a/ts3query.iced b/ts3query.iced index 001f2c1..8189d2e 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -111,6 +111,7 @@ module.exports = class TS3ClientQuery extends EventEmitter @_tcpClient.on "close", () => @emit "close" @_tcpClient.on "error", (err) => @_log.warn "Connection error", err + @_stopKeepalive() @_tcpClient = null @emit "error", err From 264f07cf9b68f5b6718b1019f416fe78cc90fd3b Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 16:31:40 +0200 Subject: [PATCH 102/115] Make sure _keepaliveInt is unset in _stopKeepalive. --- ts3query.iced | 1 + 1 file changed, 1 insertion(+) diff --git a/ts3query.iced b/ts3query.iced index 8189d2e..c80c6de 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -152,6 +152,7 @@ module.exports = class TS3ClientQuery extends EventEmitter _stopKeepalive: () => if @_keepaliveInt is not undefined clearInterval @_keepaliveInt + @_keepaliveInt = undefined _resetKeepalive: () => @_stopKeepalive() From 6a68d5c2e89cfac36a07b3d574620c69d47024b1 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 16:50:06 +0200 Subject: [PATCH 103/115] Replace _keepaliveInt undefined check with an easier check. --- ts3query.iced | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts3query.iced b/ts3query.iced index c80c6de..599fc36 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -150,9 +150,9 @@ module.exports = class TS3ClientQuery extends EventEmitter @_tcpClient.write "\n\r", "utf8", () => cb?() _stopKeepalive: () => - if @_keepaliveInt is not undefined + if @_keepaliveInt? clearInterval @_keepaliveInt - @_keepaliveInt = undefined + @_keepaliveInt = null _resetKeepalive: () => @_stopKeepalive() From 3538060002e506d97d46aece592f5a1556ff805f Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 17:04:16 +0200 Subject: [PATCH 104/115] Stop keepalive when "close" event triggers on TCP socket. --- ts3query.iced | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ts3query.iced b/ts3query.iced index 599fc36..84bddb2 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -108,7 +108,9 @@ module.exports = class TS3ClientQuery extends EventEmitter connect: (cb) => @_tcpClient = new net.Socket - @_tcpClient.on "close", () => @emit "close" + @_tcpClient.on "close", () => + @_stopKeepalive() + @emit "close" @_tcpClient.on "error", (err) => @_log.warn "Connection error", err @_stopKeepalive() From e2a05f73bffe494cb0571df8e6fc0e0d9a194132 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 17:06:41 +0200 Subject: [PATCH 105/115] Adding silly logging for keep-alive reset. --- ts3query.iced | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ts3query.iced b/ts3query.iced index 84bddb2..b07471b 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -155,9 +155,14 @@ module.exports = class TS3ClientQuery extends EventEmitter if @_keepaliveInt? clearInterval @_keepaliveInt @_keepaliveInt = null + @_log.silly "Stopped keep-alive." + return + @_log.silly "Requested to stop keep-alive sending but it's already stopped!" _resetKeepalive: () => @_stopKeepalive() + + @_log.silly "Setting up keep-alive." @_keepaliveInt = setInterval @_sendKeepalive, 60000 close: (cb) => From e45aa93b366cbc7220e21986b238c13a3702cef0 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 17:06:57 +0200 Subject: [PATCH 106/115] Start sending keep-alives only after we're actually connected. --- ts3query.iced | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ts3query.iced b/ts3query.iced index b07471b..3f07864 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -121,6 +121,10 @@ module.exports = class TS3ClientQuery extends EventEmitter @_tcpClient.connect @_port, @_host, () => @emit "open" await @once "message.selected", defer(selectedArgs) + + # send keepalives to avoid connection timeout + @_resetKeepalive() + cb?() splitterStream = StreamSplitter("\n\r") @@ -143,9 +147,6 @@ module.exports = class TS3ClientQuery extends EventEmitter else @emit "vars", response.args - # send keepalives to avoid connection timeout - @_resetKeepalive() - _sendKeepalive: (cb) => @_log.silly "Send: " From affd651e5ff741c44673d7409d26fcf3473ab21f Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 18:12:49 +0200 Subject: [PATCH 107/115] Well, this actually doesn't help at all in Docker. --- services/pulseaudio.iced | 1 - 1 file changed, 1 deletion(-) diff --git a/services/pulseaudio.iced b/services/pulseaudio.iced index 3f1c1eb..29139c2 100644 --- a/services/pulseaudio.iced +++ b/services/pulseaudio.iced @@ -53,7 +53,6 @@ module.exports = class PulseAudioService extends services.Service proc = spawn pulseaudioPath, [ "--start" "--fail=true" # quit on startup failure - "--high-priority=true" "--daemonize=false" "-v" ], opts From bd6b66b9057733aba8589ea9f76ea9a00d0fb26d Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 18:22:30 +0200 Subject: [PATCH 108/115] ts3client: Disable playback altogether. --- services/ts3client.iced | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index a179b09..047f819 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -192,6 +192,9 @@ module.exports = class TS3ClientService extends services.Service # Delete bookmars to prevent auto-connect bookmarks from weirding out the client await ts3settings.query "delete * from Bookmarks", defer() + # Delete default playback settings to enforce muted playback later on + await ts3settings.query "delete * from Profiles where key=\"Playback/Standard\"", defer() + # Let's make sure we have an identity! force = ts3settings.getIdentitiesSize() <= 0 or config.get("identity-path") if force @@ -242,7 +245,9 @@ module.exports = class TS3ClientService extends services.Service [ "Plugins", "lua_plugin", "false" ] [ "Plugins", "test_plugin", "false" ] [ "Plugins", "ts3g15", "false" ] - [ "Profiles", "DefaultPlaybackProfile", "Default" ] + [ "Profiles", "DefaultPlaybackProfile", "" ] + [ "Profiles", "Playback/", {} ] + ### [ "Profiles", "Playback/Default", { Device: '' DeviceDisplayName: "Default" @@ -254,6 +259,7 @@ module.exports = class TS3ClientService extends services.Service Mode: "PulseAudio" PlaybackMonoOverCenterSpeaker: false } ] + ### [ "Profiles", "DefaultCaptureProfile", "Default" ] [ "Profiles", "Capture/Default", { Device: '' From 375ee988df681445ca75c997a803eed6d43573a1 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 18:33:30 +0200 Subject: [PATCH 109/115] ts3client: Remove commented out code for default playback settings. --- services/ts3client.iced | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 047f819..7741f7f 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -247,19 +247,6 @@ module.exports = class TS3ClientService extends services.Service [ "Plugins", "ts3g15", "false" ] [ "Profiles", "DefaultPlaybackProfile", "" ] [ "Profiles", "Playback/", {} ] - ### - [ "Profiles", "Playback/Default", { - Device: '' - DeviceDisplayName: "Default" - VolumeModifier: -40 - VolumeFactorWaveDb: -17 - PlayMicClicksOnOwn: false - PlayMicClicksOnOthers: false - MonoSoundExpansion: 2 - Mode: "PulseAudio" - PlaybackMonoOverCenterSpeaker: false - } ] - ### [ "Profiles", "DefaultCaptureProfile", "Default" ] [ "Profiles", "Capture/Default", { Device: '' From f5df78d60c0dcae20a4c7a96a4762f2d1e4d0e59 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 18:57:18 +0200 Subject: [PATCH 110/115] ts3client: Fix removal of default playback profile. --- services/ts3client.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 7741f7f..87c9e20 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -193,7 +193,7 @@ module.exports = class TS3ClientService extends services.Service await ts3settings.query "delete * from Bookmarks", defer() # Delete default playback settings to enforce muted playback later on - await ts3settings.query "delete * from Profiles where key=\"Playback/Standard\"", defer() + await ts3settings.query "delete * from Profiles where key=\"Playback/Default\"", defer() # Let's make sure we have an identity! force = ts3settings.getIdentitiesSize() <= 0 or config.get("identity-path") From a55c1eee30f47aead771f1c1970b77e5cca6fc0e Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 19:44:47 +0200 Subject: [PATCH 111/115] ts3client: That "k" went missing. --- services/ts3client.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 87c9e20..e4587e5 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -189,7 +189,7 @@ module.exports = class TS3ClientService extends services.Service ts3settings = new TS3Settings config.get("ts3-config-path") await ts3settings.open defer() - # Delete bookmars to prevent auto-connect bookmarks from weirding out the client + # Delete bookmarks to prevent auto-connect bookmarks from weirding out the client await ts3settings.query "delete * from Bookmarks", defer() # Delete default playback settings to enforce muted playback later on From d3cc87cf8198ee598541f7359f925350006dabb5 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sat, 9 Apr 2016 19:47:36 +0200 Subject: [PATCH 112/115] ts3client: Enforce blank playback profile. --- services/ts3client.iced | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index e4587e5..76af9b5 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -192,8 +192,8 @@ module.exports = class TS3ClientService extends services.Service # Delete bookmarks to prevent auto-connect bookmarks from weirding out the client await ts3settings.query "delete * from Bookmarks", defer() - # Delete default playback settings to enforce muted playback later on - await ts3settings.query "delete * from Profiles where key=\"Playback/Default\"", defer() + # Delete all profiles so we can enforce our own + await ts3settings.query "delete * from Profiles", defer() # Let's make sure we have an identity! force = ts3settings.getIdentitiesSize() <= 0 or config.get("identity-path") @@ -245,7 +245,7 @@ module.exports = class TS3ClientService extends services.Service [ "Plugins", "lua_plugin", "false" ] [ "Plugins", "test_plugin", "false" ] [ "Plugins", "ts3g15", "false" ] - [ "Profiles", "DefaultPlaybackProfile", "" ] + # Intentionally leaving out DefaultPlaybackProfile so TS3Client will trick itself into using the blank profile [ "Profiles", "Playback/", {} ] [ "Profiles", "DefaultCaptureProfile", "Default" ] [ "Profiles", "Capture/Default", { From 0bca1a63ada62a0021a5d4fb27b7bc25729e4449 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sun, 10 Apr 2016 16:00:49 +0200 Subject: [PATCH 113/115] Use bookmarks to force the client to mute playback. --- services/ts3client.iced | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 76af9b5..415f4c9 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -8,6 +8,7 @@ TS3ClientQuery = require("../ts3query") path = require "path" merge = require "merge" fs = require "fs" +url = require "url" spawn = require("child_process").spawn StreamSplitter = require("stream-splitter") require_bin = require("../require_bin") @@ -36,8 +37,14 @@ module.exports = class TS3ClientService extends services.Service cb? null, @process return + uri = null if not args args = [] + for v in args + if v.indexOf("ts3server:") != 0 + continue + uri = v + break await fs.access ts3client_binpath, fs.R_OK | fs.X_OK, defer err if err @@ -45,7 +52,7 @@ module.exports = class TS3ClientService extends services.Service cb? new Error "Access to TeamSpeak3 binary failed." return - await @_preconfigure defer() + await @_preconfigure uri, defer() # spawn process proc = null @@ -185,7 +192,9 @@ module.exports = class TS3ClientService extends services.Service cb?() - _preconfigure: (cb) => + _preconfigure: (uriString, cb) => + uri = url.parse(uriString, true, true) if uriString? + ts3settings = new TS3Settings config.get("ts3-config-path") await ts3settings.open defer() @@ -270,6 +279,21 @@ module.exports = class TS3ClientService extends services.Service [ "Statistics", "ConfirmedParticipation", "1" ] ], defer() + if uri? + bookmarkId = "{5125344e-45ec-4510-9bbf-8b940628c5d0}" + await ts3settings.set "Bookmarks", bookmarkId, { + Name: uriString + Address: uri.host or "localhost" + Port: uri.port or 9987 + CaptureProfile: "Default" + PlaybackProfile: "" + Identity: "Standard" + Nick: uri.nickname or config.get("nickname") or "TS3Bot" + Autoconnect: false + ShowServerQueryClients: false + Uuid: bookmarkId + }, defer() + await ts3settings.close defer() cb?() From 3212134e2cbd47411bf0e52133d20ae04249390e Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Fri, 15 Apr 2016 00:34:20 +0200 Subject: [PATCH 114/115] Fix mute not applied when server_uid in ts3server URL. For this we now set ServerUID in the bookmark entry the bot generates in order to make the TS3Client disable its output device. --- services/ts3client.iced | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 415f4c9..49016cf 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -281,7 +281,7 @@ module.exports = class TS3ClientService extends services.Service if uri? bookmarkId = "{5125344e-45ec-4510-9bbf-8b940628c5d0}" - await ts3settings.set "Bookmarks", bookmarkId, { + bookmarkData = Name: uriString Address: uri.host or "localhost" Port: uri.port or 9987 @@ -292,7 +292,8 @@ module.exports = class TS3ClientService extends services.Service Autoconnect: false ShowServerQueryClients: false Uuid: bookmarkId - }, defer() + bookmarkData.ServerUID = uri.query?.server_uid if uri.query?.server_uid? + await ts3settings.set "Bookmarks", bookmarkId, bookmarkData, defer() await ts3settings.close defer() From 4ab1de5a5e5f969982b114713cac35265fda6ef0 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sun, 8 May 2016 16:26:16 +0200 Subject: [PATCH 115/115] services/ts3client: Fix client not disabling playback when connecting to non-standard port. --- services/ts3client.iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ts3client.iced b/services/ts3client.iced index 49016cf..d9cfd9f 100644 --- a/services/ts3client.iced +++ b/services/ts3client.iced @@ -283,7 +283,7 @@ module.exports = class TS3ClientService extends services.Service bookmarkId = "{5125344e-45ec-4510-9bbf-8b940628c5d0}" bookmarkData = Name: uriString - Address: uri.host or "localhost" + Address: uri.hostname or "localhost" Port: uri.port or 9987 CaptureProfile: "Default" PlaybackProfile: ""