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
**/.gitignore
# Logs
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
src/node_modules
**/*.log
**/*.ini
**/*.md

View File

@ -1,7 +1,6 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
@ -17,12 +16,14 @@ coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
# 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
# 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

View File

@ -1,10 +1,10 @@
{
"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.",
"main": "app.js",
"main": "dist/node7/index.js",
"bin": {
"ts3bot": "app.js"
"ts3bot": "dist/node7/index.js"
},
"keywords": [
"teamspeak",
@ -20,15 +20,22 @@
"media",
"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+",
"repository": {
"type": "git",
"url": "https://github.com/icedream/ts3bot-control.git"
"url": "https://github.com/icedream/ts3bot.git"
},
"dependencies": {
"express": "^4.13.3",
"iced-coffee-script": "^108.0.8",
"merge": "^1.2.0",
"mkdirp": "^0.5.1",
"named-regexp": "^0.1.1",
@ -49,7 +56,17 @@
"webchimera.js": "^0.2.7",
"which": "^1.1.2",
"winston": "^2.3.1",
"xvfb": "git://github.com/icedream/node-xvfb.git",
"xvfb": "^0.2.3",
"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"
path = require "path"
merge = require "merge"
pwgen = require "password-generator"
import nconf from 'nconf'
import path from 'path'
import merge from 'merge'
import pwgen from 'password-generator'
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/register");
require("iced-coffee-script-3/register");
Sync = require("sync");
var services = require("./services");
@ -19,7 +17,7 @@ if (process.platform === "win32") {
});
}
app = require("./app.iced");
app = require("./app");
doShutdownAsync = function(cb) {
log.info("App shutdown starting...");

View File

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

View File

@ -1,5 +1,5 @@
parseDuration = require "parse-duration"
namedRegex = require("named-regexp").named
import parseDuration from 'parse-duration'
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]*)?$/

View File

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

View File

@ -3,4 +3,4 @@ module.exports = (a, b) ->
return -1; # a before b
if b.dependencies.indexOf(a.name) >= 0
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"
EventEmitter = require("events").EventEmitter
merge = require "merge"
services = require "./services"
import getLogger from './logger'
import services from './services'
module.exports = class Service extends EventEmitter
class Service extends EventEmitter
constructor: (@name, funcs) ->
@log = getLogger @name
@_funcs = funcs
@ -152,3 +152,5 @@ module.exports = class Service extends EventEmitter
cb? err
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...
merge = require "merge"
getLogger = require("./logger")
EventEmitter = require("events").EventEmitter
log = getLogger("ServiceMgr")
Sync = require "sync"
import merge from 'merge'
import { EventEmitter } from 'events'
import Sync from '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 =
services: []
@ -52,7 +56,7 @@ module.exports =
shutdownSync: () -> Sync () => @shutdown.sync @
# base class for all services
module.exports.Service = require "./service_template"
module.exports.Service = Service
# register services
services = [

View File

@ -1,10 +1,12 @@
spawn = require("child_process").spawn
log = require("../logger")("PulseAudio")
services = require("../services")
config = require("../config")
StreamSplitter = require("stream-splitter")
require_bin = require("../require_bin")
import { spawn } from 'child_process'
import StreamSplitter from 'stream-splitter'
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")
pacmdPath = require_bin "pacmd"
@ -18,7 +20,7 @@ module.exports = class PulseAudioService extends services.Service
return
# logging
forwardLog = (token) =>
forwardLog = (token) ->
token = token.trim() # get rid of \r
level = token.substring(0, 1).toUpperCase()
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
proc = spawn pulseaudioPath, [ "--check" ], opts
stderrTokenizer = proc.stderr.pipe StreamSplitter "\n"
stderrTokenizer.encoding = "utf8";
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"}"
@ -74,7 +76,7 @@ module.exports = class PulseAudioService extends services.Service
stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", tokenHandler
proc.on "exit", () =>
proc.on "exit", () ->
if not calledCallback
calledCallback = true
cb? new Error "PulseAudio daemon terminated unexpectedly."
@ -89,11 +91,11 @@ module.exports = class PulseAudioService extends services.Service
cb?()
findIndexForProcessId: (pid, cb) => throw new Error "Not implemented yet"
findIndexForProcessId: (pid, cb) -> throw new Error "Not implemented yet"
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
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"
import sqlite3, { Database as SQLite3Database } from 'sqlite3'
import path from 'path'
import mkdirp from 'mkdirp'
import SimpleIni from 'simple-ini'
import fs from 'fs'
import merge from 'merge'
import getLogger from './logger'
# some properties sugar from http://bl.ocks.org/joyrexus/65cb3780a24ecd50f6df
Function::getter = (prop, get) ->
Object.defineProperty @prototype, prop, {get, configurable: yes}
Object.defineProperty @prototype, prop, {get, configurable: yes}
Function::setter = (prop, set) ->
Object.defineProperty @prototype, prop, {set, configurable: yes}
Object.defineProperty @prototype, prop, {set, configurable: yes}
module.exports = class SettingsFile
db: null
@ -26,21 +26,28 @@ module.exports = class SettingsFile
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
@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()
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")),
secrets = new SimpleIni (-> fs.readFileSync(secretsPath, "utf-8")),
quotedValues: false
for i in [1 .. secrets.Identities.size]
@identities.push
@ -75,7 +82,8 @@ module.exports = class SettingsFile
# Generate INI content
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
@defaultIdentity = null
@ -98,7 +106,9 @@ module.exports = class SettingsFile
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()
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
@ -112,11 +122,13 @@ module.exports = class SettingsFile
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
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
await stmt.finalize defer()
@ -141,7 +153,7 @@ module.exports = class SettingsFile
@log.info "Importing identity from #{identityFilePath}..."
# open identity file
idFile = new SimpleIni (() => fs.readFileSync(identityFilePath, "utf-8")),
idFile = new SimpleIni (-> fs.readFileSync(identityFilePath, "utf-8")),
quotedValues: true
importedIdentity = {}
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")
spawn = require("child_process").spawn
services = require("./services")
StreamSplitter = require("stream-splitter")
require_bin = require("./require_bin")
import getLogger from './logger'
import services from './services'
import require_bin from './require_bin'
log = getLogger "X11tools"
xdotoolBinPath = require_bin "xdotool", false
# Just some tools to work with the X11 windows
module.exports =
getWindowIdByProcessId: (pid, cb) =>
getWindowIdByProcessId: (pid, cb) ->
wid = null
# Return null instantly if xdotool is not available
@ -18,21 +19,23 @@ module.exports =
cb? new Error "xdotool is not available"
return
# We provide --name due to the bug mentioned at https://github.com/jordansissel/xdotool/issues/14
xdoproc = spawn xdotoolBinPath, [ "search", "--any", "--pid", pid, "--name", "xdosearch" ],
# 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) =>
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) =>
stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", (token) ->
token = token.trim() # get rid of \r
log.warn token
await xdoproc.on "exit", defer(e)
@ -45,9 +48,10 @@ module.exports =
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
if not xdotoolBinPath?
cb? new Error "xdotool not available."
@ -61,18 +65,24 @@ module.exports =
cb? new Error "Could not start a window manager."
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:
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) =>
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) =>
stderrTokenizer.encoding = "utf8"
stderrTokenizer.on "token", (token) ->
token = token.trim() # get rid of \r
log.warn token
await xdoproc.on "exit", defer(e)

File diff suppressed because it is too large Load Diff