toxio

Turn (almost) any CLI program into a Tox bot
git clone git://git.luxferre.top/toxio.git
Log | Files | Refs | README | LICENSE

commit 591928394fb93408a242eca2cc392e35f678bc3b
Author: Luxferre <lux@ferre>
Date:   Thu,  4 Jan 2024 15:43:00 +0200

initial upload

Diffstat:
ACOPYING | 24++++++++++++++++++++++++
AMakefile | 15+++++++++++++++
AREADME.md | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atoxio.nim | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 200 insertions(+), 0 deletions(-)

diff --git a/COPYING b/COPYING @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to <https://unlicense.org> diff --git a/Makefile b/Makefile @@ -0,0 +1,15 @@ +# POSIX makefile template for Nim + +CFLAGS = -O2 +LDFLAGS = -L/usr/lib +NC = nim +NCFLAGS = -d:release -d:lto -d:strip --opt:size + +TARGET = toxio +DEPS = + +$(TARGET): $(TARGET).nim + $(NC) $(NCFLAGS) -t:"$(CFLAGS)" -l:"$(LDFLAGS)" c $(TARGET).nim $(DEPS) + +clean: + rm $(TARGET) diff --git a/README.md b/README.md @@ -0,0 +1,61 @@ +# toxio: turn (almost) any CLI program/script into a Tox bot + +Toxio is a small Nim program that aims to do one thing: wrap any line-oriented CLI program or script to be able to communicate with it via the [Tox](https://tox.chat/) network. Toxio spawns any shell command of your choice as a subprocess and redirects your messages to its standard input and its output lines to the chat replies. + +## Building + +To build toxio, you obviously would need a working Nim compiler, as well as [toxcore](https://github.com/TokTok/c-toxcore) library and [nim-toxcore](https://git.sr.ht/~ehmry/nim-toxcore) package installed. The latter is available using Nimble package manager (`nimble install toxcore`). You will also need any POSIX-compatible `make` command, although nothing prevents you from compiling the `toxio.nim` file manually. + +Once you have all dependencies sorted out, just run `make` in the project directory. If everything is successful, a `toxio` binary file should appear. + +## Usage + +The overall command syntax is like this: `toxio [bot_name] [save_file] [r|w] [cmd]`. Now, let's go through the stages to set up your bot. + +### 1. Obtain a Tox save data file + +First, you will need an existing toxcore-compatible save file. For safety reasons, toxio does not create any save files, you have to supply it externally. They may or may not already have friendlists, but if they do, beware that all the friends will have the access to the process running inside your bot. Thus, it's recommended to create a new profile save file (it must be unencrypted) in a client like Toxic and then use it solely for the purposes of this bot. + +### 2. Add the bot users as friends + +Now that you have your save file (e.g. `save.tox`), you should run toxio in the **write-enable** mode with the `w` command line parameter and a dummy command like `cat` (which will essentially make toxio function as an echo bot for you to test it): + +``` +toxio "My new bot" /path/to/save.tox w cat +``` + +In the write-enable mode, the bot accepts all friend requests and saves them to your save file. This mode must only be used for initial set up and testing phase, never for normal usage! + +After running, the bot will print out its Tox ID that you can use to add as a friend to all your Tox identities you need to use the bot from. After you added this bot as a friend to every required ID, terminate it with Ctrl+C and make sure your save file is backed up in a safe place. + +### 3. Run the bot normally + +Now, you can run the bot normally (in the **read-only** mode) like this (where "My new bot" is just its displayed name and `"cmd"` is the command line you need to run): + +``` +toxio "My new bot" /path/to/save.tox r "cmd" +``` + +When toxio gets online, every connected friend will be able to communicate with the subprocess using the chat with this bot. The status message will display the command line of the process the bot is currently serving. + +Keep in mind that you only start getting the program's output if you initially send a message. What to do with your messages is fully up to the program. + +You can also pause the program's output. If you run, for instance, some monitoring service and want to temporarily stop receiving messages from the bot, just send `/toxiostop` to it. The output will resume automatically whenever you send any other message. + +## Limitations + +- Text messages only. +- Single subprocess per bot by design. This means that every connected friend will communicate with exactly the same subprocess and receive identical output from it. +- The program's output must end with newline characters. +- Every output line is sent as a separate message. +- The stdout and stderr streams are merged. This is hardcoded: find `poStdErrToStdOut` option in the `toxio.nim` file and remove it if you don't need this behavior. +- You need an external client to initially set up the bot's Tox ID and generate the save file. +- Only one DHT bootstrap node hardcoded for now. +- Only tested on Unix-like OSes, compatibility is not guaranteed for others. + +## Credits + +Created by Luxferre in 2024, released into public domain. + +Made in Ukraine. + diff --git a/toxio.nim b/toxio.nim @@ -0,0 +1,100 @@ +# toxio: turn any CLI program/script into a Tox bot +# deps: toxcore, nim-toxcore +# created by Luxferre in 2024, released into public domain + +import toxcore, times, sets, osproc, streams +from threadpool import spawn +from std/os import sleep +from std/cmdline import paramCount, paramStr + +# command line parameters +if paramCount() < 4: + echo "Usage: toxio name datafile [w|r] cmd" + quit(1) +let + botname = paramStr(1) + toxdatafile = paramStr(2) + writeperm: bool = (paramStr(3) == "w") + subcmd = paramStr(4) + +# read Tox save data +var toxconfig = "" +try: + toxconfig = readFile(toxdatafile) +except IOError: + stderr.writeLine "Cannot read the Tox data file!" + quit(1) + +# bootstrap constants +const + bootstrapHost = "46.101.197.175" + bootstrapKey = "CD133B521159541FB1D326DE9850F5E56A6C724B5B8E5EB5CD8D950408E95707".toPublicKey + bootstrapPort = 33445 + +# declare the bot type to wrap GC-safe data +type Bot = ref object + tox: Tox + datafile: string + activeFriends: HashSet[Friend] + sproc: Process + +# input loop procedure +proc inputloop(bot: Bot) = + var gline = "" + while bot.sproc.outputStream.readLine(gline): + for f in bot.activeFriends.items: + bot.tox.send(f, gline, TOX_MESSAGE_TYPE_NORMAL) + bot.sproc.outputStream.flush() + +# GC-safe bot callbacks +proc cbs(bot: Bot) = + # what do do on a new friend request (only in write-enable mode) + if writeperm: + bot.tox.onFriendRequest do (pk: PublicKey; msg: string): + discard bot.tox.addFriendNoRequest(pk) + try: + writeFile(bot.datafile, bot.tox.saveData) + except IOError: + stderr.writeLine "Cannot write the Tox data file!" + + # what to do on a new message + bot.tox.onFriendMessage do (f: Friend; msg: string; kind: MessageType): + if msg == "/toxiostop": # temporarily unsubscribe from the output + bot.activeFriends.excl(f) + else: + bot.activeFriends.incl(f) # first-time activation + bot.sproc.inputStream.writeLine msg + bot.sproc.inputStream.flush() + +# bot options description +proc botopt(opts: Options) = + opts.savedata_type = TOX_SAVEDATA_TYPE_TOX_SAVE + opts.saveData = toxconfig + +# spawn the subprocess +var subproc:Process + +try: + subproc = startProcess(subcmd, options={poEvalCommand,poStdErrToStdOut,poInteractive,poUsePath}) + stderr.writeLine "Subprocess started" +except: + stderr.writeLine "Cannot spawn the subprocess command!" + quit(1) + +# init the bot +let iobot = Bot(tox:initTox(botopt)) +iobot.tox.name = botname +iobot.datafile = toxdatafile +iobot.cbs() +iobot.tox.bootstrap(bootstrapHost, bootstrapKey, bootstrapPort) +iobot.tox.statusMessage = "Serving " & subcmd +iobot.sproc = subproc +stderr.writeLine iobot.tox.name, " online at ", iobot.tox.address + +# run the input loop +spawn inputloop(iobot) + +# run the toxcore loop +while true: + iterate(iobot.tox) + sleep inMilliseconds(iobot.tox.iterationInterval)