├── .gitignore ├── LICENSE ├── README.md ├── goslash ├── attachment.go ├── client.go └── slash.go └── plugins ├── akari └── akari.go ├── echo └── echo.go ├── googleimage └── googleimage.go ├── lgtm └── lgtm.go ├── plugins.go ├── suddendeath └── suddendeath.go └── time └── time.go /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go template 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | ### JetBrains template 27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 28 | 29 | *.iml 30 | 31 | ## Directory-based project format: 32 | .idea/ 33 | # if you remove the above rule, at least ignore the following: 34 | 35 | # User-specific stuff: 36 | # .idea/workspace.xml 37 | # .idea/tasks.xml 38 | # .idea/dictionaries 39 | 40 | # Sensitive or high-churn files: 41 | # .idea/dataSources.ids 42 | # .idea/dataSources.xml 43 | # .idea/sqlDataSources.xml 44 | # .idea/dynamic.xml 45 | # .idea/uiDesigner.xml 46 | 47 | # Gradle: 48 | # .idea/gradle.xml 49 | # .idea/libraries 50 | 51 | # Mongo Explorer plugin: 52 | # .idea/mongoSettings.xml 53 | 54 | ## File-based project format: 55 | *.ipr 56 | *.iws 57 | 58 | ## Plugin-specific files: 59 | 60 | # IntelliJ 61 | /out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | 74 | # Created by .ignore support plugin (hsz.mobi) 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 kyokomi 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goslash 2 | slack slash commands library for golang 3 | -------------------------------------------------------------------------------- /goslash/attachment.go: -------------------------------------------------------------------------------- 1 | package goslash 2 | 3 | // Attachment contains all the information for an attachment 4 | type Attachment struct { 5 | Color string `json:"color,omitempty"` 6 | Fallback string `json:"fallback"` 7 | 8 | AuthorName string `json:"author_name,omitempty"` 9 | AuthorSubname string `json:"author_subname,omitempty"` 10 | AuthorLink string `json:"author_link,omitempty"` 11 | AuthorIcon string `json:"author_icon,omitempty"` 12 | 13 | Title string `json:"title,omitempty"` 14 | TitleLink string `json:"title_link,omitempty"` 15 | Pretext string `json:"pretext,omitempty"` 16 | Text string `json:"text"` 17 | 18 | ImageURL string `json:"image_url,omitempty"` 19 | ThumbURL string `json:"thumb_url,omitempty"` 20 | 21 | Fields []AttachmentField `json:"fields,omitempty"` 22 | MarkdownIn []string `json:"mrkdwn_in,omitempty"` 23 | } 24 | 25 | // AttachmentField contains information for an attachment field 26 | // An Attachment can contain multiple of these 27 | type AttachmentField struct { 28 | Title string `json:"title"` 29 | Value string `json:"value"` 30 | Short bool `json:"short"` 31 | } 32 | -------------------------------------------------------------------------------- /goslash/client.go: -------------------------------------------------------------------------------- 1 | package goslash 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Client struct { 8 | *http.Client 9 | 10 | SlashCommand *SlashCommandService 11 | } 12 | 13 | func New(httpClient *http.Client) Client { 14 | client := Client{ 15 | Client: httpClient, 16 | } 17 | client.SlashCommand = &SlashCommandService{client: &client} 18 | 19 | return client 20 | } 21 | -------------------------------------------------------------------------------- /goslash/slash.go: -------------------------------------------------------------------------------- 1 | package goslash 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | type SlashCommandRequest struct { 11 | Token string `json:"token"` 12 | TeamID string `json:"team_id"` 13 | TeamDomain string `json:"team_domain"` 14 | ChannelID string `json:"channel_id"` 15 | ChannelName string `json:"channel_name"` 16 | UserID string `json:"user_id"` 17 | UserName string `json:"user_name"` 18 | Command string `json:"command"` 19 | Text string `json:"text"` 20 | ResponseURL string `json:"response_url"` 21 | } 22 | 23 | func (r *SlashCommandRequest) CmdArgs() (cmd string, args []string) { 24 | fields := strings.Fields(r.Text) 25 | if len(fields) == 0 { 26 | return "", []string{} 27 | } 28 | cmd, args = fields[0], fields[1:] 29 | return cmd, args 30 | } 31 | 32 | type SlashCommandMessage struct { 33 | ResponseType string `json:"response_type,omitempty"` 34 | Text string `json:"text"` 35 | Attachments []Attachment `json:"attachments,omitempty"` 36 | } 37 | 38 | func NewMessage(text string) SlashCommandMessage { 39 | return SlashCommandMessage{ 40 | Text: text, 41 | Attachments: []Attachment{}, 42 | } 43 | } 44 | 45 | func NewInChannelMessage(text string) SlashCommandMessage { 46 | return SlashCommandMessage{ 47 | ResponseType: "in_channel", 48 | Text: text, 49 | Attachments: []Attachment{}, 50 | } 51 | } 52 | 53 | func ParseFormSlashCommandRequest(r *http.Request) (SlashCommandRequest, error) { 54 | if err := r.ParseForm(); err != nil { 55 | return SlashCommandRequest{}, err 56 | } 57 | return SlashCommandRequest{ 58 | Token: r.PostForm.Get("token"), 59 | TeamID: r.PostForm.Get("team_id"), 60 | TeamDomain: r.PostForm.Get("team_domain"), 61 | ChannelID: r.PostForm.Get("channel_id"), 62 | ChannelName: r.PostForm.Get("channel_name"), 63 | UserID: r.PostForm.Get("user_id"), 64 | UserName: r.PostForm.Get("user_name"), 65 | Command: r.PostForm.Get("command"), 66 | Text: r.PostForm.Get("text"), 67 | ResponseURL: r.PostForm.Get("response_url"), 68 | }, nil 69 | } 70 | 71 | type SlashCommandService struct { 72 | client *Client 73 | } 74 | 75 | func (c *SlashCommandService) Reply(req SlashCommandRequest, msg SlashCommandMessage) (*http.Response, error) { 76 | var jsonData bytes.Buffer 77 | if err := json.NewEncoder(&jsonData).Encode(&msg); err != nil { 78 | return nil, err 79 | } 80 | 81 | return c.client.Post( 82 | req.ResponseURL, 83 | "application/json; charset=utf-8", 84 | &jsonData, 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /plugins/akari/akari.go: -------------------------------------------------------------------------------- 1 | package akari 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kyokomi/goslash/goslash" 8 | "github.com/kyokomi/goslash/plugins" 9 | ) 10 | 11 | type plugin struct { 12 | } 13 | 14 | func New() plugins.Plugin { 15 | return &plugin{} 16 | } 17 | 18 | func (p *plugin) Do(req goslash.SlashCommandRequest) goslash.SlashCommandMessage { 19 | _, args := req.CmdArgs() 20 | message := strings.Join(args, " ") 21 | 22 | return goslash.NewInChannelMessage( 23 | fmt.Sprintf("わぁい%s あかり%s大好き", message, message), 24 | ) 25 | } 26 | 27 | var _ plugins.Plugin = (*plugin)(nil) 28 | -------------------------------------------------------------------------------- /plugins/echo/echo.go: -------------------------------------------------------------------------------- 1 | package echo 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/kyokomi/goslash/goslash" 7 | "github.com/kyokomi/goslash/plugins" 8 | ) 9 | 10 | type plugin struct { 11 | } 12 | 13 | func New() plugins.Plugin { 14 | return &plugin{} 15 | } 16 | 17 | func (p *plugin) Do(req goslash.SlashCommandRequest) goslash.SlashCommandMessage { 18 | _, args := req.CmdArgs() 19 | return goslash.NewInChannelMessage(strings.Join(args, "")) 20 | } 21 | -------------------------------------------------------------------------------- /plugins/googleimage/googleimage.go: -------------------------------------------------------------------------------- 1 | package googleimage 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "strings" 7 | "time" 8 | 9 | "github.com/kyokomi/goslash/goslash" 10 | "github.com/kyokomi/goslash/plugins" 11 | "github.com/kyokomi/slackbot/plugins/googleimage" 12 | ) 13 | 14 | type plugin struct { 15 | rd *rand.Rand 16 | client googleimage.GoogleImageAPIClient 17 | cx string 18 | apiKey string 19 | } 20 | 21 | func NewPlugin(client googleimage.GoogleImageAPIClient) plugins.Plugin { 22 | return &plugin{ 23 | rd: rand.New(rand.NewSource(time.Now().UnixNano())), 24 | client: client, 25 | } 26 | } 27 | 28 | func (p *plugin) Do(req goslash.SlashCommandRequest) goslash.SlashCommandMessage { 29 | _, args := req.CmdArgs() 30 | message := strings.Join(args, " ") 31 | 32 | query := strings.Replace(strings.TrimLeft(message, "image"), "image", "", 1) 33 | 34 | links, err := p.client.GetImageLinks(query) 35 | if err != nil { 36 | log.Println(err) 37 | goslash.NewMessage(err.Error()) 38 | } 39 | 40 | idx := int(p.rd.Int() % len(links)) 41 | chMsg := goslash.NewInChannelMessage("") 42 | chMsg.Attachments = append(chMsg.Attachments, goslash.Attachment{ 43 | Text: query, 44 | ImageURL: links[idx], 45 | }) 46 | return chMsg 47 | } 48 | 49 | var _ plugins.Plugin = (*plugin)(nil) 50 | -------------------------------------------------------------------------------- /plugins/lgtm/lgtm.go: -------------------------------------------------------------------------------- 1 | package lgtm 2 | 3 | import ( 4 | "strings" 5 | 6 | "net/http" 7 | 8 | "github.com/PuerkitoBio/goquery" 9 | "github.com/kyokomi/goslash/goslash" 10 | "github.com/kyokomi/goslash/plugins" 11 | ) 12 | 13 | const lgtmURL = "http://lgtm.in/g" 14 | 15 | type plugin struct { 16 | httpClient *http.Client 17 | } 18 | 19 | func New(httpClient *http.Client) plugins.Plugin { 20 | return &plugin{ 21 | httpClient: httpClient, 22 | } 23 | } 24 | 25 | func (p *plugin) Do(req goslash.SlashCommandRequest) goslash.SlashCommandMessage { 26 | _, args := req.CmdArgs() 27 | 28 | sendMessage, ok := p.getLGTMImageURL(p.buildRandomURL(strings.Join(args, " "))) 29 | if ok { 30 | return goslash.NewInChannelMessage(sendMessage) 31 | } else { 32 | return goslash.NewMessage(sendMessage) 33 | } 34 | } 35 | 36 | func (p *plugin) buildRandomURL(message string) string { 37 | randomURL := lgtmURL 38 | args := strings.Fields(message) 39 | if len(args) == 2 { 40 | randomURL += "/" + args[1] 41 | } 42 | return randomURL 43 | } 44 | 45 | func (p *plugin) getLGTMImageURL(lgtmURL string) (string, bool) { 46 | res, err := p.httpClient.Get(lgtmURL) 47 | if err != nil { 48 | return err.Error(), false 49 | } 50 | defer res.Body.Close() 51 | 52 | doc, err := goquery.NewDocumentFromResponse(res) 53 | if err != nil { 54 | return err.Error(), false 55 | } 56 | 57 | text, exists := doc.Find("#imageUrl").Attr("value") 58 | if !exists { 59 | return lgtmURL + ": not exists", false 60 | } 61 | 62 | return text, true 63 | } 64 | 65 | var _ plugins.Plugin = (*plugin)(nil) 66 | -------------------------------------------------------------------------------- /plugins/plugins.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kyokomi/goslash/goslash" 8 | ) 9 | 10 | type SlashCommand interface { 11 | AddPlugin(cmd string, plugin Plugin) 12 | Execute(req goslash.SlashCommandRequest) string 13 | } 14 | 15 | type slashCommand struct { 16 | slash *goslash.SlashCommandService 17 | plugins map[string]Plugin 18 | } 19 | 20 | func (c *slashCommand) AddPlugin(cmd string, plugin Plugin) { 21 | c.plugins[cmd] = plugin 22 | } 23 | 24 | func (c *slashCommand) Execute(req goslash.SlashCommandRequest) string { 25 | cmd, _ := req.CmdArgs() 26 | p, ok := c.plugins[cmd] 27 | if !ok { 28 | return fmt.Sprintf("%s command not found", cmd) 29 | } 30 | 31 | resp, err := c.slash.Reply(req, p.Do(req)) 32 | if err != nil { 33 | return err.Error() 34 | } 35 | resp.Body.Close() 36 | 37 | return "" 38 | } 39 | 40 | func New(client *http.Client, plugins map[string]Plugin) SlashCommand { 41 | return &slashCommand{ 42 | slash: goslash.New(client).SlashCommand, 43 | plugins: plugins, 44 | } 45 | } 46 | 47 | type Plugin interface { 48 | Do(req goslash.SlashCommandRequest) goslash.SlashCommandMessage 49 | } 50 | -------------------------------------------------------------------------------- /plugins/suddendeath/suddendeath.go: -------------------------------------------------------------------------------- 1 | package suddendeath 2 | 3 | import ( 4 | "unicode/utf8" 5 | 6 | "github.com/kyokomi/goslash/goslash" 7 | "github.com/kyokomi/goslash/plugins" 8 | ) 9 | 10 | type plugin struct { 11 | } 12 | 13 | func New() plugins.Plugin { 14 | return &plugin{} 15 | } 16 | 17 | func (p *plugin) Do(req goslash.SlashCommandRequest) goslash.SlashCommandMessage { 18 | _, args := req.CmdArgs() 19 | 20 | text := "突然の" + args[0] 21 | size := utf8.RuneCountInString(text) 22 | header := "" 23 | for i := 0; i < size+2; i++ { 24 | header += "人" 25 | } 26 | 27 | fotter := "" 28 | for i := 0; i < size; i++ { 29 | fotter += "^Y" 30 | } 31 | 32 | reMessage := "_" + header + "_" 33 | reMessage += "\n" 34 | reMessage += "> " + text + " <" 35 | reMessage += "\n" 36 | reMessage += " ̄Y" + fotter + " ̄" 37 | 38 | return goslash.NewInChannelMessage(reMessage) 39 | } 40 | 41 | var _ plugins.Plugin = (*plugin)(nil) 42 | -------------------------------------------------------------------------------- /plugins/time/time.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kyokomi/goslash/goslash" 7 | "github.com/kyokomi/goslash/plugins" 8 | ) 9 | 10 | type plugin struct { 11 | } 12 | 13 | func New() plugins.Plugin { 14 | return &plugin{} 15 | } 16 | 17 | func (p *plugin) Do(_ goslash.SlashCommandRequest) goslash.SlashCommandMessage { 18 | return goslash.NewInChannelMessage( 19 | time.Now().Format(time.RFC3339), 20 | ) 21 | } 22 | --------------------------------------------------------------------------------