mirror of https://github.com/icedream/ts3bot.git
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.develop
commit
a0bded664a
|
@ -0,0 +1 @@
|
||||||
|
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
|
|
@ -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();
|
||||||
|
});
|
|
@ -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"
|
|
@ -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
|
|
@ -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 <icedream2k9@die-optimisten.net>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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."
|
|
@ -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
|
|
@ -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 @
|
|
@ -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
|
|
@ -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?()
|
|
@ -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?()
|
|
@ -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
|
|
@ -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?()
|
|
@ -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?()
|
||||||
|
|
|
@ -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?()
|
|
@ -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 = {}
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue