diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7054a22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +session/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..75b192e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Lamer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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 OR COPYRIGHT HOLDERS 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a364559 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +.PHONY: build +build: + docker build -t sssc dockerfiles/ + +.PHONY: dev +dev: + docker run -ti --rm -v $(CURDIR):/app sssc ash + diff --git a/README.md b/README.md new file mode 100644 index 0000000..807e4f6 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# SSSC + +Simple and Stupid Slack Client + +## Features + +SSSC is closely modeled after [(ii)](https://tools.suckless.org/ii/) which means that communication is facilitated by an `out` file and an `in` FIFO on the filesystem. + +No features other than sending and receiving messages while connected are supported. Retrieval of unread messages may be implemented at a later date, in a separate branch. + +Incoming data not handled by SSSC will be printed to stdout as formatted JSON. + +## Installation + +``` +$ yarn install +``` + +A Slack api token has to be provided as an environment variable called `TOKEN`. There's a Dockerfile included in the repository for convenience. + +## Usage + +Upon successful connection to Slack a directory with every channel, group and im will be created in the project root like so: + +``` +session/ +'-- your_team_name/ + |-- general/ + | |-- in + | '-- out + |-- random/ + | |-- in + | '-- out + |-- alice/ + | |-- in + | '-- out + '-- bob/ + |-- in + '-- out +``` + +To send a message to alice: + +``` +$ echo "Hi" > ./session/your_team_name/alice/in +``` + +To keep track of the conversation: + +``` +$ tail -f ./session/you_team_name/alice/out +``` + +Incoming messages are formatted as follows: + +``` +{timestamp} <{username}> {text} +``` + +It's up to the user to further process this output as needed using his preferred method of text manipulation. diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile new file mode 100644 index 0000000..f2019ce --- /dev/null +++ b/dockerfiles/Dockerfile @@ -0,0 +1,16 @@ +FROM mhart/alpine-node:10 + +RUN apk update + +RUN adduser -D -u 1000 dev + +RUN apk add --no-cache curl make sudo +RUN echo "dev ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + +WORKDIR /app +RUN chown dev:dev /app + +ENV HOME /home/dev +USER dev + +CMD /usr/bin/node sssc.js diff --git a/package.json b/package.json new file mode 100644 index 0000000..d2684a9 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "request": "^2.85.0", + "ws": "^5.1.0" + } +} diff --git a/sssc.js b/sssc.js new file mode 100644 index 0000000..b5c5efe --- /dev/null +++ b/sssc.js @@ -0,0 +1,151 @@ +#!/usr/bin/env node + +const { spawnSync } = require('child_process') +const request = require('request'); +const fs = require('fs'); +const WebSocket = require('ws'); +const C = require('constants'); +const path = require('path'); + +const SESSION_DIR = path.resolve(__dirname, './session'); + +if (!fs.existsSync(SESSION_DIR)) { + fs.mkdirSync(SESSION_DIR); +} + +const map = {}; + +callSlackMethod('rtm.start', (data) => { + if (data.ok === false) { + process.stderr.write(JSON.stringify(data, null, 2) + '\n'); + + return; + } + + const userName = data.self.name; + const teamName = data.team.name; + + if (!fs.existsSync(`${SESSION_DIR}/${teamName}`)) { + fs.mkdirSync(`${SESSION_DIR}/${teamName}`); + } + + map[data.self.id] = data.self.name; + map[data.team.id] = teamName; + + for (let i = 0; i < data.users.length; i++) { + map[data.users[i].id] = data.users[i].name; + } + + const chats = [].concat( + data.channels, + data.groups, + data.ims.map(im => { + im.name = map[im.user]; + + return im; + }), + ); + + const socket = new WebSocket(data.url); + const sendBuffer = {}; + + for (let i = 0; i < chats.length; i++) { + setupChat(socket, sendBuffer, teamName, chats[i]); + } + + socket.on('message', (rawMessage) => { + let message = JSON.parse(rawMessage); + + if (message.type === 'message') { + if (message.subtype === 'message_changed') { + const sub = message.message; + + sub.text = '(*) ' + sub.text; + sub.channel = message.channel; + + message = sub; + } + + fs.appendFileSync( + `${SESSION_DIR}/${teamName}/${map[message.channel]}/out`, + `${message.ts} <${map[message.user]}> ${message.text}\n`, + ); + + return; + } + + if (message.type === 'group_joined' || message.type === 'channel_created') { + setupChat(socket, sendBuffer, teamName, message.channel); + + return; + } + + if (sendBuffer[message.reply_to] !== undefined && message.ok === true) { + fs.appendFileSync( + `${SESSION_DIR}/${teamName}/${map[sendBuffer[message.reply_to]]}/out`, + `${message.ts} <${userName}> ${message.text}\n`, + ); + + delete sendBuffer[message.reply_to]; + + return; + } + + process.stdout.write(JSON.stringify(message, null, 2)); + }); +}); + +function setupChat(socket, sendBuffer, teamName, chat) { + map[chat.id] = chat.name; + + if (!fs.existsSync(`${SESSION_DIR}/${teamName}/${chat.name}`)) { + fs.mkdirSync(`${SESSION_DIR}/${teamName}/${chat.name}`); + fs.writeFileSync(`${SESSION_DIR}/${teamName}/${chat.name}/out`, ''); + spawnSync('mkfifo', [`${SESSION_DIR}/${teamName}/${chat.name}/in`]); + } + + const inFifo = `${SESSION_DIR}/${teamName}/${chat.name}/in`; + const fd = fs.openSync(inFifo, C.O_RDONLY | C.O_NONBLOCK); + + setInterval(() => { + const allocSize = 1024; + + fs.read(fd, Buffer.alloc(allocSize), 0, allocSize, null, (error, size, buffer) => { + if (size > 0) { + const id = Date.now(); + + // remove trailing newlines + if (size < allocSize) { + while (size > 0 && buffer[size - 1] === 10) { + --size; + } + } + + const text = buffer.slice(0, size).toString(); + + socket.send(JSON.stringify({id, channel: chat.id, type: 'message', text})); + + sendBuffer[String(id)] = chat.id; + } + }); + }, 128); +} + +function callSlackMethod(apiMethod, callback = (...args) => {}, params = {}) { + // @Incomplete: replace external library with a simple https call + request.get( + { + url: `https://slack.com/api/${apiMethod}`, + qs: Object.assign({}, params, {token: process.env.TOKEN}), + json: true, + }, + (error, response, data) => { + if (error !== null) { + console.log(error); + return; + } + + callback(data, response); + } + ); +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..947f4f4 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,274 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +combined-stream@1.0.6, combined-stream@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + +mime-types@^2.1.12, mime-types@~2.1.17: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + +oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.5.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + +request@^2.85.0: + version "2.87.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +safer-buffer@^2.0.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +sshpk@^1.7.0: + version "1.14.2" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + safer-buffer "^2.0.2" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +tough-cookie@~2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +ws@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.1.0.tgz#ad7f95a65c625d47c24f2b8e5928018cf965e2a6" + dependencies: + async-limiter "~1.0.0"