Skip to content

Commit

Permalink
Merge pull request #21 from vektorlab/add-streaming
Browse files Browse the repository at this point in the history
Add streaming
  • Loading branch information
bcicen committed Jan 11, 2016
2 parents b603493 + 1435327 commit 82a8696
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 70 deletions.
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,6 @@ Slackcat is a simple commandline utility to post snippets to Slack.
<img width="500px" src="https://raw.githubusercontent.com/vektorlab/slackcat/master/demo.gif" alt="slackcat"/>


## Usage
Pipe command output:
```bash
$ echo -e "hi\nthere" | slackcat --channel general --filename hello
file hello uploaded to general
```

Post an existing file:
```bash
$ slackcat -c general /home/user/bot.png
file bot.png uploaded to general
```

## Installing

Download the latest release for your platform:
Expand Down Expand Up @@ -51,6 +38,27 @@ Create a Slackcat config file and you're ready to go!
echo '<your-slack-token>' > ~/.slackcat
```

## Usage
Pipe command output as a text snippet:
```bash
$ echo -e "hi\nthere" | slackcat --channel general --filename hello
*slackcat* file hello uploaded to general
```

Post an existing file:
```bash
$ slackcat --channel general /home/user/bot.png
*slackcat* file bot.png uploaded to general
```

Stream input continously as a formatted message:
```bash
$ tail -f /path/to/log | slackcat --channel general --stream
*slackcat* posted 5 message lines to general
*slackcat* posted 2 message lines to general
...
```

## Options

Option | Description
Expand Down
5 changes: 2 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import (
const (
base_url = "https://slack.com/oauth/authorize"
client_id = "7065709201.17699618306"
scope = "channels%3Aread+groups%3Aread+im%3Aread+chat%3Awrite%3Auser+files%3Awrite%3Auser"
scope = "channels%3Aread+groups%3Aread+im%3Aread+users%3Aread+chat%3Awrite%3Auser+files%3Awrite%3Auser"
)

func getConfigPath() string {
homedir := os.Getenv("HOME")
if homedir == "" {
exit(fmt.Errorf("$HOME not set"))
exitErr(fmt.Errorf("$HOME not set"))
}
return homedir + "/.slackcat"
}
Expand Down Expand Up @@ -45,5 +45,4 @@ func configureOA() {
output("Please open the below URL in your browser to authorize SlackCat")
output(oa_url)
}
os.Exit(0)
}
87 changes: 33 additions & 54 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,13 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"time"

"github.com/bluele/slack"
"github.com/codegangsta/cli"
"github.com/fatih/color"
)

var version = "dev-build"

//Lookup Slack id for channel, group, or im
func lookupSlackId(api *slack.Slack, name string) (string, error) {
channel, err := api.FindChannelByName(name)
if err == nil {
return channel.Id, nil
}
group, err := api.FindGroupByName(name)
if err == nil {
return group.Id, nil
}
im, err := api.FindImByName(name)
if err == nil {
return im.Id, nil
}
return "", fmt.Errorf("No such channel, group, or im")
}

func readIn(lines chan string, tee bool) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanLines)
Expand Down Expand Up @@ -66,14 +46,14 @@ func output(s string) {
func failOnError(err error, msg string, appendErr bool) {
if err != nil {
if appendErr {
exit(fmt.Errorf("%s: %s", msg, err))
exitErr(fmt.Errorf("%s: %s", msg, err))
} else {
exit(fmt.Errorf("%s", msg))
exitErr(fmt.Errorf("%s", msg))
}
}
}

func exit(err error) {
func exitErr(err error) {
output(color.RedString(err.Error()))
os.Exit(1)
}
Expand All @@ -88,6 +68,10 @@ func main() {
Name: "tee, t",
Usage: "Print stdin to screen before posting",
},
cli.BoolFlag{
Name: "stream, s",
Usage: "Stream messages to Slack continuously instead of uploading a single snippet",
},
cli.BoolFlag{
Name: "noop",
Usage: "Skip posting file to Slack. Useful for testing",
Expand All @@ -107,52 +91,47 @@ func main() {
}

app.Action = func(c *cli.Context) {
var filePath string
var fileName string

if c.Bool("configure") {
configureOA()
os.Exit(0)
}

token := readConfig()
api := slack.New(token)
fileName := c.String("filename")

if c.String("channel") == "" {
exit(fmt.Errorf("no channel provided!"))
exitErr(fmt.Errorf("no channel provided!"))
}

channelId, err := lookupSlackId(api, c.String("channel"))
failOnError(err, "", true)
slackcat, err := newSlackCat(token, c.String("channel"))
failOnError(err, "Slack API Error", true)

if len(c.Args()) > 0 {
filePath = c.Args()[0]
fileName = filepath.Base(filePath)
} else {
lines := make(chan string)
go readIn(lines, c.Bool("tee"))
filePath = writeTemp(lines)
fileName = strconv.FormatInt(time.Now().Unix(), 10)
defer os.Remove(filePath)
if c.Bool("stream") {
output("filepath provided, ignoring stream option")
}
filePath := c.Args()[0]
if fileName == "" {
fileName = filepath.Base(filePath)
}
slackcat.postFile(filePath, fileName, c.Bool("noop"))
os.Exit(0)
}

//override default filename with provided option value
if c.String("filename") != "" {
fileName = c.String("filename")
}
lines := make(chan string)
go readIn(lines, c.Bool("tee"))

if c.Bool("noop") {
output(fmt.Sprintf("skipping upload of file %s to %s", fileName, c.String("channel")))
if c.Bool("stream") {
output("starting stream")
go slackcat.addToStreamQ(lines)
go slackcat.processStreamQ(c.Bool("noop"))
go slackcat.trap()
select {}
} else {
start := time.Now()
err = api.FilesUpload(&slack.FilesUploadOpt{
Filepath: filePath,
Filename: fileName,
Title: fileName,
Channels: []string{channelId},
})
failOnError(err, "error uploading file to Slack", true)
duration := strconv.FormatFloat(time.Since(start).Seconds(), 'f', 3, 64)
output(fmt.Sprintf("file %s uploaded to %s (%ss)", fileName, c.String("channel"), duration))
filePath := writeTemp(lines)
defer os.Remove(filePath)
slackcat.postFile(filePath, fileName, c.Bool("noop"))
os.Exit(0)
}
}

Expand Down
35 changes: 35 additions & 0 deletions queue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"sync"
)

type StreamQ struct {
queue []string
lock sync.RWMutex
}

func newStreamQ() *StreamQ {
return &StreamQ{
queue: []string{},
lock: sync.RWMutex{},
}
}

func (q *StreamQ) add(line string) {
q.lock.Lock()
q.queue = append(q.queue, line)
q.lock.Unlock()
}

func (q *StreamQ) isEmpty() bool {
return (len(q.queue) < 1)
}

func (q *StreamQ) flush() []string {
q.lock.Lock()
items := q.queue
q.queue = []string{}
q.lock.Unlock()
return items
}
134 changes: 134 additions & 0 deletions slackcat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package main

import (
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"time"

"github.com/bluele/slack"
)

type SlackCat struct {
api *slack.Slack
opts *slack.ChatPostMessageOpt
queue *StreamQ
shutdown chan os.Signal
channelName string
channelId string
}

func newSlackCat(token, channelName string) (*SlackCat, error) {
sc := &SlackCat{
api: slack.New(token),
opts: &slack.ChatPostMessageOpt{AsUser: true},
queue: newStreamQ(),
shutdown: make(chan os.Signal, 1),
channelName: channelName,
}
err := sc.lookupSlackId()
if err != nil {
return nil, err
}
signal.Notify(sc.shutdown, os.Interrupt)
return sc, nil
}

func (sc *SlackCat) trap() {
sigcount := 0
for sig := range sc.shutdown {
if sigcount > 0 {
exitErr(fmt.Errorf("aborted"))
}
output(fmt.Sprintf("got signal: %s", sig.String()))
output("press ctrl+c again to exit immediately")
sigcount++
go sc.exit()
}
}

func (sc *SlackCat) exit() {
for {
if sc.queue.isEmpty() {
os.Exit(0)
} else {
output("flushing remaining messages to Slack...")
time.Sleep(3 * time.Second)
}
}
}

//Lookup Slack id for channel, group, or im
func (sc *SlackCat) lookupSlackId() error {
api := sc.api
channel, err := api.FindChannelByName(sc.channelName)
if err == nil {
sc.channelId = channel.Id
return nil
}
group, err := api.FindGroupByName(sc.channelName)
if err == nil {
sc.channelId = group.Id
return nil
}
im, err := api.FindImByName(sc.channelName)
if err == nil {
sc.channelId = im.Id
return nil
}
fmt.Println(err)
return fmt.Errorf("No such channel, group, or im")
}

func (sc *SlackCat) addToStreamQ(lines chan string) {
for line := range lines {
sc.queue.add(line)
}
sc.exit()
}

//TODO: handle messages with length exceeding maximum for Slack chat
func (sc *SlackCat) processStreamQ(noop bool) {
if !(sc.queue.isEmpty()) {
msglines := sc.queue.flush()
if noop {
output(fmt.Sprintf("skipped posting of %s message lines to %s", strconv.Itoa(len(msglines)), sc.channelName))
} else {
sc.postMsg(msglines)
}
}
time.Sleep(3 * time.Second)
sc.processStreamQ(noop)
}

func (sc *SlackCat) postMsg(msglines []string) {
msg := fmt.Sprintf("```%s```", strings.Join(msglines, "\n"))
err := sc.api.ChatPostMessage(sc.channelId, msg, sc.opts)
failOnError(err, "", true)
output(fmt.Sprintf("posted %s message lines to %s", strconv.Itoa(len(msglines)), sc.channelName))
}

func (sc *SlackCat) postFile(filePath, fileName string, noop bool) {
//default to timestamp for filename
if fileName == "" {
fileName = strconv.FormatInt(time.Now().Unix(), 10)
}

if noop {
output(fmt.Sprintf("skipping upload of file %s to %s", fileName, sc.channelName))
}

start := time.Now()
err := sc.api.FilesUpload(&slack.FilesUploadOpt{
Filepath: filePath,
Filename: fileName,
Title: fileName,
Channels: []string{sc.channelId},
})
failOnError(err, "error uploading file to Slack", true)
duration := strconv.FormatFloat(time.Since(start).Seconds(), 'f', 3, 64)
output(fmt.Sprintf("file %s uploaded to %s (%ss)", fileName, sc.channelName, duration))
os.Exit(0)
}

0 comments on commit 82a8696

Please sign in to comment.