├── .gitignore ├── .godir ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── app.json ├── go.mod ├── go.sum ├── httpd.go ├── main.go ├── message.go ├── message_bus.go ├── slack.go └── takosan.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | takosan 27 | pkg/* 28 | -------------------------------------------------------------------------------- /.godir: -------------------------------------------------------------------------------- 1 | takosan 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kentaro Kuribayashi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | release: 2 | gox -output "pkg/{{.Dir}}_{{.OS}}_{{.Arch}}"; ghr -u kentaro -r takosan --delete $(VERSION) pkg/ 3 | 4 | gox: 5 | test -e $(GOPATH)/bin/gox || go get github.com/mitchellh/gox 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: takosan -host 0.0.0.0 -port $PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Takosan 2 | 3 | Takosan is a simple Web interface to Slack ([Ikachan](https://github.com/yappo/p5-App-Ikachan) for Slack). 4 | 5 | ![](./takosan.jpg) 6 | 7 | _Illustrated by [@demiflare168](https://twitter.com/demiflare168)_ 8 | 9 | ## Installing 10 | 11 | ### For Users 12 | 13 | You can choose and get binaries from the [releases](https://github.com/kentaro/takosan/releases) like below: 14 | 15 | ``` 16 | $ wget https://github.com/kentaro/takosan/releases/download/1.0.9/takosan_linux_amd64 -O takosan 17 | $ chmod +x takosan 18 | ``` 19 | 20 | ### For Developers 21 | 22 | Just `go get` as below: 23 | 24 | ``` 25 | $ go get github.com/kentaro/takosan 26 | ``` 27 | 28 | ### Deploy to Heroku 29 | 30 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 31 | 32 | ## Usage 33 | 34 | First, set your Slack API token. 35 | 36 | ``` 37 | $ export SLACK_API_TOKEN="YOUR SLACK API TOKEN" 38 | ``` 39 | 40 | Then, execute `takosan` command like below: 41 | 42 | ``` 43 | $ takosan [-host string] [-port int] [-name string] [-icon string] 44 | ``` 45 | 46 | ## Options 47 | 48 | ### `-host` (default: "127.0.0.1") 49 | 50 | The interface which `takosan` binds. 51 | 52 | ### `-port` (default: 4979) 53 | 54 | The port to which `takosan` listens. 55 | 56 | ### `-name` (default: "takosan") 57 | 58 | The name which you want to display on Slack for this bot. 59 | 60 | ### `-icon` (default: the URL of the image above) 61 | 62 | The icon URL which you want to display on Slack for this bot. 63 | 64 | See [httpd.go](./httpd.go) for other params. 65 | 66 | ## API 67 | 68 | ### `/notice` 69 | ### `/privmsg` 70 | 71 | ``` 72 | $ curl -d "channel=#channel&message=test message" localhost:4979/privmsg 73 | ``` 74 | 75 | You can use both of the endpoints to send messages to Slack. No change can be seen on Slack, though. 76 | 77 | ### `/join` 78 | ### `/leave` 79 | 80 | When you post requests to these endpoints, the server always returns `404`. Which is because you don't need to join/leave groups on Slack explicitely. 81 | 82 | ## License 83 | 84 | MIT 85 | 86 | ## Author 87 | 88 | [Kentaro Kuribayashi](http://kentarok.org) 89 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "takosan", 3 | "description": "Takosan is a simple Web interface to Slack (Ikachan for Slack).", 4 | "repository": "https://github.com/kentaro/takosan", 5 | "env": { 6 | "BUILDPACK_URL": "https://github.com/kr/heroku-buildpack-go.git", 7 | "SLACK_API_TOKEN": { 8 | "description": "Your Slack API token" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kentaro/takosan 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab 7 | github.com/martini-contrib/binding v0.0.0-20160701174519-05d3e151b6cf 8 | github.com/slack-go/slack v0.10.1 9 | ) 10 | 11 | require ( 12 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect 13 | github.com/gorilla/websocket v1.4.2 // indirect 14 | github.com/pkg/errors v0.8.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= 2 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= 6 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= 7 | github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= 8 | github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 9 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 10 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 11 | github.com/martini-contrib/binding v0.0.0-20160701174519-05d3e151b6cf h1:6YSkbjZVghliN7zwJC/U3QQG+OVXOrij3qQ8sxfPIMg= 12 | github.com/martini-contrib/binding v0.0.0-20160701174519-05d3e151b6cf/go.mod h1:aCggxkm1kuifLw/LEQUbz91N1ZM6PhV7dz03xPQduZA= 13 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 14 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/slack-go/slack v0.10.1 h1:BGbxa0kMsGEvLOEoZmYs8T1wWfoZXwmQFBb6FgYCXUA= 18 | github.com/slack-go/slack v0.10.1/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= 19 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 20 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 21 | -------------------------------------------------------------------------------- /httpd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-martini/martini" 6 | "github.com/martini-contrib/binding" 7 | "log" 8 | "math" 9 | "time" 10 | ) 11 | 12 | type Httpd struct { 13 | Host string 14 | Port int 15 | } 16 | 17 | type Param struct { 18 | Channel string `form:"channel" binding:"required"` 19 | Message string `form:"message"` 20 | Name string `form:"name"` 21 | Icon string `form:"icon"` 22 | Fallback string `form:"fallback"` 23 | Color string `form:"color"` 24 | Pretext string `form:"pretext"` 25 | AuthorName string `form:"author_name"` 26 | AuthorLink string `form:"author_link"` 27 | AuthorIcon string `form:"author_icon"` 28 | Title string `form:"title"` 29 | TitleLink string `form:"title_link"` 30 | Text string `form:"text"` 31 | FieldTitle []string `form:"field_title[]"` 32 | FieldValue []string `form:"field_value[]"` 33 | FieldShort []bool `form:"field_short[]"` 34 | ImageURL string `form:"image_url"` 35 | Manual bool `form:"manual"` 36 | PostAt int64 `form:"post_at"` 37 | } 38 | 39 | func NewHttpd(host string, port int) *Httpd { 40 | return &Httpd{ 41 | Host: host, 42 | Port: port, 43 | } 44 | } 45 | 46 | func (h *Httpd) Run() { 47 | m := martini.Classic() 48 | m.Get("/", func() string { return "Hello, I'm Takosan!!1" }) 49 | m.Post("/notice", binding.Bind(Param{}), messageHandler) 50 | m.Post("/privmsg", binding.Bind(Param{}), messageHandler) 51 | m.RunOnAddr(fmt.Sprintf("%s:%d", h.Host, h.Port)) 52 | } 53 | 54 | func messageHandler(p Param) (int, string) { 55 | ch := make(chan error, 1) 56 | 57 | if p.PostAt > 0 { 58 | diff := p.PostAt - time.Now().Unix() 59 | delay := int64(math.Max(float64(diff), 0)) 60 | 61 | return sendLater(p, delay, ch) 62 | } else { 63 | return sendNow(p, ch) 64 | } 65 | } 66 | 67 | func sendNow(p Param, ch chan error) (int, string) { 68 | go MessageBus.Publish(NewMessage(p, ch), 0) 69 | err := <-ch 70 | 71 | if err != nil { 72 | message := fmt.Sprintf("Failed to send message to %s: %s\n", p.Channel, err) 73 | log.Printf(fmt.Sprintf("[error] %s", message)) 74 | return 400, message 75 | } else { 76 | return 200, fmt.Sprintf("Message sent successfully to %s", p.Channel) 77 | } 78 | } 79 | 80 | func sendLater(p Param, delay int64, ch chan error) (int, string) { 81 | go MessageBus.Publish(NewMessage(p, ch), delay) 82 | 83 | return 200, fmt.Sprintf("Message accepted and will be sent after %d seconds", delay) 84 | } 85 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | ) 7 | 8 | var host string 9 | var port int 10 | var name string 11 | var icon string 12 | 13 | func init() { 14 | flag.StringVar(&host, "host", "127.0.0.1", "host") 15 | flag.IntVar(&port, "port", 4979, "port number") 16 | flag.StringVar(&name, "name", "takosan", "bot name") 17 | flag.StringVar(&icon, "icon", "https://raw.githubusercontent.com/kentaro/takosan/master/takosan.jpg", "icon for takosan") 18 | flag.Parse() 19 | } 20 | 21 | func main() { 22 | slack := NewSlack(name, icon, os.Getenv("SLACK_API_TOKEN")) 23 | MessageBus.Subscribe(slack) 24 | httpd := NewHttpd(host, port) 25 | httpd.Run() 26 | } 27 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Message struct { 4 | Channel string 5 | Message string 6 | Name string 7 | Icon string 8 | Attachment *Attachment 9 | Manual bool 10 | Result chan error 11 | } 12 | 13 | type Attachment struct { 14 | Color string 15 | Pretext string 16 | AuthorName string 17 | AuthorLink string 18 | AuthorIcon string 19 | Title string 20 | TitleLink string 21 | Fallback string 22 | Text string 23 | Fields []Field 24 | ImageURL string 25 | } 26 | 27 | type Field struct { 28 | Title string 29 | Value string 30 | Short bool 31 | } 32 | 33 | func (p *Param) HasAttachment() bool { 34 | if (p.Text != "") || 35 | (p.Color != "") || 36 | (p.Fallback != "") || 37 | (p.Pretext != "") || 38 | (p.AuthorName != "") || 39 | (p.AuthorLink != "") || 40 | (p.AuthorIcon != "") || 41 | (p.Title != "") || 42 | (p.TitleLink != "") || 43 | (p.ImageURL != "") || 44 | (len(p.FieldTitle) > 0) || 45 | (len(p.FieldValue) > 0) { 46 | return true 47 | } 48 | 49 | return false 50 | } 51 | 52 | func (p *Param) HasField() bool { 53 | if (len(p.FieldTitle) > 0) || (len(p.FieldValue) > 0) { 54 | return true 55 | } 56 | 57 | return false 58 | } 59 | 60 | func (p *Param) Adjust() { 61 | if p.Name == "" { 62 | p.Name = name 63 | } 64 | if p.Icon == "" { 65 | p.Icon = icon 66 | } 67 | 68 | if p.Manual == false { 69 | if p.Message != "" && p.Text == "" && p.Color != "" { 70 | p.Text = p.Message 71 | p.Message = "" 72 | } 73 | if p.Text != "" && p.Fallback == "" { 74 | p.Fallback = p.Text 75 | } 76 | } 77 | } 78 | 79 | func NewMessage(p Param, ch chan error) *Message { 80 | p.Adjust() 81 | 82 | message := Message{ 83 | Channel: p.Channel, 84 | Message: p.Message, 85 | Name: p.Name, 86 | Icon: p.Icon, 87 | Manual: p.Manual, 88 | Result: ch, 89 | } 90 | 91 | if p.HasAttachment() { 92 | message.Attachment = NewAttachment(p) 93 | } 94 | 95 | return &message 96 | } 97 | 98 | func NewAttachment(p Param) *Attachment { 99 | attachment := Attachment{ 100 | Fallback: p.Fallback, 101 | Color: p.Color, 102 | Pretext: p.Pretext, 103 | AuthorName: p.AuthorName, 104 | AuthorLink: p.AuthorLink, 105 | AuthorIcon: p.AuthorIcon, 106 | Title: p.Title, 107 | TitleLink: p.TitleLink, 108 | Text: p.Text, 109 | ImageURL: p.ImageURL, 110 | } 111 | 112 | if p.HasField() { 113 | attachment.Fields = NewFields(p) 114 | } 115 | 116 | return &attachment 117 | } 118 | 119 | func NewFields(p Param) []Field { 120 | field_title_max := len(p.FieldTitle) 121 | field_value_max := len(p.FieldValue) 122 | field_short_max := len(p.FieldShort) 123 | 124 | field_max := 0 125 | if field_title_max >= field_value_max { 126 | field_max = field_title_max 127 | } else { 128 | field_max = field_value_max 129 | } 130 | 131 | fields := make([]Field, field_max) 132 | for i := range fields { 133 | if i < field_title_max { 134 | fields[i].Title = p.FieldTitle[i] 135 | } 136 | if i < field_value_max { 137 | fields[i].Value = p.FieldValue[i] 138 | } 139 | if i < field_short_max { 140 | fields[i].Short = p.FieldShort[i] 141 | } 142 | } 143 | 144 | return fields 145 | } 146 | -------------------------------------------------------------------------------- /message_bus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Bus struct { 8 | queue chan *Message 9 | } 10 | 11 | type Subscriber interface { 12 | onMessage(message *Message) error 13 | } 14 | 15 | var MessageBus = &Bus{ 16 | queue: make(chan *Message), 17 | } 18 | 19 | func (b Bus) Publish(message *Message, delay int64) { 20 | time.Sleep(time.Duration(delay) * time.Second) 21 | b.queue <- message 22 | } 23 | 24 | func (b Bus) Subscribe(subscriber Subscriber) { 25 | go func() { 26 | for { 27 | message := <-b.queue 28 | 29 | // To comply with API rate limit requirement 30 | // https://api.slack.com/docs/rate-limits 31 | done := make(chan interface{}, 1) 32 | go func() { 33 | done <- time.After(1 * time.Second) 34 | }() 35 | 36 | err := subscriber.onMessage(message) 37 | message.Result <- err 38 | 39 | <-done 40 | } 41 | }() 42 | } 43 | -------------------------------------------------------------------------------- /slack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/slack-go/slack" 5 | "regexp" 6 | ) 7 | 8 | type Slack struct { 9 | Name string 10 | Icon string 11 | Client *slack.Client 12 | } 13 | 14 | func NewSlack(name, icon, token string) *Slack { 15 | return &Slack{ 16 | Name: name, 17 | Icon: icon, 18 | Client: slack.New(token), 19 | } 20 | } 21 | 22 | func (s *Slack) onMessage(message *Message) error { 23 | postMessage := slack.PostMessageParameters{ 24 | Username: message.Name, 25 | LinkNames: 1, 26 | } 27 | 28 | re := regexp.MustCompile("^:.*:$") 29 | if re.MatchString(message.Icon) { 30 | postMessage.IconEmoji = message.Icon 31 | } else { 32 | postMessage.IconURL = message.Icon 33 | } 34 | 35 | var attachment slack.Attachment 36 | if message.Attachment != nil { 37 | attachment = slack.Attachment{ 38 | Fallback: message.Attachment.Fallback, 39 | Color: message.Attachment.Color, 40 | Pretext: message.Attachment.Pretext, 41 | AuthorName: message.Attachment.AuthorName, 42 | AuthorLink: message.Attachment.AuthorLink, 43 | AuthorIcon: message.Attachment.AuthorIcon, 44 | Title: message.Attachment.Title, 45 | TitleLink: message.Attachment.TitleLink, 46 | Text: message.Attachment.Text, 47 | ImageURL: message.Attachment.ImageURL, 48 | MarkdownIn: []string{"text", "pretext", "fields"}, 49 | } 50 | if len(message.Attachment.Fields) > 0 { 51 | fields := make([]slack.AttachmentField, len(message.Attachment.Fields)) 52 | for i := range fields { 53 | fields[i].Title = message.Attachment.Fields[i].Title 54 | fields[i].Value = message.Attachment.Fields[i].Value 55 | fields[i].Short = message.Attachment.Fields[i].Short 56 | } 57 | attachment.Fields = fields 58 | } 59 | } 60 | 61 | _, _, err := s.Client.PostMessage( 62 | message.Channel, 63 | slack.MsgOptionText(message.Message, false), 64 | slack.MsgOptionAttachments(attachment), 65 | slack.MsgOptionPostMessageParameters(postMessage), 66 | ) 67 | 68 | return err 69 | } 70 | -------------------------------------------------------------------------------- /takosan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentaro/takosan/cdbb97e6cc0e7523a195a842b72d33cc450729f2/takosan.jpg --------------------------------------------------------------------------------