mirror of https://github.com/icedream/ts3bot.git
commit
7160acee75
|
@ -0,0 +1,55 @@
|
||||||
|
# TS3Bot
|
||||||
|
|
||||||
|
A new and easy way to set up your own TeamSpeak 3 bot!
|
||||||
|
|
||||||
|
This repository contains the Node.js/IcedCoffeeScript code needed for the TS3Bot Docker image.
|
||||||
|
|
||||||
|
## Running TS3Bot without Docker
|
||||||
|
|
||||||
|
Basically these instructions are derived from [the Docker build files](https://github.com/icedream/ts3bot-docker).
|
||||||
|
You can adopt these depending on your OS (the Docker image uses Debian Jessie, so the instructions below are for
|
||||||
|
that OS).
|
||||||
|
|
||||||
|
We assume that we want to run the "develop" branch of TS3Bot here. You can easily replace "develop" with another branch you want to run, like "master" for the stable code.
|
||||||
|
|
||||||
|
Commands being run as your own user are marked with `$` and commands being run as root are marked with `#`.
|
||||||
|
|
||||||
|
- Install the dependencies:
|
||||||
|
|
||||||
|
$ apt-get install blackbox xvfb xdotool pulseaudio pulseaudio-utils cmake libvlc-dev vlc-plugin-pulse
|
||||||
|
|
||||||
|
- Create a new user to run TS3Bot on - do not run this on your own user if you use TeamSpeak3 on it as that will overwrite your configuration!
|
||||||
|
|
||||||
|
# adduser --system --disabled-login --disabled-password ts3bot
|
||||||
|
|
||||||
|
- Download and unpack TeamSpeak3 client for your platform into a folder accessible by the TS3Bot user. Only read access is required. Replace `3.0.18.2` with whatever version of TeamSpeak3 you prefer to install, usually that is the most recent one. Accept the license that shows up in the process.
|
||||||
|
|
||||||
|
# wget -Ots3client.run http://dl.4players.de/ts/releases/3.0.18.2/TeamSpeak3-Client-linux_$(uname -p)-3.0.18.2.run
|
||||||
|
# chmod +x ts3client.run
|
||||||
|
# ./ts3client.run --target ~ts3bot/ts3client
|
||||||
|
# rm ts3client.run
|
||||||
|
|
||||||
|
- Download the TS3Bot control application into your TS3Bot user's home folder. The TS3Bot user itself only needs read access to the code. You can do this in two ways:
|
||||||
|
o By downloading the tar.gz archive from GitHub and unpacking it.
|
||||||
|
|
||||||
|
# wget -q -O- https://github.com/icedream/ts3bot-control/archive/develop.tar.gz | tar xz -C ~ts3bot
|
||||||
|
|
||||||
|
o By cloning the Git repository from GitHub.
|
||||||
|
|
||||||
|
# git clone https://github.com/icedream/ts3bot-control -b develop ~ts3bot/ts3bot-control-develop
|
||||||
|
|
||||||
|
- Install the Node.js dependencies using `npm`. Note how a simple `npm install` will install the wrong version of WebChimera.js and you need to provide it with correct Node.js information (environment variables `npm_config_wcjs_runtime` and `npm_config_wcjs_runtime_version`) like this:
|
||||||
|
|
||||||
|
# cd ~ts3bot/ts3bot-control-develop
|
||||||
|
# npm_config_wcjs_runtime="node" npm_config_wcjs_runtime_version="$(node --version | tr -d 'v')" npm install
|
||||||
|
|
||||||
|
- Now set up your TS3Bot configuration files in your TS3Bot user's home folder. For this create a folder `.ts3bot` in the home directory and put a `config.json` with your configuration there. The most minimal configuration consists of:
|
||||||
|
o `identity-path` - The path to your identity INI file, export a newly generated identity from your own TeamSpeak3 client for that.
|
||||||
|
o `ts3-install-path` - The path where you installed the TeamSpeak3 client to, you can skip this if you used exactly the same path as in the instructions above.
|
||||||
|
o `ts3-server` - The URL to the server/channel you want the bot to connect to, you can get from your own TS3 client via "Extras" > "Invite friend", select the checkbox "Channel" and select "ts3server link" as invitation type.
|
||||||
|
|
||||||
|
Running the bot can finally be done like this:
|
||||||
|
|
||||||
|
$ sudo -u ts3bot -H node ~ts3bot/ts3bot-control-develop
|
||||||
|
|
||||||
|
You can provide your configuration as command line arguments instead if you want, that can be useful for just temporary configuration you want to test. For that just append the configuration options to the command line above and prefix every command line option with `--`, for example for `ts3-install-path` you would write `--ts3-install-path=/your/path/to/ts3client`
|
278
app.iced
278
app.iced
|
@ -8,6 +8,9 @@ request = require "request"
|
||||||
fs = require("fs")
|
fs = require("fs")
|
||||||
path = require("path")
|
path = require("path")
|
||||||
qs = require "querystring"
|
qs = require "querystring"
|
||||||
|
temp = require("temp").track()
|
||||||
|
youtubedl = require "youtube-dl"
|
||||||
|
isValidUrl = (require "valid-url").isWebUri
|
||||||
|
|
||||||
log = getLogger "Main"
|
log = getLogger "Main"
|
||||||
|
|
||||||
|
@ -16,13 +19,6 @@ removeBB = (str) -> str.replace /\[(\w+)[^\]]*](.*?)\[\/\1]/g, "$2"
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
shutdown: (cb) =>
|
shutdown: (cb) =>
|
||||||
apiService = services.find("api")
|
|
||||||
if apiService and apiService.state == "started"
|
|
||||||
await apiService.stop defer(err)
|
|
||||||
if err
|
|
||||||
cb? new Error "Could not stop API"
|
|
||||||
return
|
|
||||||
|
|
||||||
ts3clientService = services.find("ts3client")
|
ts3clientService = services.find("ts3client")
|
||||||
if ts3clientService and ts3clientService.state == "started"
|
if ts3clientService and ts3clientService.state == "started"
|
||||||
await ts3clientService.stop defer(err)
|
await ts3clientService.stop defer(err)
|
||||||
|
@ -39,10 +35,37 @@ module.exports =
|
||||||
cb?()
|
cb?()
|
||||||
shutdownSync: => Sync @shutdown
|
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
|
# PulseAudio daemon
|
||||||
await services.find("pulseaudio").start defer err
|
await services.find("pulseaudio").start defer err
|
||||||
if err
|
if err
|
||||||
log.warn "PulseAudio could not start up, audio may not act as expected!"
|
log.warn "PulseAudio could not start up, audio may not act as expected!", err
|
||||||
|
|
||||||
|
# VLC via WebChimera.js
|
||||||
|
vlcService = services.find("vlc")
|
||||||
|
await vlcService.start defer err, vlc
|
||||||
|
if err
|
||||||
|
log.warn "VLC could not start up!", 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
|
# TeamSpeak3
|
||||||
ts3clientService = services.find("ts3client")
|
ts3clientService = services.find("ts3client")
|
||||||
|
@ -50,6 +73,32 @@ ts3clientService = services.find("ts3client")
|
||||||
ts3clientService.on "started", (ts3proc) =>
|
ts3clientService.on "started", (ts3proc) =>
|
||||||
ts3query = ts3clientService.query
|
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.currentScHandlerID = 1
|
||||||
ts3query.mydata = {}
|
ts3query.mydata = {}
|
||||||
|
|
||||||
|
@ -114,6 +163,9 @@ ts3clientService.on "started", (ts3proc) =>
|
||||||
ts3query.on "message.notifytextmessage", (args) =>
|
ts3query.on "message.notifytextmessage", (args) =>
|
||||||
await ts3query.use args.schandlerid, defer(err, data)
|
await ts3query.use args.schandlerid, defer(err, data)
|
||||||
|
|
||||||
|
if not args.msg?
|
||||||
|
return
|
||||||
|
|
||||||
msg = args.msg
|
msg = args.msg
|
||||||
invoker = { name: args.invokername, uid: args.invokeruid, id: args.invokerid }
|
invoker = { name: args.invokername, uid: args.invokeruid, id: args.invokerid }
|
||||||
targetmode = args.targetmode # 1 = private, 2 = channel
|
targetmode = args.targetmode # 1 = private, 2 = channel
|
||||||
|
@ -134,61 +186,177 @@ ts3clientService.on "started", (ts3proc) =>
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
switch name.toLowerCase()
|
switch name.toLowerCase()
|
||||||
when "play"
|
when "current"
|
||||||
q =
|
item = vlc.playlist.items[vlc.playlist.currentItem]
|
||||||
uid: invoker.uid
|
info = vlcMediaInfo[item.mrl]
|
||||||
input: removeBB paramline
|
url = info?.originalUrl or item.mrl
|
||||||
await request "http://127.0.0.1:16444/play?#{qs.stringify q}", defer(err, response)
|
title = info?.title or item.mrl
|
||||||
switch response.statusCode
|
ts3query?.sendtextmessage 2, 0, "Currently playing [URL=#{url}]#{title}[/URL]."
|
||||||
when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing #{paramline}."
|
|
||||||
when 400 then ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with what you wrote. Maybe check the URL/sound name you provided?"
|
# Restore audio volume
|
||||||
when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to play #{q.input} via the bot."
|
vlc.audio.volume = vlcVolume
|
||||||
else
|
when "pause"
|
||||||
log.warn "API reported error", response.statusCode, err
|
# now we can toggle-pause playback this easily! yay!
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later."
|
vlc.togglePause()
|
||||||
when "stop"
|
|
||||||
q =
|
|
||||||
uid: invoker.uid
|
|
||||||
await request "http://127.0.0.1:16444/stop?#{qs.stringify q}", defer(err, response)
|
|
||||||
switch response.statusCode
|
|
||||||
when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback."
|
|
||||||
when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to do that."
|
|
||||||
else
|
|
||||||
log.warn "API reported error", response.statusCode, err
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later."
|
|
||||||
when "setvolume"
|
|
||||||
q =
|
|
||||||
uid: invoker.uid
|
|
||||||
volume: parseFloat paramline
|
|
||||||
await request "http://127.0.0.1:16444/setvolume?#{qs.stringify q}", defer(err, response)
|
|
||||||
switch response.statusCode
|
|
||||||
when 200 then ts3query.sendtextmessage args.targetmode, invoker.id, "Set volume to #{q.volume}"
|
|
||||||
when 400 then ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with what you wrote. Maybe check the volume? It's supposed to be a floating-point number between 0 and 2."
|
|
||||||
when 403 then ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, you're not allowed to do that."
|
|
||||||
else
|
|
||||||
log.warn "API reported error", response.statusCode, err
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later."
|
|
||||||
when "changenick"
|
|
||||||
nick = if paramline.length > params[0].length then paramline else params[0]
|
|
||||||
if nick.length < 1 or nick.length > 32
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Invalid nickname."
|
|
||||||
return
|
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 "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 () =>
|
Sync () =>
|
||||||
try
|
try
|
||||||
ts3query.clientupdate.sync ts3query, { client_nickname: nick }
|
ts3query.clientupdate.sync ts3query, { client_nickname: nick }
|
||||||
catch err
|
catch err
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out."
|
|
||||||
log.warn "ChangeNick failed, error information:", 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)
|
await ts3clientService.start [ config.get("ts3-server") ], defer(err, ts3proc)
|
||||||
if err
|
if err
|
||||||
log.error "TeamSpeak3 could not start, shutting down."
|
log.error "TeamSpeak3 could not start, shutting down.", err
|
||||||
await module.exports.shutdown defer()
|
|
||||||
process.exit 1
|
|
||||||
|
|
||||||
# HTTP API
|
|
||||||
await services.find("api").start defer err
|
|
||||||
if err
|
|
||||||
log.error "API could not start up, shutting down!"
|
|
||||||
await module.exports.shutdown defer()
|
await module.exports.shutdown defer()
|
||||||
process.exit 1
|
process.exit 1
|
23
app.js
23
app.js
|
@ -1,3 +1,5 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
require("iced-coffee-script/register");
|
require("iced-coffee-script/register");
|
||||||
Sync = require("sync");
|
Sync = require("sync");
|
||||||
|
|
||||||
|
@ -33,6 +35,7 @@ doShutdownAsync = function(cb) {
|
||||||
process.on("uncaughtException", function(err) {
|
process.on("uncaughtException", function(err) {
|
||||||
log.error("Shutting down due to an uncaught exception!", err);
|
log.error("Shutting down due to an uncaught exception!", err);
|
||||||
app.shutdownSync();
|
app.shutdownSync();
|
||||||
|
process.exit(0xFF);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("exit", function(e) {
|
process.on("exit", function(e) {
|
||||||
|
@ -42,25 +45,35 @@ process.on("exit", function(e) {
|
||||||
|
|
||||||
process.on("SIGTERM", function(e) {
|
process.on("SIGTERM", function(e) {
|
||||||
log.debug("Caught SIGTERM signal");
|
log.debug("Caught SIGTERM signal");
|
||||||
app.shutdown();
|
app.shutdown(function() {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", function() {
|
process.on("SIGINT", function() {
|
||||||
log.debug("Caught SIGINT signal");
|
log.debug("Caught SIGINT signal");
|
||||||
app.shutdown();
|
app.shutdown(function() {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGHUP", function() {
|
process.on("SIGHUP", function() {
|
||||||
log.debug("Caught SIGHUP signal");
|
log.debug("Caught SIGHUP signal");
|
||||||
app.shutdown();
|
app.shutdown(function() {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGQUIT", function() {
|
process.on("SIGQUIT", function() {
|
||||||
log.debug("Caught SIGQUIT signal");
|
log.debug("Caught SIGQUIT signal");
|
||||||
app.shutdown();
|
app.shutdown(function() {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGABRT", function() {
|
process.on("SIGABRT", function() {
|
||||||
log.debug("Caught SIGABRT signal");
|
log.debug("Caught SIGABRT signal");
|
||||||
app.shutdown();
|
app.shutdown(function() {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,9 @@ nconf.defaults
|
||||||
if not nconf.get("ts3-server")
|
if not nconf.get("ts3-server")
|
||||||
throw new Error "You need to provide a TeamSpeak3 server URL (starts with ts3server:// and can be generated from any TS3 client GUI)."
|
throw new Error "You need to provide a TeamSpeak3 server URL (starts with ts3server:// and can be generated from any TS3 client GUI)."
|
||||||
|
|
||||||
|
if nconf.get("nickname")? and (nconf.get("nickname").length < 3 or nconf.get("nickname").length > 30)
|
||||||
|
throw new Error "Nickname must be between 3 and 30 characters long."
|
||||||
|
|
||||||
console.log "Configuration loaded."
|
console.log "Configuration loaded."
|
||||||
|
|
||||||
if nconf.get "dump-config"
|
if nconf.get "dump-config"
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "ts3bot",
|
"name": "ts3bot",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
"description": "Allows running TeamSpeak3 as a bot for all kinds of media (local music/videos/streams/YouTube/...) without the need for a real GUI to exist.",
|
"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": "app.js",
|
||||||
|
"bin": {
|
||||||
|
"ts3bot": "app.js"
|
||||||
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"teamspeak",
|
"teamspeak",
|
||||||
"teamspeak3",
|
"teamspeak3",
|
||||||
|
@ -38,8 +41,9 @@
|
||||||
"stream-splitter": "^0.3.2",
|
"stream-splitter": "^0.3.2",
|
||||||
"string.prototype.startswith": "^0.2.0",
|
"string.prototype.startswith": "^0.2.0",
|
||||||
"sync": "^0.2.5",
|
"sync": "^0.2.5",
|
||||||
|
"temp": "^0.8.3",
|
||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
"vlc-api": "0.0.0",
|
"webchimera.js": "^0.1.38",
|
||||||
"which": "^1.1.2",
|
"which": "^1.1.2",
|
||||||
"winston": "^1.0.1",
|
"winston": "^1.0.1",
|
||||||
"xvfb": "git://github.com/icedream/node-xvfb.git",
|
"xvfb": "git://github.com/icedream/node-xvfb.git",
|
||||||
|
|
|
@ -2,7 +2,9 @@ which = require("which").sync
|
||||||
path = require "path"
|
path = require "path"
|
||||||
log = require("./logger")("RequireBin")
|
log = require("./logger")("RequireBin")
|
||||||
|
|
||||||
module.exports = (binName) =>
|
module.exports = (binName, doErrorIfNotFound) =>
|
||||||
|
doErrorIfNotFound = true unless doErrorIfNotFound?
|
||||||
|
|
||||||
# check if xvfb is findable from here
|
# check if xvfb is findable from here
|
||||||
if path.resolve(binName) == path.normalize(binName)
|
if path.resolve(binName) == path.normalize(binName)
|
||||||
# this is an absolute path
|
# this is an absolute path
|
||||||
|
@ -14,5 +16,9 @@ module.exports = (binName) =>
|
||||||
log.debug "#{binName} detected:", binPath
|
log.debug "#{binName} detected:", binPath
|
||||||
return binPath
|
return binPath
|
||||||
catch err
|
catch err
|
||||||
log.error "#{binName} could not be found.", err
|
if doErrorIfNotFound
|
||||||
|
log.error "#{binName} could not be found."
|
||||||
throw new Error "#{binName} could not be found."
|
throw new Error "#{binName} could not be found."
|
||||||
|
else
|
||||||
|
log.warn "#{binName} could not be found."
|
||||||
|
return null
|
||||||
|
|
|
@ -19,7 +19,7 @@ module.exports = class Service extends EventEmitter
|
||||||
|
|
||||||
state: "stopped"
|
state: "stopped"
|
||||||
|
|
||||||
start: () => @_start.apply @, [ false ].concat Array.prototype.slice.call(arguments)
|
start: () => @_start false, arguments...
|
||||||
|
|
||||||
startSync: () => Sync () => @start.sync @
|
startSync: () => Sync () => @start.sync @
|
||||||
|
|
||||||
|
@ -78,14 +78,14 @@ module.exports = class Service extends EventEmitter
|
||||||
if not quiet
|
if not quiet
|
||||||
@log.info "Started #{@name}"
|
@log.info "Started #{@name}"
|
||||||
|
|
||||||
@_lastArgs = args
|
@_lastArgs = serviceArgs
|
||||||
|
|
||||||
@state = "started"
|
@state = "started"
|
||||||
@emit "started", service
|
@emit "started", service
|
||||||
|
|
||||||
cb? null, service
|
cb? null, service
|
||||||
|
|
||||||
stop: () => @_stop.apply @, [ false ].concat Array.prototype.slice.call(arguments)
|
stop: () => @_stop false, arguments...
|
||||||
|
|
||||||
stopSync: () => Sync () => @stop.sync @
|
stopSync: () => Sync () => @stop.sync @
|
||||||
|
|
||||||
|
|
|
@ -56,12 +56,11 @@ module.exports.Service = require "./service_template"
|
||||||
|
|
||||||
# register services
|
# register services
|
||||||
services = [
|
services = [
|
||||||
new(require "./services/api")
|
|
||||||
new(require "./services/pulseaudio")
|
new(require "./services/pulseaudio")
|
||||||
new(require "./services/ts3client")
|
new(require "./services/ts3client")
|
||||||
new(require "./services/vlc")
|
new(require "./services/vlc")
|
||||||
new(require "./services/xvfb")
|
new(require "./services/xvfb")
|
||||||
new(require "./services/blackbox")
|
new(require "./services/xwm")
|
||||||
]
|
]
|
||||||
services.sort require("./service_depcomparer") # sort services by dependency
|
services.sort require("./service_depcomparer") # sort services by dependency
|
||||||
for service in services
|
for service in services
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
express = require "express"
|
|
||||||
url = require "url"
|
|
||||||
path = require "path"
|
|
||||||
spawn = require("child_process").spawn
|
|
||||||
net = require "net"
|
|
||||||
Socket = net.Socket
|
|
||||||
getLogger = require "../logger"
|
|
||||||
config = require "../config"
|
|
||||||
log = getLogger "API"
|
|
||||||
youtubedl = require "youtube-dl"
|
|
||||||
#PulseAudio = require "pulseaudio"
|
|
||||||
isValidUrl = (require "valid-url").isWebUri
|
|
||||||
|
|
||||||
services = require "../services"
|
|
||||||
|
|
||||||
module.exports = class APIService extends services.Service
|
|
||||||
dependencies: [
|
|
||||||
"pulseaudio"
|
|
||||||
"vlc"
|
|
||||||
"ts3client"
|
|
||||||
]
|
|
||||||
constructor: () -> super "API",
|
|
||||||
start: (cb) ->
|
|
||||||
if @httpServer
|
|
||||||
cb? null
|
|
||||||
return
|
|
||||||
|
|
||||||
vlc = services.find("vlc").instance
|
|
||||||
ts3query = services.find("ts3client").query
|
|
||||||
|
|
||||||
# set up HTTP server
|
|
||||||
log.debug "Starting up HTTP API..."
|
|
||||||
app = express()
|
|
||||||
app.get "/play", (req, res) =>
|
|
||||||
if not req.query.uid
|
|
||||||
log.debug "Didn't get a UID, sending forbidden"
|
|
||||||
res.status(400).send("Forbidden")
|
|
||||||
return
|
|
||||||
if not req.query.input
|
|
||||||
log.debug "Didn't get an input URI/alias, sending bad request"
|
|
||||||
res.status(400).send("Bad request")
|
|
||||||
return
|
|
||||||
|
|
||||||
input = null
|
|
||||||
# only allow playback from file if it's a preconfigured alias
|
|
||||||
if isValidUrl req.query.input
|
|
||||||
log.debug "Got input URL:", req.query.input
|
|
||||||
input = req.query.input
|
|
||||||
else
|
|
||||||
input = config.get("aliases:#{req.query.input}")
|
|
||||||
if not(isValidUrl input) and not(fs.existsSync input)
|
|
||||||
log.debug "Got neither valid URL nor valid alias:", req.query.input
|
|
||||||
res.status(403).send("Forbidden")
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO: permission system to check if uid is allowed to play this url or alias
|
|
||||||
|
|
||||||
await vlc.status.empty defer(err)
|
|
||||||
if err
|
|
||||||
res.status(503).send("Something went wrong")
|
|
||||||
log.warn "VLC API returned an error when trying to empty", err
|
|
||||||
return
|
|
||||||
|
|
||||||
# let's give youtube-dl a shot!
|
|
||||||
await youtubedl.getInfo input, [
|
|
||||||
"--format=bestaudio"
|
|
||||||
], defer(err, info)
|
|
||||||
if err or not info?
|
|
||||||
await youtubedl.getInfo input, [
|
|
||||||
"--format=best"
|
|
||||||
], defer(err, info)
|
|
||||||
if err or not info?
|
|
||||||
info =
|
|
||||||
url: input
|
|
||||||
if not info.url?
|
|
||||||
info.url = input
|
|
||||||
info.title = input # URL as title
|
|
||||||
|
|
||||||
await vlc.status.play info.url, defer(err)
|
|
||||||
if err
|
|
||||||
vlc.status.empty()
|
|
||||||
res.status(503).send("Something went wrong")
|
|
||||||
log.warn "VLC API returned an error when trying to play", err
|
|
||||||
return
|
|
||||||
|
|
||||||
res.send JSON.stringify info
|
|
||||||
|
|
||||||
app.get "/stop", (req, res) =>
|
|
||||||
if not req.query.uid
|
|
||||||
log.debug "Didn't get a UID, sending forbidden"
|
|
||||||
res.status(403).send("Forbidden - missing UID")
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO: permission system to check if uid is allowed to stop playback
|
|
||||||
|
|
||||||
vlc.status.stop()
|
|
||||||
vlc.status.empty()
|
|
||||||
|
|
||||||
res.send("OK")
|
|
||||||
|
|
||||||
app.get "/setvolume", (req, res) =>
|
|
||||||
throw new "Not implemented yet" # FIXME below, still need to implement audio
|
|
||||||
|
|
||||||
@httpServer = app.listen 16444
|
|
||||||
|
|
||||||
cb? null
|
|
||||||
|
|
||||||
stop: (cb) ->
|
|
||||||
@httpServer.close()
|
|
||||||
|
|
||||||
cb?()
|
|
|
@ -10,7 +10,6 @@ pacmdPath = require_bin "pacmd"
|
||||||
|
|
||||||
module.exports = class PulseAudioService extends services.Service
|
module.exports = class PulseAudioService extends services.Service
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"xvfb"
|
|
||||||
]
|
]
|
||||||
constructor: -> super "PulseAudio",
|
constructor: -> super "PulseAudio",
|
||||||
start: (cb) ->
|
start: (cb) ->
|
||||||
|
|
|
@ -16,12 +16,18 @@ ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3cl
|
||||||
|
|
||||||
module.exports = class TS3ClientService extends services.Service
|
module.exports = class TS3ClientService extends services.Service
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"xvfb",
|
|
||||||
"blackbox",
|
|
||||||
"pulseaudio"
|
"pulseaudio"
|
||||||
]
|
]
|
||||||
constructor: -> super "TS3Client",
|
constructor: -> super "TS3Client",
|
||||||
start: (args, cb) =>
|
start: (args, cb) =>
|
||||||
|
if not process.env.XDG_RUNTIME_DIR? or process.env.XDG_RUNTIME_DIR.trim() == ""
|
||||||
|
cb? new Error "XDG runtime directory needs to be set."
|
||||||
|
return
|
||||||
|
|
||||||
|
if not process.env.DISPLAY? or process.env.DISPLAY.trim() == ""
|
||||||
|
cb? new Error "There is no display to run TeamSpeak3 on."
|
||||||
|
return
|
||||||
|
|
||||||
if typeof args == "function"
|
if typeof args == "function"
|
||||||
cb = args
|
cb = args
|
||||||
args = null
|
args = null
|
||||||
|
|
|
@ -1,87 +1,45 @@
|
||||||
spawn = require("child_process").spawn
|
spawn = require("child_process").spawn
|
||||||
services = require("../services")
|
services = require("../services")
|
||||||
config = require("../config")
|
config = require("../config")
|
||||||
VLCApi = require("vlc-api")
|
wc = require("webchimera.js")
|
||||||
StreamSplitter = require("stream-splitter")
|
StreamSplitter = require("stream-splitter")
|
||||||
require_bin = require("../require_bin")
|
|
||||||
|
|
||||||
vlcBinPath = require_bin "vlc"
|
|
||||||
|
|
||||||
module.exports = class VLCService extends services.Service
|
module.exports = class VLCService extends services.Service
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"pulseaudio"
|
"pulseaudio"
|
||||||
]
|
]
|
||||||
constructor: -> super "VLC",
|
constructor: -> super "VLC",
|
||||||
|
###
|
||||||
|
# Starts an instance of VLC and keeps it ready for service.
|
||||||
|
###
|
||||||
start: (cb) ->
|
start: (cb) ->
|
||||||
if @_process
|
if @_instance
|
||||||
cb? null, @_process
|
cb? null, @_instance
|
||||||
return
|
return
|
||||||
|
|
||||||
calledCallback = false
|
calledCallback = false
|
||||||
|
|
||||||
proc = null
|
instance = wc.createPlayer [
|
||||||
doStart = null
|
|
||||||
doStart = () =>
|
|
||||||
await services.find("pulseaudio").start defer(err)
|
|
||||||
if err
|
|
||||||
throw new Error "Dependency pulseaudio failed."
|
|
||||||
|
|
||||||
proc = spawn vlcBinPath, [
|
|
||||||
"-I", "http",
|
|
||||||
"--http-host", config.get("vlc-host"),
|
|
||||||
"--http-port", config.get("vlc-port"),
|
|
||||||
"--http-password", config.get("vlc-password")
|
|
||||||
"--aout", "pulse",
|
"--aout", "pulse",
|
||||||
"--no-video"
|
"--no-video"
|
||||||
],
|
]
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
instance.audio.volume = 50
|
||||||
detached: true
|
|
||||||
|
|
||||||
# logging
|
@_instance = instance
|
||||||
stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n"
|
cb? null, @_instance
|
||||||
stdoutTokenizer.encoding = "utf8";
|
|
||||||
stdoutTokenizer.on "token", (token) =>
|
|
||||||
token = token.trim() # get rid of \r
|
|
||||||
@log.debug token
|
|
||||||
|
|
||||||
stderrTokenizer = proc.stderr.pipe StreamSplitter "\n"
|
|
||||||
stderrTokenizer.encoding = "utf8";
|
|
||||||
stderrTokenizer.on "token", (token) =>
|
|
||||||
token = token.trim() # get rid of \r
|
|
||||||
@log.debug token
|
|
||||||
|
|
||||||
proc.on "exit", () =>
|
|
||||||
if @state == "stopping"
|
|
||||||
return
|
|
||||||
if not calledCallback
|
|
||||||
calledCallback = true
|
|
||||||
@log.warn "VLC terminated unexpectedly during startup."
|
|
||||||
cb? new Error "VLC terminated unexpectedly."
|
|
||||||
@log.warn "VLC terminated unexpectedly, restarting."
|
|
||||||
doStart()
|
|
||||||
|
|
||||||
@_process = proc
|
|
||||||
|
|
||||||
doStart()
|
|
||||||
|
|
||||||
setTimeout (() =>
|
|
||||||
if not calledCallback
|
|
||||||
calledCallback = true
|
|
||||||
|
|
||||||
@instance = new VLCApi
|
|
||||||
host: ":#{encodeURIComponent config.get("vlc-password")}@#{config.get("vlc-host")}",
|
|
||||||
port: config.get("vlc-port")
|
|
||||||
cb? null, @instance), 1500 # TODO: Use some more stable condition
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Shuts down the VLC instance.
|
||||||
|
###
|
||||||
stop: (cb) ->
|
stop: (cb) ->
|
||||||
if not @_process
|
if not @_instance
|
||||||
cb?()
|
cb?()
|
||||||
return
|
return
|
||||||
|
|
||||||
@instance = null
|
@_instance.stop()
|
||||||
|
|
||||||
@_process.kill()
|
# TODO: Is there even a proper way to shut this down?
|
||||||
await @_process.once "exit", defer()
|
@_instance = null
|
||||||
|
|
||||||
cb?()
|
cb?()
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,15 @@ config = require("../config")
|
||||||
services = require("../services")
|
services = require("../services")
|
||||||
require_bin = require("../require_bin")
|
require_bin = require("../require_bin")
|
||||||
|
|
||||||
require_bin "Xvfb"
|
xvfbPath = require_bin "Xvfb", false
|
||||||
|
|
||||||
module.exports = class XvfbService extends services.Service
|
module.exports = class XvfbService extends services.Service
|
||||||
constructor: -> super "Xvfb",
|
constructor: -> super "Xvfb",
|
||||||
start: (cb) ->
|
start: (cb) ->
|
||||||
|
if not xvfbPath?
|
||||||
|
cb? new Error "Xvfb is not available."
|
||||||
|
return
|
||||||
|
|
||||||
if @instance
|
if @instance
|
||||||
cb? null, @instance
|
cb? null, @instance
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
spawn = require("child_process").spawn
|
spawn = require("child_process").spawn
|
||||||
log = require("../logger")("BlackBox")
|
log = require("../logger")("XWindowManager")
|
||||||
services = require("../services")
|
services = require("../services")
|
||||||
StreamSplitter = require("stream-splitter")
|
StreamSplitter = require("stream-splitter")
|
||||||
require_bin = require("../require_bin")
|
require_bin = require("../require_bin")
|
||||||
|
|
||||||
blackboxBinPath = require_bin "blackbox"
|
xwmBinPath = require_bin "x-window-manager", false
|
||||||
|
|
||||||
module.exports = class BlackBoxService extends services.Service
|
module.exports = class XWindowManagerService extends services.Service
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"xvfb"
|
|
||||||
]
|
]
|
||||||
constructor: -> super "BlackBox",
|
constructor: -> super "XWindowManager",
|
||||||
start: (cb) ->
|
start: (cb) ->
|
||||||
|
if not xwmBinPath?
|
||||||
|
cb? new Error "A window manager not available."
|
||||||
|
return
|
||||||
|
|
||||||
|
if not process.env.XDG_RUNTIME_DIR? or process.env.XDG_RUNTIME_DIR.trim() == ""
|
||||||
|
cb? new Error "XDG runtime directory needs to be set."
|
||||||
|
return
|
||||||
|
|
||||||
|
if not process.env.DISPLAY? or process.env.DISPLAY.trim() == ""
|
||||||
|
cb? new Error "There is no display to run TeamSpeak3 on."
|
||||||
|
return
|
||||||
|
|
||||||
if @process
|
if @process
|
||||||
cb? null, @process
|
cb? null, @process
|
||||||
return
|
return
|
||||||
|
@ -21,11 +32,7 @@ module.exports = class BlackBoxService extends services.Service
|
||||||
proc = null
|
proc = null
|
||||||
doStart = null
|
doStart = null
|
||||||
doStart = () =>
|
doStart = () =>
|
||||||
await services.find("xvfb").start defer(err)
|
proc = spawn xwmBinPath, [],
|
||||||
if err
|
|
||||||
throw new Error "Dependency xvfb failed."
|
|
||||||
|
|
||||||
proc = spawn blackboxBinPath, [ "-rc", "/dev/null" ],
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
detached: true
|
detached: true
|
||||||
env:
|
env:
|
||||||
|
@ -51,9 +58,9 @@ module.exports = class BlackBoxService extends services.Service
|
||||||
return
|
return
|
||||||
if not calledCallback
|
if not calledCallback
|
||||||
calledCallback = true
|
calledCallback = true
|
||||||
@log.warn "BlackBox terminated unexpectedly during startup."
|
@log.warn "Window manager terminated unexpectedly during startup."
|
||||||
cb? new Error "BlackBox terminated unexpectedly."
|
cb? new Error "Window manager terminated unexpectedly."
|
||||||
@log.warn "BlackBox terminated unexpectedly, restarting."
|
@log.warn "Window manager terminated unexpectedly, restarting."
|
||||||
doStart()
|
doStart()
|
||||||
|
|
||||||
@process = proc
|
@process = proc
|
|
@ -10,9 +10,23 @@ merge = require "merge"
|
||||||
|
|
||||||
parserLog = getLogger "parser"
|
parserLog = getLogger "parser"
|
||||||
|
|
||||||
escape = (value) => value.toString().replace(///\\///g, "\\\\").replace(/\//g, "\\/").replace(/\|/g, "\\|").replace(///\ ///g, "\\s")
|
escape = (value) => value.toString()\
|
||||||
|
.replace(/\\/g, "\\\\")\
|
||||||
|
.replace(/\//g, "\\/")\
|
||||||
|
.replace(/\|/g, "\\p")\
|
||||||
|
.replace(/\n/g, "\\n")\
|
||||||
|
.replace(/\r/g, "\\r")\
|
||||||
|
.replace(/\t/g, "\\t")\
|
||||||
|
.replace(/\ /g, "\\s")
|
||||||
|
|
||||||
unescape = (value) => value.toString().replace(/\\s/g, " ").replace(/\\\//g, "/").replace(/\\\|/g, "|").replace(/\\\\/g, "\\")
|
unescape = (value) => value.toString()\
|
||||||
|
.replace(/\\s/g, " ")\
|
||||||
|
.replace(/\\t/g, "\t")\
|
||||||
|
.replace(/\\r/g, "\r")\
|
||||||
|
.replace(/\\n/g, "\n")\
|
||||||
|
.replace(/\\p/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)
|
||||||
|
|
|
@ -211,9 +211,18 @@ module.exports = class SettingsFile
|
||||||
select: () ->
|
select: () ->
|
||||||
settingsObj.defaultIdentity = @id
|
settingsObj.defaultIdentity = @id
|
||||||
update: () ->
|
update: () ->
|
||||||
|
settingsObj.log.silly "Requested update of #{id.id}"
|
||||||
for own index, identity of settingsObj.identities
|
for own index, identity of settingsObj.identities
|
||||||
if identity.id == id.id
|
if identity.id == id.id
|
||||||
settingsObj.identities[index] = merge identity, id
|
# remove functions from this object
|
||||||
|
cleanIdentity = merge @
|
||||||
|
for own k, v of cleanIdentity
|
||||||
|
if typeof v == "function"
|
||||||
|
delete cleanIdentity[k]
|
||||||
|
|
||||||
|
# now this is our new identity object!
|
||||||
|
settingsObj.log.silly "Updating identity #{id.id}"
|
||||||
|
settingsObj.identities[index] = cleanIdentity
|
||||||
return
|
return
|
||||||
remove: () ->
|
remove: () ->
|
||||||
for own index, identity of settingsObj.identities
|
for own index, identity of settingsObj.identities
|
||||||
|
|
22
x11.iced
22
x11.iced
|
@ -6,13 +6,18 @@ services = require("./services")
|
||||||
StreamSplitter = require("stream-splitter")
|
StreamSplitter = require("stream-splitter")
|
||||||
require_bin = require("./require_bin")
|
require_bin = require("./require_bin")
|
||||||
|
|
||||||
xdotoolBinPath = require_bin "xdotool"
|
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
|
||||||
|
if not xdotoolBinPath?
|
||||||
|
cb? new Error "xdotool is not available"
|
||||||
|
return
|
||||||
|
|
||||||
# We provide --name due to the bug mentioned at https://github.com/jordansissel/xdotool/issues/14
|
# 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" ],
|
xdoproc = spawn xdotoolBinPath, [ "search", "--any", "--pid", pid, "--name", "xdosearch" ],
|
||||||
env:
|
env:
|
||||||
|
@ -43,12 +48,17 @@ module.exports =
|
||||||
getWindowIdByProcessIdSync: (pid) => Sync() => @getWindowIdByProcessId.sync @, pid
|
getWindowIdByProcessIdSync: (pid) => Sync() => @getWindowIdByProcessId.sync @, pid
|
||||||
|
|
||||||
sendKeys: (wid, keys, cb) =>
|
sendKeys: (wid, keys, cb) =>
|
||||||
# blackbox needs to be running for windowactivate to work
|
# Do not bother trying if xdotool is not available
|
||||||
blackboxService = services.find("BlackBox")
|
if not xdotoolBinPath?
|
||||||
if blackboxService.state != "started"
|
cb? new Error "xdotool not available."
|
||||||
await blackboxService.start defer(err)
|
return
|
||||||
|
|
||||||
|
# a window manager needs to be running for windowactivate to work
|
||||||
|
xwmService = services.find("XWindowManager")
|
||||||
|
if xwmService.state != "started"
|
||||||
|
await xwmService.start defer(err)
|
||||||
if err
|
if err
|
||||||
cb? new Error "Could not start compatible 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),
|
||||||
|
|
Loading…
Reference in New Issue