Update to ICSv2.

ICSv2 will be used as a preparation to fully change to ES2017 only. For now the compilation will be done by transpiling from ICSv2 to ES2015+, then using lebab to upgrade some left-over code parts and then using Babel to transpile down some of the features that Node does not support yet. This way we already make sure we can theoretically use ES2017.

- Use import instead of require.
- Rename *.iced to *.coffee with some exceptions (see below).
- Rename app.iced to index.coffee.
- Rename app.js to index.js.
- Wrap app code in function definition to avoid import statements not being at top-most root.
- Avoid using fat arrows where unnecessary to reduce overhead in generated ES code.

Known issues:

- Currently the code can not be run without being built first. We need to see how we can solve that soon.
develop
Icedream 2017-05-18 01:22:46 +02:00
parent f3b2230976
commit 07de32a44e
Signed by: icedream
GPG Key ID: 1573F6D8EFE4D0CF
25 changed files with 814 additions and 2110 deletions

View File

@ -1,7 +1,34 @@
.git # Logs
**/.gitignore logs
*.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
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules
###
.git*
.docker*
Dockerfile Dockerfile
src/node_modules
**/*.log **/*.log
**/*.ini **/*.ini
**/*.md **/*.md

View File

@ -1,7 +1,6 @@
# Logs # Logs
logs logs
*.log *.log
npm-debug.log*
# Runtime data # Runtime data
pids pids
@ -17,12 +16,14 @@ coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt .grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html) # Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release build/Release
# Dependency directory # Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git # Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules node_modules
# Built files
src.js
dist

View File

@ -1,4 +1,4 @@
FROM node:6.3.1 FROM node:7.10.0
ARG TS3CLIENT_VERSION=3.0.19.4 ARG TS3CLIENT_VERSION=3.0.19.4

View File

@ -1,10 +1,10 @@
{ {
"name": "ts3bot", "name": "ts3bot",
"version": "0.3.0", "version": "0.4.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.", "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", "main": "dist/node7/index.js",
"bin": { "bin": {
"ts3bot": "app.js" "ts3bot": "dist/node7/index.js"
}, },
"keywords": [ "keywords": [
"teamspeak", "teamspeak",
@ -20,15 +20,22 @@
"media", "media",
"musicbot" "musicbot"
], ],
"author": "Carl Kittelberger <icedream2k9@die-optimisten.net>", "scripts": {
"iced2es": "iced3 -I none -b --no-header -o src.js -c src && lebab -t let,multi-var,for-each,arg-rest,arg-spread,obj-method,obj-shorthand,no-strict,exponent,template,default-param,includes,class --replace src.js",
"es2017-node7": "babel src.js --presets es2017-node7 --source-maps --out-dir ./dist/node7",
"build": "npm run iced2es && npm run es2017-node7",
"prepublish": "npm run build",
"start": "node .",
"start-coffee": "node ./src/index.js"
},
"author": "Carl Kittelberger <icedream@icedream.pw>",
"license": "GPL-3.0+", "license": "GPL-3.0+",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/icedream/ts3bot-control.git" "url": "https://github.com/icedream/ts3bot.git"
}, },
"dependencies": { "dependencies": {
"express": "^4.13.3", "express": "^4.13.3",
"iced-coffee-script": "^108.0.8",
"merge": "^1.2.0", "merge": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"named-regexp": "^0.1.1", "named-regexp": "^0.1.1",
@ -49,7 +56,17 @@
"webchimera.js": "^0.2.7", "webchimera.js": "^0.2.7",
"which": "^1.1.2", "which": "^1.1.2",
"winston": "^2.3.1", "winston": "^2.3.1",
"xvfb": "git://github.com/icedream/node-xvfb.git", "xvfb": "^0.2.3",
"youtube-dl": "^1.10.5" "youtube-dl": "^1.10.5"
},
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-preset-es2017-node7": "^0.5.2",
"iced-coffee-script-3": "^111.1.1",
"lebab": "^2.7.2"
},
"engineStrict": true,
"engines": {
"node": ">= 7.6.0"
} }
} }

382
src/app.coffee Normal file
View File

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

View File

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

View File

@ -1,7 +1,7 @@
nconf = require "nconf" import nconf from 'nconf'
path = require "path" import path from 'path'
merge = require "merge" import merge from 'merge'
pwgen = require "password-generator" import pwgen from 'password-generator'
console.log "Loading configuration..." console.log "Loading configuration..."

73
src/index.coffee Normal file
View File

@ -0,0 +1,73 @@
import Sync from 'sync'
import readline from 'readline'
import services from './services'
import getLogger from './logger'
import app from './app.iced'
log = getLogger('app')
# compatibility with Windows for interrupt signal
if process.platform == 'win32'
rl = readline.createInterface(
input: process.stdin
output: process.stdout)
rl.on 'SIGINT', ->
process.emit 'SIGINT'
doShutdownAsync = (cb) ->
log.info 'App shutdown starting...'
app.shutdown ->
log.info 'Services shutdown starting...'
services.shutdown ->
if cb and typeof cb == 'function'
cb()
return
return
return
process.on 'uncaughtException', (err) ->
log.error 'Shutting down due to an uncaught exception!', err
app.shutdownSync()
process.exit 0xFF
return
process.on 'exit', (e) ->
log.debug 'Triggered exit', e
app.shutdownSync()
return
process.on 'SIGTERM', (e) ->
log.debug 'Caught SIGTERM signal'
app.shutdown ->
process.exit 0
return
return
process.on 'SIGINT', ->
log.debug 'Caught SIGINT signal'
app.shutdown ->
process.exit 0
return
return
process.on 'SIGHUP', ->
log.debug 'Caught SIGHUP signal'
app.shutdown ->
process.exit 0
return
return
process.on 'SIGQUIT', ->
log.debug 'Caught SIGQUIT signal'
app.shutdown ->
process.exit 0
return
return
process.on 'SIGABRT', ->
log.debug 'Caught SIGABRT signal'
app.shutdown ->
process.exit 0
return
return

View File

@ -1,6 +1,4 @@
#!/usr/bin/env node require("iced-coffee-script-3/register");
require("iced-coffee-script/register");
Sync = require("sync"); Sync = require("sync");
var services = require("./services"); var services = require("./services");
@ -19,7 +17,7 @@ if (process.platform === "win32") {
}); });
} }
app = require("./app.iced"); app = require("./app");
doShutdownAsync = function(cb) { doShutdownAsync = function(cb) {
log.info("App shutdown starting..."); log.info("App shutdown starting...");

View File

@ -1,8 +1,9 @@
winston = require "winston" import winston from 'winston'
path = require "path" import path from 'path'
config = require "./config" import merge from 'merge'
merge = require "merge" import winstonCommon from 'winston/lib/winston/common'
winstonCommon = require "winston/lib/winston/common"
import config from './config'
winston.emitErrs = true winston.emitErrs = true
@ -40,10 +41,10 @@ container = new (winston.Container)
initialized_loggers = [] initialized_loggers = []
module.exports = (name, options) => module.exports = (name, options) ->
if not(name in initialized_loggers) if not(name in initialized_loggers)
logger = container.add name logger = container.add name
logger.filters.push (level, msg, meta) => "[#{name}] #{msg}" logger.filters.push (level, msg, meta) -> "[#{name}] #{msg}"
initialized_loggers.push name initialized_loggers.push name
return logger return logger

View File

@ -1,5 +1,5 @@
parseDuration = require "parse-duration" import parseDuration from 'parse-duration'
namedRegex = require("named-regexp").named import { named as namedRegex } from 'named-regexp'
durationRegex = namedRegex /^(((:<h>[0-9]{0,2}):)?(:<m>[0-9]{0,2}):)?(:<s>[0-9]{0,2})(:<ms>\.[0-9]*)?$/ durationRegex = namedRegex /^(((:<h>[0-9]{0,2}):)?(:<m>[0-9]{0,2}):)?(:<s>[0-9]{0,2})(:<ms>\.[0-9]*)?$/

View File

@ -1,8 +1,11 @@
which = require("which").sync import { sync as which } from 'which'
path = require "path" import path from 'path'
log = require("./logger")("RequireBin")
module.exports = (binName, doErrorIfNotFound) => import getLogger from './logger'
log = getLogger "RequireBin"
module.exports = (binName, doErrorIfNotFound) ->
doErrorIfNotFound = true unless doErrorIfNotFound? doErrorIfNotFound = true unless doErrorIfNotFound?
# check if xvfb is findable from here # check if xvfb is findable from here

View File

@ -3,4 +3,4 @@ module.exports = (a, b) ->
return -1; # a before b return -1; # a before b
if b.dependencies.indexOf(a.name) >= 0 if b.dependencies.indexOf(a.name) >= 0
return 1; # a after b return 1; # a after b
return 0; # does not matter return 0 # does not matter

View File

@ -1,11 +1,11 @@
Sync = require "sync" import Sync from 'sync'
import { EventEmitter } from 'events'
import merge from 'merge'
getLogger = require "./logger" import getLogger from './logger'
EventEmitter = require("events").EventEmitter import services from './services'
merge = require "merge"
services = require "./services"
module.exports = class Service extends EventEmitter class Service extends EventEmitter
constructor: (@name, funcs) -> constructor: (@name, funcs) ->
@log = getLogger @name @log = getLogger @name
@_funcs = funcs @_funcs = funcs
@ -152,3 +152,5 @@ module.exports = class Service extends EventEmitter
cb? err cb? err
restartSync: () => Sync () => @restart.sync @ restartSync: () => Sync () => @restart.sync @
module.exports = Service

View File

@ -1,12 +1,16 @@
# At this point I feel like I'm writing my own init system. Phew... # At this point I feel like I'm writing my own init system. Phew...
merge = require "merge" import merge from 'merge'
getLogger = require("./logger") import { EventEmitter } from 'events'
EventEmitter = require("events").EventEmitter import Sync from 'sync'
log = getLogger("ServiceMgr")
Sync = require "sync"
getLegacyServiceName = (serviceName) -> serviceName.toLowerCase().replace(/[^A-z0-9]/g, "_") import getLogger from './logger'
import { Service } from './service_template'
log = getLogger("ServiceMgr")
getLegacyServiceName = (serviceName) ->
serviceName.toLowerCase().replace(/[^A-z0-9]/g, "_")
module.exports = module.exports =
services: [] services: []
@ -52,7 +56,7 @@ module.exports =
shutdownSync: () -> Sync () => @shutdown.sync @ shutdownSync: () -> Sync () => @shutdown.sync @
# base class for all services # base class for all services
module.exports.Service = require "./service_template" module.exports.Service = Service
# register services # register services
services = [ services = [

View File

@ -1,10 +1,12 @@
spawn = require("child_process").spawn import { spawn } from 'child_process'
log = require("../logger")("PulseAudio") import StreamSplitter from 'stream-splitter'
services = require("../services")
config = require("../config")
StreamSplitter = require("stream-splitter")
require_bin = require("../require_bin")
import getLogger from '../logger'
import services from '../services'
import config from '../config'
import require_bin from '../require_bin'
log = getLogger "PulseAudio"
pulseaudioPath = require_bin config.get("PULSE_BINARY") pulseaudioPath = require_bin config.get("PULSE_BINARY")
pacmdPath = require_bin "pacmd" pacmdPath = require_bin "pacmd"
@ -18,7 +20,7 @@ module.exports = class PulseAudioService extends services.Service
return return
# logging # logging
forwardLog = (token) => forwardLog = (token) ->
token = token.trim() # get rid of \r token = token.trim() # get rid of \r
level = token.substring(0, 1).toUpperCase() level = token.substring(0, 1).toUpperCase()
msg = token.substring token.indexOf("]") + 2 msg = token.substring token.indexOf("]") + 2
@ -41,7 +43,7 @@ module.exports = class PulseAudioService extends services.Service
# check if there is already a daemon running # check if there is already a daemon running
proc = spawn pulseaudioPath, [ "--check" ], opts proc = spawn pulseaudioPath, [ "--check" ], opts
stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" stderrTokenizer = proc.stderr.pipe StreamSplitter "\n"
stderrTokenizer.encoding = "utf8"; stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", forwardLog stderrTokenizer.on "token", forwardLog
await proc.once "exit", defer(code, signal) 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"}" @log.silly "PulseAudio daemon check returned that #{if code == 0 then "a daemon is already running" else "no daemon is running"}"
@ -74,7 +76,7 @@ module.exports = class PulseAudioService extends services.Service
stderrTokenizer.encoding = "utf8" stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", tokenHandler stderrTokenizer.on "token", tokenHandler
proc.on "exit", () => proc.on "exit", () ->
if not calledCallback if not calledCallback
calledCallback = true calledCallback = true
cb? new Error "PulseAudio daemon terminated unexpectedly." cb? new Error "PulseAudio daemon terminated unexpectedly."
@ -89,11 +91,11 @@ module.exports = class PulseAudioService extends services.Service
cb?() cb?()
findIndexForProcessId: (pid, cb) => throw new Error "Not implemented yet" findIndexForProcessId: (pid, cb) -> throw new Error "Not implemented yet"
findIndexForProcessIdSync: (pid) => Sync () => @findIndexForProcessId @, pid findIndexForProcessIdSync: (pid) => Sync () => @findIndexForProcessId @, pid
setSinkInputMute: (index, value, cb) => throw new Error "Not implemented yet" setSinkInputMute: (index, value, cb) -> throw new Error "Not implemented yet"
setSinkInputMuteSync: (index, value) => Sync () => @setSinkInputMute @, index, value setSinkInputMuteSync: (index, value) => Sync () => @setSinkInputMute @, index, value

View File

@ -1,17 +1,20 @@
xvfb = require("xvfb") import xvfb from 'xvfb'
log = require("../logger")("TS3Client") import path from 'path'
config = require("../config") import merge from 'merge'
services = require("../services") import fs from 'fs'
x11tools = require("../x11") import url from 'url'
TS3Settings = require("../ts3settings") import { spawn } from 'child_process'
TS3ClientQuery = require("../ts3query") import StreamSplitter from 'stream-splitter'
path = require "path"
merge = require "merge" import getLogger from '../logger'
fs = require "fs" import config from '../config'
url = require "url" import services from '../services'
spawn = require("child_process").spawn import x11tools from '../x11'
StreamSplitter = require("stream-splitter") import TS3Settings from '../ts3settings'
require_bin = require("../require_bin") import TS3ClientQuery from '../ts3query'
import require_bin from '../require_bin'
log = getLogger "TS3Client"
ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3client_linux_" + (if process.arch == "x64" then "amd64" else process.arch)) ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3client_linux_" + (if process.arch == "x64" then "amd64" else process.arch))
@ -57,7 +60,7 @@ module.exports = class TS3ClientService extends services.Service
# spawn process # spawn process
proc = null proc = null
doStart = null doStart = null
forwardLog = (token) => forwardLog = (token) ->
token = token.trim() # get rid of \r token = token.trim() # get rid of \r
if token.indexOf("|") > 0 if token.indexOf("|") > 0
token = token.split("|") token = token.split("|")
@ -106,11 +109,11 @@ module.exports = class TS3ClientService extends services.Service
# logging # logging
stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n" stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n"
stdoutTokenizer.encoding = "utf8"; stdoutTokenizer.encoding = "utf8"
stdoutTokenizer.on "token", forwardLog stdoutTokenizer.on "token", forwardLog
stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" stderrTokenizer = proc.stderr.pipe StreamSplitter "\n"
stderrTokenizer.encoding = "utf8"; stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", forwardLog stderrTokenizer.on "token", forwardLog
# connect to client query plugin when it's loaded # connect to client query plugin when it's loaded

View File

@ -1,8 +1,9 @@
spawn = require("child_process").spawn import { spawn } from 'child_process'
services = require("../services") import wc from 'webchimera.js'
config = require("../config") import StreamSplitter from 'stream-splitter'
wc = require("webchimera.js")
StreamSplitter = require("stream-splitter") import services from '../services'
import config from '../config'
module.exports = class VLCService extends services.Service module.exports = class VLCService extends services.Service
dependencies: [ dependencies: [
@ -42,4 +43,3 @@ module.exports = class VLCService extends services.Service
@_instance = null @_instance = null
cb?() cb?()

View File

@ -1,9 +1,10 @@
Xvfb = require("xvfb") import Xvfb from 'xvfb'
log = require("../logger")("Xvfb") import getLogger from '../logger'
config = require("../config") import config from '../config'
services = require("../services") import services from '../services'
require_bin = require("../require_bin") import require_bin from '../require_bin'
log = getLogger "Xvfb"
xvfbPath = require_bin "Xvfb", false xvfbPath = require_bin "Xvfb", false
module.exports = class XvfbService extends services.Service module.exports = class XvfbService extends services.Service

View File

@ -1,8 +1,11 @@
spawn = require("child_process").spawn import { spawn } from 'child_process'
log = require("../logger")("XWindowManager") import StreamSplitter from 'stream-splitter'
services = require("../services")
StreamSplitter = require("stream-splitter") import getLogger from '../logger'
require_bin = require("../require_bin") import services from '../services'
import require_bin from '../require_bin'
log = getLogger "XWindowManager"
xwmBinPath = require_bin "x-window-manager", false xwmBinPath = require_bin "x-window-manager", false
@ -42,13 +45,13 @@ module.exports = class XWindowManagerService extends services.Service
# logging # logging
stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n" stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n"
stdoutTokenizer.encoding = "utf8"; stdoutTokenizer.encoding = "utf8"
stdoutTokenizer.on "token", (token) => stdoutTokenizer.on "token", (token) =>
token = token.trim() # get rid of \r token = token.trim() # get rid of \r
@log.debug token @log.debug token
stderrTokenizer = proc.stderr.pipe StreamSplitter "\n" stderrTokenizer = proc.stderr.pipe StreamSplitter "\n"
stderrTokenizer.encoding = "utf8"; stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", (token) => stderrTokenizer.on "token", (token) =>
token = token.trim() # get rid of \r token = token.trim() # get rid of \r
@log.warn token @log.warn token

View File

@ -1,11 +1,14 @@
# @property "prop", { desc... } # @property "prop", { desc... }
Function::property = (prop, desc) -> Object.defineProperty @prototype, prop, desc Function::property = (prop, desc) ->
Object.defineProperty @prototype, prop, desc
# defineProperty "prop", { desc... } # defineProperty "prop", { desc... }
#Object::defineProperty = (prop, desc) -> Object.defineProperty @, prop, desc #Object::defineProperty = (prop, desc) -> Object.defineProperty @, prop, desc
# propertiesof obj # propertiesof obj
#global.propertiesof = (obj) -> Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertyNames(obj.constructor.prototype or {})) #global.propertiesof = (obj) ->
# Object.getOwnPropertyNames(obj).concat(
# Object.getOwnPropertyNames(obj.constructor.prototype or {}))
# descriptorof obj, name # descriptorof obj, name
#global.descriptorof = (obj, name) -> Object.getOwnPropertyDescriptor obj, name #global.descriptorof = (obj, name) -> Object.getOwnPropertyDescriptor obj, name

View File

@ -1,16 +1,16 @@
require "string.prototype.startswith" import 'string.prototype.startswith'
net = require "net" import net from 'net'
getLogger = require "./logger" import { StringDecoder } from 'string_decoder'
StringDecoder = require("string_decoder").StringDecoder import StreamSplitter from 'stream-splitter'
StreamSplitter = require "stream-splitter" import { EventEmitter } from 'events'
events = require "events" import merge from 'merge'
EventEmitter = events.EventEmitter
merge = require "merge" import getLogger from './logger'
parserLog = getLogger "parser" parserLog = getLogger "parser"
escape = (value) => value.toString()\ escape = (value) -> value.toString()\
.replace(/\\/g, "\\\\")\ .replace(/\\/g, "\\\\")\
.replace(/\//g, "\\/")\ .replace(/\//g, "\\/")\
.replace(/\|/g, "\\p")\ .replace(/\|/g, "\\p")\
@ -19,7 +19,7 @@ escape = (value) => value.toString()\
.replace(/\t/g, "\\t")\ .replace(/\t/g, "\\t")\
.replace(/\ /g, "\\s") .replace(/\ /g, "\\s")
unescape = (value) => value.toString()\ unescape = (value) -> value.toString()\
.replace(/\\s/g, " ")\ .replace(/\\s/g, " ")\
.replace(/\\t/g, "\t")\ .replace(/\\t/g, "\t")\
.replace(/\\r/g, "\r")\ .replace(/\\r/g, "\r")\
@ -28,7 +28,7 @@ unescape = (value) => value.toString()\
.replace(/\\\//g, "/")\ .replace(/\\\//g, "/")\
.replace(/\\\\/g, "\\") .replace(/\\\\/g, "\\")
buildCmd = (name, namedArgs, posArgs) => buildCmd = (name, namedArgs, posArgs) ->
# TODO: Add support for collected arguments (aka lists) # TODO: Add support for collected arguments (aka lists)
if not name if not name
throw new Error "Need command name" throw new Error "Need command name"
@ -52,7 +52,7 @@ buildCmd = (name, namedArgs, posArgs) =>
param += "#{escape(v)}" param += "#{escape(v)}"
param + "\n\r" param + "\n\r"
parseCmd = (str) => parseCmd = (str) ->
params = str.split " " params = str.split " "
startIndex = 0 startIndex = 0
@ -92,7 +92,7 @@ parseCmd = (str) =>
args: collectedArgs args: collectedArgs
} }
checkError = (err) => checkError = (err) ->
err.id = parseInt err.id err.id = parseInt err.id
if err.id == 0 if err.id == 0
return null return null
@ -150,7 +150,7 @@ module.exports = class TS3ClientQuery extends EventEmitter
_sendKeepalive: (cb) => _sendKeepalive: (cb) =>
@_log.silly "Send: <keep-alive>" @_log.silly "Send: <keep-alive>"
@_tcpClient.write "\n\r", "utf8", () => cb?() @_tcpClient.write "\n\r", "utf8", () -> cb?()
_stopKeepalive: () => _stopKeepalive: () =>
if @_keepaliveInt? if @_keepaliveInt?
@ -191,37 +191,37 @@ module.exports = class TS3ClientQuery extends EventEmitter
@_log.silly "Send:", text.trim() @_log.silly "Send:", text.trim()
@_tcpClient.write text, "utf8", () => cb?() @_tcpClient.write text, "utf8", () -> cb?()
@_resetKeepalive() @_resetKeepalive()
banadd: (cb) => banadd: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
banclient: (cb) => banclient: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
bandel: (cb) => bandel: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
bandelall: (cb) => bandelall: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
banlist: (cb) => banlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channeladdperm: (cb) => channeladdperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelclientaddperm: (cb) => channelclientaddperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelclientdelperm: (cb) => channelclientdelperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelclientlist: (cb) => channelclientlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelclientpermlist: (cb) => channelclientpermlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
### ###
@ -234,8 +234,8 @@ module.exports = class TS3ClientQuery extends EventEmitter
cb = cid cb = cid
cid = null cid = null
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "channelconnectinfo", @send "channelconnectinfo",
cid: cid cid: cid
@ -254,70 +254,70 @@ module.exports = class TS3ClientQuery extends EventEmitter
channel_properties = {} channel_properties = {}
channel_properties["channel_name"] = channel_name channel_properties["channel_name"] = channel_name
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "channelcreate", channel_properties @send "channelcreate", channel_properties
channeldelete: (cb) => channeldelete: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channeldelperm: (cb) => channeldelperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
### ###
Changes a channels configuration using given properties. Changes a channels configuration using given properties.
### ###
channeledit: (cid, channel_properties, cb) => channeledit: (cid, channel_properties, cb) =>
@once "message.error", (args) => cb? checkError(args) @once "message.error", (args) -> cb? checkError(args)
@send "channeledit", merge true, channel_properties, @send "channeledit", merge true, channel_properties,
cid: cid cid: cid
channelgroupadd: (cb) => channelgroupadd: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelgroupaddperm: (cb) => channelgroupaddperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelgroupclientlist: (cb) => channelgroupclientlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelgroupdel: (cb) => channelgroupdel: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelgroupdelperm: (cb) => channelgroupdelperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelgrouplist: (cb) => channelgrouplist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelgrouppermlist: (cb) => channelgrouppermlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channellist: (cb) => channellist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelmove: (cb) => channelmove: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelpermlist: (cb) => channelpermlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
channelvariable: (cb) => channelvariable: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
clientaddperm: (cb) => clientaddperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
clientdbdelete: (cb) => clientdbdelete: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
clientdbedit: (cb) => clientdbedit: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
clientdblist: (cb) => clientdblist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
clientdelperm: (cb) => clientdelperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
### ###
@ -325,8 +325,8 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
clientgetdbidfromuid: (cluid, cb) => clientgetdbidfromuid: (cluid, cb) =>
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientgetdbidfromuid", @send "clientgetdbidfromuid",
cluid: cluid cluid: cluid
@ -335,8 +335,8 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
clientgetids: (cb) => clientgetids: (cb) =>
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientgetids", @send "clientgetids",
cluid: cluid cluid: cluid
@ -346,8 +346,8 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
clientgetnamefromdbid: (cldbid, cb) => clientgetnamefromdbid: (cldbid, cb) =>
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientgetnamefromdbid", @send "clientgetnamefromdbid",
cldbid: cldbid cldbid: cldbid
@ -357,8 +357,8 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
clientgetnamefromuid: (cluid, cb) => clientgetnamefromuid: (cluid, cb) =>
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientgetnamefromuid", @send "clientgetnamefromuid",
cluid: cluid cluid: cluid
@ -368,8 +368,8 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
clientgetuidfromclid: (clid, cb) => clientgetuidfromclid: (clid, cb) =>
retval = { } retval = { }
@once "notifyclientuidfromclid", (args) => merge retval, args @once "notifyclientuidfromclid", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientgetuidfromclid", @send "clientgetuidfromclid",
clid: clid clid: clid
@ -391,7 +391,7 @@ module.exports = class TS3ClientQuery extends EventEmitter
if typeof clid == "function" if typeof clid == "function"
cb = clid cb = clid
clid = null clid = null
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientkick", @send "clientkick",
reasonid: reasonid reasonid: reasonid
reasonmsg: reasonmsg reasonmsg: reasonmsg
@ -449,14 +449,14 @@ module.exports = class TS3ClientQuery extends EventEmitter
cleanedModifiers.push v cleanedModifiers.push v
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientlist", cleanedModifiers @send "clientlist", cleanedModifiers
clientmove: (cb) => clientmove: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
clientmute: (cb) => clientmute: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
### ###
@ -501,7 +501,7 @@ module.exports = class TS3ClientQuery extends EventEmitter
notifyconnectstatuschange notifyconnectstatuschange
### ###
clientnotifyregister: (schandlerid, event, cb) => clientnotifyregister: (schandlerid, event, cb) =>
@once "message.error", (args) => cb? checkError(args) @once "message.error", (args) -> cb? checkError(args)
@send "clientnotifyregister", @send "clientnotifyregister",
schandlerid: schandlerid schandlerid: schandlerid
event: event event: event
@ -510,7 +510,7 @@ module.exports = class TS3ClientQuery extends EventEmitter
Unregisters from all previously registered client notifications. Unregisters from all previously registered client notifications.
### ###
clientnotifyunregister: (cb) => clientnotifyunregister: (cb) =>
@once "message.error", (args) => cb? checkError(args) @once "message.error", (args) -> cb? checkError(args)
@send "clientnotifyunregister" @send "clientnotifyunregister"
### ###
@ -518,8 +518,8 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
clientpermlist: (cldbid, cb) => clientpermlist: (cldbid, cb) =>
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientpermlist", @send "clientpermlist",
cldbid: cldbid cldbid: cldbid
@ -530,12 +530,12 @@ module.exports = class TS3ClientQuery extends EventEmitter
if typeof msg == "function" if typeof msg == "function"
cb = msg cb = msg
msg = null msg = null
@once "message.error", (args) => cb? checkError(args) @once "message.error", (args) -> cb? checkError(args)
@send "clientpoke", @send "clientpoke",
msg: msg msg: msg
clid: clid clid: clid
clientunmute: (cb) => clientunmute: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
### ###
@ -558,7 +558,7 @@ module.exports = class TS3ClientQuery extends EventEmitter
connect connect
### ###
clientupdate: (idents, cb) => clientupdate: (idents, cb) =>
@once "message.error", (args) => cb? checkError(args) @once "message.error", (args) -> cb? checkError(args)
@send "clientupdate", idents @send "clientupdate", idents
### ###
@ -624,20 +624,20 @@ module.exports = class TS3ClientQuery extends EventEmitter
if not Array.isArray variables if not Array.isArray variables
throw new Error "variables needs to be an array of requested client variables." throw new Error "variables needs to be an array of requested client variables."
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "clientvariable", { clid: clid }, variables @send "clientvariable", { clid: clid }, variables
complainadd: (cb) => complainadd: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
complaindel: (cb) => complaindel: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
complaindelall: (cb) => complaindelall: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
complainlist: (cb) => complainlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
### ###
@ -645,67 +645,67 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
currentschandlerid: (cb) => currentschandlerid: (cb) =>
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "currentschandlerid" @send "currentschandlerid"
disconnect: (cb) => close(cb) disconnect: (cb) -> close(cb)
exam: (cb) => exam: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftcreatedir: (cb) => ftcreatedir: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftdeletefile: (cb) => ftdeletefile: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftgetfileinfo: (cb) => ftgetfileinfo: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftgetfilelist: (cb) => ftgetfilelist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftinitdownload: (cb) => ftinitdownload: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftinitupload: (cb) => ftinitupload: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftlist: (cb) => ftlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftrenamefile: (cb) => ftrenamefile: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
ftstop: (cb) => ftstop: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
hashpassword: (cb) => hashpassword: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
help: (cb) => help: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
messageadd: (cb) => messageadd: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
messagedel: (cb) => messagedel: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
messageget: (cb) => messageget: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
messagelist: (cb) => messagelist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
messageupdateflag: (cb) => messageupdateflag: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
permoverview: (cb) => permoverview: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
quit: (cb) => close(cb) quit: (cb) -> close(cb)
### ###
Sends a text message a specified target. The type of the target is determined Sends a text message a specified target. The type of the target is determined
@ -720,61 +720,61 @@ module.exports = class TS3ClientQuery extends EventEmitter
cb = msg cb = msg
msg = target msg = target
target = null target = null
@once "message.error", (args) => cb? checkError(args) @once "message.error", (args) -> cb? checkError(args)
@send "sendtextmessage", @send "sendtextmessage",
targetmode: targetmode targetmode: targetmode
target: target target: target
msg: msg msg: msg
serverconnectinfo: (cb) => serverconnectinfo: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
serverconnectionhandlerlist: (cb) => serverconnectionhandlerlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergroupadd: (cb) => servergroupadd: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergroupaddclient: (cb) => servergroupaddclient: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergroupaddperm: (cb) => servergroupaddperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergroupclientlist: (cb) => servergroupclientlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergroupdel: (cb) => servergroupdel: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergroupdelclient: (cb) => servergroupdelclient: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergroupdelperm: (cb) => servergroupdelperm: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergrouplist: (cb) => servergrouplist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergrouppermlist: (cb) => servergrouppermlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servergroupsbyclientid: (cb) => servergroupsbyclientid: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
servervariable: (cb) => servervariable: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
setclientchannelgroup: (cb) => setclientchannelgroup: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
tokenadd: (cb) => tokenadd: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
tokendelete: (cb) => tokendelete: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
tokenlist: (cb) => tokenlist: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
### ###
@ -782,7 +782,7 @@ module.exports = class TS3ClientQuery extends EventEmitter
server will automatically delete the token after it has been used. server will automatically delete the token after it has been used.
### ###
tokenuse: (token, cb) => tokenuse: (token, cb) =>
@once "message.error", (args) => cb? checkError(args) @once "message.error", (args) -> cb? checkError(args)
@send "tokenuse", @send "tokenuse",
token: token token: token
@ -792,12 +792,12 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
use: (schandlerid, cb) => use: (schandlerid, cb) =>
retval = { } retval = { }
@once "message.selected", (args) => merge retval, args @once "message.selected", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "use", @send "use",
schandlerid: schandlerid schandlerid: schandlerid
verifychannelpassword: (cb) => verifychannelpassword: (cb) ->
throw new Error "Not implemented yet" throw new Error "Not implemented yet"
### ###
@ -805,7 +805,7 @@ module.exports = class TS3ClientQuery extends EventEmitter
incorrect. incorrect.
### ###
verifyserverpassword: (password, cb) => verifyserverpassword: (password, cb) =>
@once "message.error", (args) => cb? checkError(args) @once "message.error", (args) -> cb? checkError(args)
@send "verifyserverpassword", @send "verifyserverpassword",
password: password password: password
@ -818,6 +818,6 @@ module.exports = class TS3ClientQuery extends EventEmitter
### ###
whoami: (cb) => whoami: (cb) =>
retval = { } retval = { }
@once "vars", (args) => merge retval, args @once "vars", (args) -> merge retval, args
@once "message.error", (args) => cb? checkError(args), retval @once "message.error", (args) -> cb? checkError(args), retval
@send "whoami" @send "whoami"

View File

@ -1,17 +1,17 @@
sqlite3 = require("sqlite3") #.verbose() import sqlite3, { Database as SQLite3Database } from 'sqlite3'
SQLite3Database = sqlite3.Database import path from 'path'
path = require "path" import mkdirp from 'mkdirp'
mkdirp = require "mkdirp" import SimpleIni from 'simple-ini'
SimpleIni = require "simple-ini" import fs from 'fs'
fs = require "fs" import merge from 'merge'
merge = require "merge"
getLogger = require "./logger" import getLogger from './logger'
# some properties sugar from http://bl.ocks.org/joyrexus/65cb3780a24ecd50f6df # some properties sugar from http://bl.ocks.org/joyrexus/65cb3780a24ecd50f6df
Function::getter = (prop, get) -> Function::getter = (prop, get) ->
Object.defineProperty @prototype, prop, {get, configurable: yes} Object.defineProperty @prototype, prop, {get, configurable: yes}
Function::setter = (prop, set) -> Function::setter = (prop, set) ->
Object.defineProperty @prototype, prop, {set, configurable: yes} Object.defineProperty @prototype, prop, {set, configurable: yes}
module.exports = class SettingsFile module.exports = class SettingsFile
db: null db: null
@ -26,21 +26,28 @@ module.exports = class SettingsFile
catch err catch err
throw new Error "Could not create TS3 config directory." 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 "isInitialized", ->
@getter "isReady", -> () => @db != null () =>
fs.existsSync(path.join(@configPath, "settings.db")) and
fs.existsSync(path.join(@configPath, "ts3clientui_qt.secrets.conf"))
@getter "isReady", ->
() =>
@db != null
open: (cb) => open: (cb) =>
# settings database # settings database
@db = new SQLite3Database path.join(@configPath, "settings.db") @db = new SQLite3Database path.join(@configPath, "settings.db")
await @db.serialize defer() await @db.serialize defer()
await @query "CREATE TABLE IF NOT EXISTS TS3Tables (key varchar NOT NULL UNIQUE,timestamp integer unsigned NOT NULL)", defer() await @query "CREATE TABLE IF NOT EXISTS TS3Tables "+
"(key varchar NOT NULL UNIQUE,timestamp integer unsigned NOT NULL)",
defer()
# secrets file # secrets file
@identities = [] @identities = []
@defaultIdentity = null @defaultIdentity = null
secretsPath = path.join(@configPath, "ts3clientui_qt.secrets.conf") secretsPath = path.join(@configPath, "ts3clientui_qt.secrets.conf")
if fs.existsSync(secretsPath) if fs.existsSync(secretsPath)
secrets = new SimpleIni (() => fs.readFileSync(secretsPath, "utf-8")), secrets = new SimpleIni (-> fs.readFileSync(secretsPath, "utf-8")),
quotedValues: false quotedValues: false
for i in [1 .. secrets.Identities.size] for i in [1 .. secrets.Identities.size]
@identities.push @identities.push
@ -75,7 +82,8 @@ module.exports = class SettingsFile
# Generate INI content # Generate INI content
await secrets.save defer(iniText) await secrets.save defer(iniText)
fs.writeFileSync path.join(@configPath, "ts3clientui_qt.secrets.conf"), iniText fs.writeFileSync path.join(@configPath, "ts3clientui_qt.secrets.conf"),
iniText
@identities = null @identities = null
@defaultIdentity = null @defaultIdentity = null
@ -98,7 +106,9 @@ module.exports = class SettingsFile
if not table if not table
throw new Error "Need 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() await @query "create table if not exists #{table} "+
"(timestamp integer unsigned NOT NULL, key varchar NOT NULL UNIQUE, "+
"value varchar)", defer()
if not key if not key
return return
@ -112,11 +122,13 @@ module.exports = class SettingsFile
timestamp = Math.round (new Date).getTime() / 1000 timestamp = Math.round (new Date).getTime() / 1000
stmt = @db.prepare "insert or replace into TS3Tables (key, timestamp) values (?, ?)" stmt = @db.prepare "insert or replace into TS3Tables (key, timestamp) "+
"values (?, ?)"
stmt.run table, timestamp stmt.run table, timestamp
await stmt.finalize defer() await stmt.finalize defer()
stmt = @db.prepare "insert or replace into #{table} (timestamp, key, value) values (?, ?, ?)" stmt = @db.prepare "insert or replace into #{table} (timestamp, key, value) "+
"values (?, ?, ?)"
stmt.run timestamp, key, value stmt.run timestamp, key, value
await stmt.finalize defer() await stmt.finalize defer()
@ -141,7 +153,7 @@ module.exports = class SettingsFile
@log.info "Importing identity from #{identityFilePath}..." @log.info "Importing identity from #{identityFilePath}..."
# open identity file # open identity file
idFile = new SimpleIni (() => fs.readFileSync(identityFilePath, "utf-8")), idFile = new SimpleIni (-> fs.readFileSync(identityFilePath, "utf-8")),
quotedValues: true quotedValues: true
importedIdentity = {} importedIdentity = {}
for own k, v of idFile.Identity for own k, v of idFile.Identity

View File

@ -1,16 +1,17 @@
Sync = require "sync" import Sync from 'sync'
import { spawn } from 'child_process'
import StreamSplitter from 'stream-splitter'
log = require("./logger")("X11tools") import getLogger from './logger'
spawn = require("child_process").spawn import services from './services'
services = require("./services") import require_bin from './require_bin'
StreamSplitter = require("stream-splitter")
require_bin = require("./require_bin")
log = getLogger "X11tools"
xdotoolBinPath = require_bin "xdotool", false xdotoolBinPath = require_bin "xdotool", false
# Just some tools to work with the X11 windows # Just some tools to work with the X11 windows
module.exports = module.exports =
getWindowIdByProcessId: (pid, cb) => getWindowIdByProcessId: (pid, cb) ->
wid = null wid = null
# Return null instantly if xdotool is not available # Return null instantly if xdotool is not available
@ -18,21 +19,23 @@ module.exports =
cb? new Error "xdotool is not available" cb? new Error "xdotool is not available"
return return
# We provide --name due to the bug mentioned at https://github.com/jordansissel/xdotool/issues/14 # We provide --name due to the bug mentioned at
xdoproc = spawn xdotoolBinPath, [ "search", "--any", "--pid", pid, "--name", "xdosearch" ], # https://github.com/jordansissel/xdotool/issues/14
xdoproc = spawn xdotoolBinPath, [
"search", "--any", "--pid", pid, "--name", "xdosearch" ],
env: env:
DISPLAY: process.env.DISPLAY DISPLAY: process.env.DISPLAY
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR
stdoutTokenizer = xdoproc.stdout.pipe StreamSplitter "\n" stdoutTokenizer = xdoproc.stdout.pipe StreamSplitter "\n"
stdoutTokenizer.encoding = "utf8"; stdoutTokenizer.encoding = "utf8"
stdoutTokenizer.on "token", (token) => stdoutTokenizer.on "token", (token) ->
token = token.trim() # get rid of \r token = token.trim() # get rid of \r
newWid = parseInt(token) newWid = parseInt(token)
if newWid != 0 and wid == null if newWid != 0 and wid == null
wid = newWid wid = newWid
stderrTokenizer = xdoproc.stderr.pipe StreamSplitter "\n" stderrTokenizer = xdoproc.stderr.pipe StreamSplitter "\n"
stderrTokenizer.encoding = "utf8"; stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", (token) => stderrTokenizer.on "token", (token) ->
token = token.trim() # get rid of \r token = token.trim() # get rid of \r
log.warn token log.warn token
await xdoproc.on "exit", defer(e) await xdoproc.on "exit", defer(e)
@ -45,9 +48,10 @@ module.exports =
cb? null, parseInt(wid) cb? null, parseInt(wid)
getWindowIdByProcessIdSync: (pid) => Sync() => @getWindowIdByProcessId.sync @, pid getWindowIdByProcessIdSync: (pid) ->
Sync() -> @getWindowIdByProcessId.sync @, pid
sendKeys: (wid, keys, cb) => sendKeys: (wid, keys, cb) ->
# Do not bother trying if xdotool is not available # Do not bother trying if xdotool is not available
if not xdotoolBinPath? if not xdotoolBinPath?
cb? new Error "xdotool not available." cb? new Error "xdotool not available."
@ -61,18 +65,24 @@ module.exports =
cb? new Error "Could not start a window manager." cb? new Error "Could not start a window manager."
return return
xdoproc = spawn xdotoolBinPath, [ "windowactivate", "--sync", wid, "key", "--clearmodifiers", "--delay", "100" ].concat(keys), xdoproc = spawn xdotoolBinPath, [
"windowactivate",
"--sync",
wid,
"key",
"--clearmodifiers",
"--delay", "100" ].concat(keys),
env: env:
DISPLAY: process.env.DISPLAY DISPLAY: process.env.DISPLAY
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR
stdoutTokenizer = xdoproc.stdout.pipe StreamSplitter "\n" stdoutTokenizer = xdoproc.stdout.pipe StreamSplitter "\n"
stdoutTokenizer.encoding = "utf8"; stdoutTokenizer.encoding = "utf8"
stdoutTokenizer.on "token", (token) => stdoutTokenizer.on "token", (token) ->
token = token.trim() # get rid of \r token = token.trim() # get rid of \r
log.debug token log.debug token
stderrTokenizer = xdoproc.stderr.pipe StreamSplitter "\n" stderrTokenizer = xdoproc.stderr.pipe StreamSplitter "\n"
stderrTokenizer.encoding = "utf8"; stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", (token) => stderrTokenizer.on "token", (token) ->
token = token.trim() # get rid of \r token = token.trim() # get rid of \r
log.warn token log.warn token
await xdoproc.on "exit", defer(e) await xdoproc.on "exit", defer(e)

File diff suppressed because it is too large Load Diff