├── .gitignore ├── Godeps ├── Godeps.json └── Readme ├── Procfile ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the binary 2 | anonymous-slack 3 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/recursionpharma/anonymous-slack", 3 | "GoVersion": "go1.5.1", 4 | "Deps": [] 5 | } 6 | -------------------------------------------------------------------------------- /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: anonymous-slack -port=$PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anonymous-slack 2 | A Heroku service to send anonymous messages to colleagues on Slack. Your message shows up under the guise of a random animal: `an anonymous aardvark says: [your message]`. 3 | 4 | As a slack administrator: 5 | 6 | Clone this repository, enter it and generate the Godeps: 7 | 8 | git clone git@github.com:recursionpharma/anonymous-slack.git 9 | cd anonymous-slack 10 | godep save -r 11 | 12 | Create a new Heroku app: 13 | 14 | heroku create 15 | >>> Creating whispering-cliffs-7437... done, stack is cedar-14 16 | >>> https://whispering-cliffs-7437.herokuapp.com/ | https://git.heroku.com/whispering-cliffs-7437.git 17 | 18 | In Slack integrations, add a Slash command, for example, `/anon` . Set the URL in Slack to your Heroku website URL (in our example, `https://whispering-cliffs-7437.herokuapp.com/`). The resulting slack "token" should be set as a Heroku environment variable: 19 | 20 | heroku config:set INCOMING_SLACK_TOKEN=XXX 21 | 22 | Then, in Slack integrations, add a Slash Incoming webhook. The resulting "webhook url" should be set as the Heroku environment variable: 23 | 24 | heroku config:set INCOMING_SLACK_WEBHOOK=https://hooks.slack.com/services/BLAH/BLAH/BLAH 25 | 26 | Deploy to heroku. 27 | 28 | git push heroku trunk 29 | 30 | Success! Now if you send a message in any channel, public or private, like the following: 31 | 32 | /anon @somebodyelse hey, guess who? 33 | 34 | That message will be suppressed, and @somebodyelse gets a message like this: 35 | 36 | an anonymous capybara says: hey, guess who? 37 | 38 | Be nice! 39 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "regexp" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | tokenConfig = "INCOMING_SLACK_TOKEN" 18 | webhookConfig = "INCOMING_SLACK_WEBHOOK" 19 | // Incoming payload form will have the following keys: 20 | // (See: https://api.slack.com/slash-commands) 21 | keyToken = "token" 22 | keyTeamID = "team_id" 23 | keyChannelId = "channel_id" 24 | keyChannelName = "channel_name" 25 | keyUserID = "user_id" 26 | keyUserName = "user_name" 27 | keyCommand = "command" 28 | keyText = "text" 29 | ) 30 | 31 | type slackMsg struct { 32 | Text string `json:"text"` 33 | Username string `json:"username"` // Anonymous animal sender 34 | Channel string `json:"channel"` // Recipient 35 | } 36 | 37 | var ( 38 | port int 39 | // Random animals cribbed from Google Drive's "Anonymous [Animal]" notifications 40 | animals = []string{ 41 | "alligator", "anteater", "armadillo", "auroch", "axolotl", "badger", "bat", "beaver", "buffalo", 42 | "camel", "chameleon", "cheetah", "chipmunk", "chinchilla", "chupacabra", "cormorant", "coyote", 43 | "crow", "dingo", "dinosaur", "dolphin", "duck", "elephant", "ferret", "fox", "frog", "giraffe", 44 | "gopher", "grizzly", "hedgehog", "hippo", "hyena", "jackal", "ibex", "ifrit", "iguana", "koala", 45 | "kraken", "lemur", "leopard", "liger", "llama", "manatee", "mink", "monkey", "narwhal", "nyan cat", 46 | "orangutan", "otter", "panda", "penguin", "platypus", "python", "pumpkin", "quagga", "rabbit", "raccoon", 47 | "rhino", "sheep", "shrew", "skunk", "slow loris", "squirrel", "turtle", "walrus", "wolf", "wolverine", "wombat", 48 | } 49 | // Username must be first. 50 | payloadExp = regexp.MustCompile(`([@#][^\s]+):?(.*)`) 51 | ) 52 | 53 | // readAnonymousMessage parses the username and re-routes 54 | // the message to the user from an anonymous animal 55 | func readAnonymousMessage(r *http.Request) string { 56 | err := r.ParseForm() 57 | // TODO: Change HTTP status code 58 | if err != nil { 59 | return string(err.Error()) 60 | } 61 | // Incoming POST's token should match the one set in Heroku 62 | if len(r.Form[keyToken]) == 0 || r.Form[keyToken][0] != os.Getenv(tokenConfig) { 63 | return "Config error." 64 | } 65 | if len(r.Form[keyText]) == 0 { 66 | return "Slack bug; inform the team." 67 | } 68 | msg := strings.TrimSpace(r.Form[keyText][0]) 69 | matches := payloadExp.FindStringSubmatch(msg) 70 | if matches == nil { 71 | return "Failed; message should be like: /anon @ashwin hey what's up?" 72 | } 73 | user := matches[1] 74 | msg = strings.TrimSpace(matches[2]) 75 | err = sendAnonymousMessage(user, msg) 76 | if err != nil { 77 | return "Failed to send message." 78 | } 79 | return fmt.Sprintf("Anonymously sent [%s] to %s", msg, user) 80 | } 81 | 82 | // sendAnonymousMessage uses an incoming hook to Direct Message 83 | // the given user the message, from a random animal. 84 | func sendAnonymousMessage(username, message string) error { 85 | url := os.Getenv(webhookConfig) 86 | payload, err := json.Marshal(slackMsg{ 87 | Text: message, 88 | Channel: username, 89 | Username: fmt.Sprintf("an anonymous %s", animals[rand.Intn(len(animals))]), 90 | }) 91 | if err != nil { 92 | return err 93 | } 94 | _, err = http.Post(url, "application/json", bytes.NewBuffer(payload)) 95 | return err 96 | } 97 | 98 | func main() { 99 | rand.Seed(time.Now().UnixNano()) 100 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 101 | result := readAnonymousMessage(r) 102 | fmt.Fprintf(w, result) 103 | }) 104 | http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 105 | } 106 | 107 | func init() { 108 | flag.IntVar(&port, "port", 5000, "HTTP server port") 109 | flag.Parse() 110 | } 111 | --------------------------------------------------------------------------------