├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── Procfile ├── Readme.md ├── dockerdep └── ca-certificates.crt ├── docs ├── eventcb.md ├── install.md ├── linkcb.md ├── lua.md ├── messagecb.md ├── plugins.md ├── questioncb.md ├── screenshots │ ├── add_bot_integration.png │ ├── lazlo.png │ ├── link_choice.png │ ├── link_choice1.png │ └── ping_lazlo.png └── timercb.md ├── importfix.sh ├── lib ├── api.go ├── brain.go ├── broker.go ├── callbacks.go ├── config.go ├── httpserver.go ├── slackTypes.go └── timers.go ├── loadModules.go ├── lua └── test.lua ├── main.go └── modules ├── braintest.go ├── help.go ├── linktest.go ├── luaMod.go ├── ping.go ├── qtest.go └── rtmping.go /.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 | rt_start* 26 | lazlo 27 | tmp/ 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD dockerdep/ca-certificates.crt /etc/ssl/certs/ 3 | ADD lazlo / 4 | CMD ["/lazlo"] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Josephsen 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 | all: go docker 2 | 3 | go: 4 | CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o lazlo . 5 | 6 | docker: 7 | docker build -t lazlo . 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: slacker 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Lazlo 2 | ## An event-driven, lua-scriptable chatops automation framework for Slack in Go. (phew) 3 | 4 | The prototypical IRC bot responds to text. Generally, the pattern is you 5 | provide a regex to match on, and some code to run when someone says something 6 | in chat that matches your regular expression. Your plugin runs when a pattern 7 | match happens, and then returns. 8 | 9 | Your Lazlo module, by comparison is started at runtime and stays resident in 10 | memory. Outwardly, Lazlo *acts* like a bot, but internally Lazlo works as an 11 | event broker. Your module registers for callbacks -- you can tell Lazlo what 12 | sorts of events your module finds interesting. For each callback your module 13 | registers, Lazlo will hand back a *channel*. Your module can block on the 14 | channel, waiting for something to happen, or it can register more callbacks (as 15 | many as you have memory for), and select between them in a loop. Throughout its 16 | lifetime, your Module can de-register the callbacks it doesn't need anymore, and 17 | ask for new ones as circumstances demand. 18 | 19 | ![](docs/screenshots/lazlo.png) 20 | 21 | Currently there are five different kinds of callbacks you can ask for. 22 | 23 | * [Message callbacks](docs/messagecb.md) specify regex you want to listen for and respond to. 24 | * [Event callbacks](docs/eventcb.md) specify [slack api events](https://api.slack.com/events) you want to listen for and respond to. 25 | * [Timer Callbacks](docs/timercb.md) start a (possibly reoccuring) timer (in cron syntax), and notify you when it runs down 26 | * [Link Callbacks](docs/linkcb.md) create a URL that users can click on. When they do, their GET request is brokered back to your module. (Post and Put support coming soon) 27 | * [Question Callbacks](docs/questioncb.md) make it easy to ask users a question, and capture their response. 28 | 29 | Your module can register for all or none of these, as many times as it likes 30 | during the lifetime of the bot. Lazlo makes it easier to write modules that 31 | carry out common chat-ops patterns. For example, you can pretty easily write a 32 | module that: 33 | 34 | 1. registers for a message callback for `bot deploy (\w+)` 35 | 2. blocks waiting for that command to be executed 36 | 3. when executed, registers for a message callback that matches the specific user that asked for the deploy with the regex: 'authenticate ' 37 | 4. DM's that user prompting for a password 38 | 5. registers a timer callback that expires in 3 minutes 39 | 6. Blocks waiting for either the password or the timer 40 | 7. Authenticates the user, and runs the CM tool of the week to perform the deploy 41 | 8. Captures output from that tool and presents it back to the user 42 | 9. de-registers the timer and password callbacks 43 | 44 | That's an oversimplified example, but I hope you get the idea. Check out the 45 | Modules directory for working examples that use the various callbacks. 46 | 47 | ## Lua plug-ins 48 | Lazlo's event-driven framework is quite flexible. You can use it to write some 49 | pretty powerful modules in Go. For example, I implemented a module that embeds 50 | a lua state machine which makes it possible to extend Lazlo by write simple 51 | plugins that use hubot-like syntax [in lua](docs/lua.md) 52 | 53 | ## Whats next? 54 | * [get up and running](docs/install.md) 55 | * [writing awesome event-driven modules in Go](docs/plugins.md) 56 | * [writing simple, fast modules in lua](docs/lua.md) 57 | 58 | ## Current Status 59 | 60 | Lazlo is basically working and basically documented. All callback types are 61 | implemented and functional and there are several included modules of varying 62 | degress of complexity that should help get you started writing your own. 63 | 64 | ### Todo's in order of when I'll probably get to them: 65 | 66 | * Plugin to keep lazlo metadata in synch with Slack 67 | * Leader-elections for HA bots. (moved up because [heroku changing it's dyno pricing](http://www.octolabs.com/blogs/octoblog/2015/03/31/analysis-of-the-rumored-heroku-pricing-changes/)) 68 | * Godoc, Documentation Documentation and Documentation 69 | * Lua support is new and only includes support for message callbacks (hear respond and reply). I'd like you to be able to get timers and links via lua as well. 70 | * More included plugins 71 | -------------------------------------------------------------------------------- /docs/eventcb.md: -------------------------------------------------------------------------------- 1 | # The event callback 2 | 3 | event callbacks tell Lazlo that you're interested in being notified of API 4 | events other than messages. These are things like new users entering a room, or 5 | someone uploading a snippet or image. The Function call looks like this: 6 | 7 | ``` 8 | cb := broker.EventCallback(`type`, `foo`) 9 | ``` 10 | 11 | The formal definition is [here](https://github.com/djosephsen/lazlo/blob/master/lib/callbacks.go#L132) in callbacks.go 12 | 13 | As you can see, *EventCallback* is a method on the lazlo.Broker type. 14 | Whenever you write a lazlo module, you'll always be passed a pointer to the 15 | broker ([more about that here](plugins.md)) 16 | 17 | EventCallbacks require two string arguments, the first is a key, and the second 18 | a value. Together they give you a way to say "Hey lazlo, tell me whenever you 19 | see an event that has this key with this value". Check out the [slack API 20 | documentation]() for a list of all possible events. 21 | 22 | Lazlo will give you back an object of type lazlo.EventCallback. It looks like 23 | this: 24 | 25 | ``` 26 | type EventCallback struct { 27 | ID string 28 | Key string 29 | Val string 30 | Chan chan map[string]interface{} 31 | } 32 | ``` 33 | 34 | *ID* uniquely identifies this callback with the broker. It's set automatically 35 | when you register the module, and you can use it to deregister the module later 36 | if you want. 37 | 38 | *key* is a regex that matches a json key value in the event type that you're interested in. 39 | 40 | *val* is a regex that matches the value of the json key in the event type that 41 | you're interested in. 42 | 43 | *Chan* is the cool part, I'll talk more about that in a minute. First, here are a couple examples: 44 | 45 | 46 | "Lazlo, tell me about any non-message event in the 'ops' channel" 47 | ``` 48 | opschan := broker.SlackMeta.GetChannelByName(`ops`) 49 | cb := broker.EventCallback(`Channel`,opschan.ID) 50 | thingy := <- cb.Chan 51 | ``` 52 | 53 | "Lazlo, tell me whenever Tess does anything (except chatting)" 54 | ``` 55 | tess := broker.SlackMeta.GetUserByName(`Tess`) 56 | cb := broker.EventCallback(`User`,tess.ID) 57 | thingy := <- cb.Chan 58 | ``` 59 | 60 | ## Waiting for something to happen 61 | cb.Chan is the Go channel that Lazlo will use to pass you back events that 62 | met your criteria. A common pattern is to block on it waiting for something to 63 | happen that matches your regex like this: 64 | 65 | ``` 66 | thingy := <-cb.Chan 67 | if thingy[`type`] == `user_typing`{ 68 | b.say(`I see you typing Tess`) 69 | } 70 | ``` 71 | 72 | If you do that though, your module will respond to the first event lazlo 73 | brokers back to you, but never again. So you probably want to block in a for 74 | loop like this: 75 | 76 | ``` 77 | for{ 78 | thingy := <-cb.Chan 79 | if thingy[`type`] == `user_typing`{ 80 | b.say(`I see you typing Tess`) 81 | } 82 | } 83 | ``` 84 | 85 | That way, your module will block waiting for an event from lazlo and respond 86 | when it gets one, but then it'll loop back around and block again, waiting for 87 | the next matching event. 88 | 89 | Remember you can register as many callbacks as you like. When you do that, a 90 | common pattern is to select between them like so: 91 | 92 | 93 | ``` 94 | helpChannel := broker.SlackMeta.GetChannelByName(`help`) 95 | cb1 := broker.EventCallback(`type`, `user_entered`) 96 | cb2 := broker.EventCallback(`type`, `new_channel`) 97 | cb3 := broker.EventCallback(`channel`, helpChannel) 98 | 99 | for{ 100 | select{ 101 | case thingy := <-cb1.Chan: 102 | go handle_new_users(thingy) 103 | case thingy := <-cb2.Chan: 104 | go handle_new_channels(thingy) 105 | case thingy := <-cb3.Chan: 106 | go handle_somebody_needs_help(thingy) 107 | } 108 | ``` 109 | 110 | ## Thingy 111 | The thingies passed to you by lazlo.EventCallback.Chan are type 112 | map[string]interface{}. That's why I refer to them as thingies; they're just 113 | JSON blobs from the slack API, wrapped up in a map of interfaces. You can 114 | implement the types yourself if you want, or just use them directly. Refer to 115 | the [slack API]() documentation for the response structure of the object you're 116 | asking for. 117 | 118 | 119 | ## In the Future 120 | I'll probably add an additional EventCallback() method that works in a list 121 | context.. accepting more than one key/value pair, so you can specify multiple 122 | criteria to match. 123 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | ## Up and running in 5 minutes 2 | 3 | 0: Have [Golang](https://golang.org/doc/install) installed 4 | 5 | 1: Select *Configure Integrations* from your team menu in slack 6 | 7 | 2: Add a new *Bots* integration, give your bot a clever name, and take note of your Token 8 | 9 | ![integration](screenshots/add_bot_integration.png) 10 | 11 | 3: 12 | ``` 13 | go get github.com/djosephsen/lazlo 14 | ``` 15 | 16 | 4: 17 | ``` 18 | export LAZLO_NAME= 19 | export LAZLO_TOKEN= 20 | export PORT=5000 21 | export LAZLO_LOG_LEVEL=DEBUG # (optional if you'd like to see verbose console messages) 22 | ``` 23 | 24 | 5: 25 | ``` 26 | lazlo 27 | ``` 28 | 29 | Now you can ping Lazlo to make sure he's alive: 30 | 31 | ![](screenshots/ping_lazlo.png) 32 | 33 | Lazlo comes with a variety of simple plugins to get you started and give you 34 | examples to work from, and it's pretty easy to add your own. [Making and 35 | managing your own plugins](plugins.md) is pretty much why you're here in 36 | the first place after all. 37 | 38 | ## Deploy Lazlo to Heroku and be all #legit in 10 minutes 39 | 40 | 0: Have a github account, a Heroku account, Heroku Toolbelt installed, and upload your ssh key to Github and Heroku 41 | 42 | 1: Select *Configure Integrations* from your team menu in slack 43 | 44 | 2: Add a new *Bots* integration, give your bot a clever name, and take note of your Token 45 | 46 | 3: 47 | ``` 48 | go get github.com/kr/godep 49 | ``` 50 | 51 | 4: Go to https://github.com/djosephsen/lazlo/fork to fork this repository (or click the fork button up there ^^) 52 | 53 | 5 through like 27: 54 | ``` 55 | mkdir -p $GOPATH/github.com/ 56 | cd $GOPATH/github.com/ 57 | git clone git@github.com:/lazlo.git 58 | cd lazlo 59 | git remote add upstream https://github.com/djosephsen/lazlo.git 60 | chmod 755 ./importfix.sh && ./importfix.sh 61 | go get 62 | godep save 63 | heroku create -b https://github.com/kr/heroku-buildpack-go.git 64 | heroku config:set LAZLO_NAME= 65 | heroku config:set LAZLO_TOKEN= 66 | heroku config:set LAZLO_LOG_LEVEL=DEBUG 67 | git add --all . 68 | git commit -am 'lets DO THIS' 69 | git push 70 | git push heroku master 71 | ``` 72 | 73 | At this point you can ping Lazlo to make sure he's alive. 74 | 75 | ![hi](screenshots/ping_lazlo.png) 76 | 77 | ### kind of done mostly 78 | When you make changes or add plugins in the future, you can push them to heroku with: 79 | 80 | ``` 81 | godep save 82 | git add --all . 83 | git commit -am 'snarky commit message' 84 | git push && get push heroku 85 | ``` 86 | 87 | ## Use docker to run lazlo and be one of the cool kids in like 42 seconds 88 | (sorry this isn't actually a thing yet) 89 | 90 | ## What now? 91 | Find out [what lazlo can do](included_plugins.md) out of the box 92 | Get started [adding, removing, and creating plugins](plugins.md) 93 | Learn more about [configuring](configuration.md) Lazlo (there's not much to it) 94 | 95 | -------------------------------------------------------------------------------- /docs/linkcb.md: -------------------------------------------------------------------------------- 1 | # The Link Callback 2 | 3 | Link callbacks let you present your users with embedded URLs they can click on. 4 | I use them to present my users with multiple-choice questions like this: 5 | 6 | ![](screenshots/link_choice.png) 7 | 8 | The callback function looks like this: 9 | 10 | ``` 11 | cb := broker.LinkCallback(`option1`) 12 | cb := broker.LinkCallback(`option2`) 13 | ``` 14 | 15 | The formal definition is [here](https://github.com/djosephsen/lazlo/blob/master/lib/httpserver.go#L55) in httpserver.go 16 | 17 | Link callbacks are a lot different from the other types, and you have to know a 18 | little bit more about how lazlo works to use them effectively. The first thing 19 | you need to know, is that lazlo has a built-in HTTP server. This is implemented 20 | in lib/httpserver.go. 21 | 22 | The next thing you need to know is the built-in HTTP server uses two 23 | environment variables to configure itself. The first of these is *PORT*, and 24 | the second is *LAZLO_URL* 25 | 26 | The *PORT* environment variable dictates the TCP port lazlo listens on. Paas 27 | services like Heroku set this variable for you automatically when they run your 28 | app in a dyno, but if you run Lazlo on localhost, you need to set this or lazlo 29 | won't run. 30 | 31 | The *LAZLO_URL* variable is an optional variable you need to set if you're 32 | using link callbacks. It specifies the public address of the server you're 33 | running Lazlo on. The linkCallback uses this value to create a clickable URL 34 | that lazlo will be able to see. 35 | 36 | ## Now you know what you need to know 37 | Ok, lets say you're running lazlo on an AWS instance, and you set PORT to 5000, 38 | and LAZLO_URL to 54.87.22.100, and then you create a link callback like so: 39 | 40 | ``` 41 | cb := broker.LinkCallback(`option1`) 42 | ``` 43 | 44 | Lazlo is going to create a local API endpoint at /option1, and give you back a 45 | struct of type lazlo.LinkCallback. It looks like this: 46 | 47 | ``` 48 | type LinkCallback struct { 49 | ID string 50 | Path string // the computed URL 51 | URL string 52 | Handler func(res http.ResponseWriter, req *http.Request) 53 | Chan chan *http.Request 54 | } 55 | ``` 56 | 57 | *ID* uniquely identifies your callback (as you're probably used to by now) 58 | 59 | *PATH* is the URL Lazlo intends for you to present your user. In our current 60 | example, PATH would equal this: ``` http://54.87.22.100:5000/option1 ``` 61 | 62 | *URL* is the URL you passed in to broker.LinkCallback() 63 | 64 | *Handler* if you're familiar with [pat](http://github.com/bmizerany/pat), you 65 | can pass in your own http handler function as an optional second argument to 66 | broker.LinkCallback(). If that sounded like greek to you, no worries; ignore 67 | it. 68 | 69 | *Chan* here's the magic part. Whenver a user visits the link specified 70 | by your callback's *Path* attribute, Lazlo will hand your module an 71 | [http.Request]() that represents their click. 72 | 73 | ## Wait, what? 74 | 75 | ;tldr, if you properly set up the PORT and LAZLO_URL environment variables, and Lazlo is reachable from your user's browser, then you can do this: 76 | 77 | ``` 78 | option1 := broker.LinkCallback(`option1`) 79 | option2 := broker.LinkCallback(`option2`) 80 | ``` 81 | 82 | And lazlo will wire up http://localhost/option1 and http://localhost/option2 to 83 | your callback's Chan channel. 84 | 85 | You can present those links to your users by wrapping them in [slack compatible 86 | markdown]() like this: 87 | 88 | ``` 89 | options := fmt.Sprintf("choose <%s|option1> or <%s|option2>", option1.URL, option2.URL) 90 | broker.Say(options) 91 | ``` 92 | 93 | Then you can block waiting for the user to click on an answer: 94 | 95 | ``` 96 | for { 97 | select { 98 | case choice := <-option1.Chan: 99 | broker.Say('you chose option1') 100 | case choice := <-option2.Chan: 101 | broker.Say('you chose option2') 102 | } 103 | } 104 | ``` 105 | -------------------------------------------------------------------------------- /docs/lua.md: -------------------------------------------------------------------------------- 1 | #sorry nothing here yet 2 | -------------------------------------------------------------------------------- /docs/messagecb.md: -------------------------------------------------------------------------------- 1 | # The message callback 2 | 3 | Message callbacks tell Lazlo that you're interested in being notified when 4 | someone says something specific in chat. The function call looks like this: 5 | 6 | ``` 7 | cb := broker.MessageCallback(`flip table`, true) 8 | ``` 9 | 10 | The formal definition is [here](https://github.com/djosephsen/lazlo/blob/master/lib/callbacks.go#L113) in callbacks.go 11 | 12 | As you can see, *MessageCallback* is a method on the lazlo.Broker type. 13 | Whenever you write a lazlo module, you'll always be passed a pointer to the 14 | broker ([more about that here](plugins.md)) 15 | 16 | MessageCallback requires two arguments, the first is a string, and the second a 17 | bool. The string argument specifies a regular expression that matches the text 18 | you want to be notified of. The bool specifies weather or not this message is 19 | a formal command addressed to the bot. When set to *true*, Lazlo will only 20 | broker the message back to your module if it's prefaced with the name of the 21 | your bot. In other words, if you named your bot lazlo, the above example will 22 | only fire when a user says "lazlo flip table". If you set it to *false* it 23 | would fire any time anyone said "flip table" 24 | 25 | Lazlo will give you back an object of type lazlo.MessageCallback. It looks like 26 | this: 27 | 28 | ``` 29 | type MessageCallback struct { 30 | ID string 31 | Pattern string 32 | Respond bool // if true, only respond if the bot is mentioned by name 33 | Chan chan PatternMatch 34 | SlackChan string // if set filter message callbacks to this Slack channel 35 | } 36 | ``` 37 | 38 | *ID* uniquely identifies this callback with the broker. It's set automatically 39 | when you register the module. 40 | 41 | *Pattern* is the regex you specified 42 | 43 | *Respond* is the bool you specified 44 | 45 | *Chan* is the cool part, I'll talk about that next 46 | 47 | *SlackChan* is an optional third argument to MessageCallback(). It's a string 48 | that specifies a slack channel on which you want this callback active (if, for 49 | example, you want *bot deploy* to work in the ops channel but not the main 50 | channel 51 | 52 | ## Waiting for something to happen 53 | So about this cb.Chan thing; This is the Go channel that Lazlo will use to pass 54 | you back messages that met your criteria. A common pattern is to block on it 55 | waiting for someone to type something that matches your regex like this: 56 | 57 | ``` 58 | pm := <-cb.Chan 59 | pm.Event.Respond(`(╯°□°)╯︵ ┻━┻`) 60 | ``` 61 | 62 | If you do that though, your module will respond to the first event lazlo 63 | brokers back to you, but never again. So you probably want to block in a for 64 | loop like this: 65 | 66 | ``` 67 | for{ 68 | pm := <-cb.Chan 69 | pm.Event.Respond(`(╯°□°)╯︵ ┻━┻`) 70 | } 71 | ``` 72 | That way, your module will block waiting for a message from lazlo and respond 73 | when it gets one, but then it'll loop back around and block again, waiting for 74 | another message. 75 | 76 | Remember you can register as many callbacks as you like. When you do that, a 77 | common pattern is to select between them like so: 78 | 79 | 80 | ``` 81 | cb1 := broker.MessageCallback(`flip table`, true) 82 | cb2 := broker.MessageCallback(`I am [0-9]+ mads right now`, false) 83 | cb3 := broker.MessageCallback(`who moved my cheese`, false) 84 | 85 | for{ 86 | select{ 87 | case pm := <-cb1: 88 | go flipFunc(pm) 89 | case pm := <-cb2: 90 | go madsFunc(pm) 91 | case pm := <-cb3: 92 | go cheeseFunc(pm) 93 | } 94 | ``` 95 | 96 | ## lazlo.PatternMatch 97 | You may have noticed that the callback chan was type lazlo.PatternMatch. Good 98 | job! That was very observant of you. PatternMatch is a pretty simple type. It 99 | looks like this: 100 | 101 | ``` 102 | type PatternMatch struct { 103 | Event *Event 104 | Match []string 105 | } 106 | ``` 107 | 108 | *Event* is a pointer to the *lazlo.Event* that matched your regex. It is a very 109 | large and intricate type that gives you everything lazlo knows about the event 110 | including who said the thing that matched your regex, what channel they said it 111 | in, what time it was when they said it and so on and so forth. You can find the 112 | Event type definition in lib/slacktypes. Event also comes with a couple 113 | convienience functions that you can use to respond to the message: *Reply()*, 114 | and *Respond()* (which I used in the example above. The distinction is that 115 | *Reply()* echo's the users name. So if you pm.Event.Reply('I see you') to 116 | something Tess said, Lazlo will literally say "Tess, I see you". 117 | 118 | *Match* is an array of strings. It is exactly what you would get back if you 119 | called [regex.FindAllStringSubmatch]() on pm.Event.Text with your regex 120 | pattern, because that's literally what lazlo is doing for you internally. To 121 | make a very long story short, this means you can use your regex to capture 122 | strings like so: 123 | 124 | ``` 125 | cb := b.MessageCallback(`(?i)(tell me how) (\w+) (I am)`, true) 126 | pm := <- cb.Chan 127 | ``` 128 | 129 | Then if I said "Lazlo tell me how pretty I am" in channel, pm.Match[2] would 130 | equal "pretty", so this... 131 | 132 | ``` 133 | pm.Event.Reply(fmt.Printf(`ZOMG you are 42 %s`, pm.Match[2])) 134 | ``` 135 | 136 | ... would make lazlo respond: "Dave: ZOMG you are 42 pretty" 137 | 138 | 139 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # How do I even plugin? 2 | 3 | There are two ways you can write automation for Lazlo. The quick, simple way, 4 | is to use Lua scripts. If you're familiar with hubot, Lazlo's lua syntax is 5 | very similar. But this documentation is about writing automation in Go, which 6 | is more powerful, and gives you more control but is also a bit more 7 | complicated. 8 | 9 | ## The lazlo.Module type 10 | To write a lazlo module in Go, make a new file in the *modules* directory, 11 | import lazlo's lib package (and whatever else), and create an exporable var of 12 | type lazlo.Module. Then tell Lazlo to ingest your module by adding it to the 13 | *loadModules.go* file in the root of the project directory. Here's a pretty 14 | typical module preamble: 15 | 16 | ``` 17 | package modules 18 | 19 | import( 20 | lazlo "github.com/djosephsen/lazlo/lib" 21 | // other stuff 22 | ) 23 | 24 | var Hi = &lazlo.Module{ // note the capital H in Hi, makes this exportable 25 | Name: `Hi`, 26 | Usage: `"%BOTNAME% foo" : says hi on startup, and also replies "bar" to "%BOTNAME% foo"`, 27 | Run: hiMain, 28 | } 29 | ``` 30 | 31 | The *Module* struct has three attributes, a Name string, a Usage string, and a 32 | function reference. Your module name is used to create an ID that will uniquely 33 | identify your module internally. The Usage string is used by the *help plugin* to 34 | print information about your bot to users. There are a few Macro's the help 35 | module understands that make it a little easier to write usage strings. 36 | 37 | * %BOTNAME% will be replaced with the value of the environment variable LAZLO.NAME 38 | * %HIDDEN% will instruct the *help module* to omit your module from its output 39 | 40 | 41 | ## The Lazlo Modules Run Function 42 | The Run attribute specifies the entry point for your module. You can think of 43 | it as your *main()* function. When Lazlo launches, once it's done with its 44 | initial housekeeping, it'll execute your Run function in its own go-routine. 45 | This is a little different than other bots you might have worked with that only 46 | execute your function when something happens in chat. Under Lazlo, your run 47 | function gets run in parallel at startup, and does whatever you want for as 48 | long as you want. 49 | 50 | Your run function is passed a reference to lazlo's broker, which you can use to 51 | interact with the chatroom in interesting ways, but more on that in a bit. For 52 | now, if we wanted to write a module that just said "*hi*" whenever the Bot 53 | starts up, we could write a run function like this: 54 | 55 | ``` 56 | func hiMain(b *lazlo.Broker){ 57 | b.Say("Hi!") 58 | } 59 | ``` 60 | 61 | Whever Lazlo starts, he'll register our *Hi* module (add it to a slice of 62 | modules), and execute this run function, which will use the broker to say 63 | *Hi!* in the default room, and then exit. I think you'll agree that's not very 64 | interesting. 65 | 66 | ## Registering for callbacks with the broker 67 | The fun stuff begins with *callbacks*. With callbacks, we can ask the broker to 68 | tell us when things happen. The most common kind of callback is a *Message* 69 | callback: 70 | 71 | ``` 72 | func hiMain(b *lazlo.Broker){ 73 | b.Say("Hi!") 74 | cb := b.MessageCallback(`(?i)foo`, true) 75 | } 76 | ``` 77 | 78 | The broker's *MessageCallback* function takes two arguments, a regex, and a 79 | boolean which dictates wheather or not the bot needs to be mentioned by name; 80 | ie: If this is *true* then our regex will match a command like *lazlo foo*, but 81 | when set to false, it'll match anyone saying *foo* arbitrarily (also note the 82 | *(?i)* token which is a golang regex convention for case insensitivity). 83 | 84 | The *MessageCallback* function returns a custom type called, unimaginatively: 85 | *MessageCallback*. A pointer to this callback is registered in a top-level data 86 | structure within the broker. This is the same struct Lazlo uses internally to 87 | check for matches, and enables you to tell the broker to *DeRegister* it later 88 | if you wish. It contains some metainfo about your callback, as well as a 89 | channel, which is really the cruxt of the whole system. 90 | 91 | ``` 92 | type MessageCallback struct{ 93 | ID string //created automatically by the broker 94 | Pattern string //your regex pattern 95 | Respond bool // if true, only respond if the bot is mentioned by name 96 | Chan chan PatternMatch 97 | } 98 | ``` 99 | 100 | ## Callback channels 101 | Each type of callback returns a different type of callback struct, and each 102 | type of callback struct contains a channel of one kind or another. In the case 103 | of the *MessageCallback* type, we get a channel that spits out *PatternMatch* 104 | structs. 105 | 106 | Every time someone says something in chat that matches the regex we specified 107 | when we registered the callback, this channel will spit out a struct with 108 | information about the message that matched. Now we can block, or select on these 109 | channels as a means of responding to events that happen in chat. 110 | 111 | ``` 112 | func hiMain(b *lazlo.Broker){ 113 | b.Say("Hi!") 114 | cb := b.MessageCallback(`(?i)foo`, true) 115 | for { 116 | pm := <- cb.Chan // block waiting for someone to say "bot foo" 117 | pm.Event.Reply("bar") 118 | } // loop forever (don't return) 119 | } 120 | ``` 121 | 122 | ## Multi-pattern regex 123 | Now we have a fully functional Lazlo Go Module, that will say *Hi!* when Lazlo 124 | first starts up, and will respond with *bar* whenever a user says * 125 | foo*. Lets complicate it a bit with a logical OR in our regex: 126 | 127 | ``` 128 | func fooOrBar(str string) string{ 129 | if str == foo{ 130 | return "bar" 131 | }else{ 132 | return "biz" 133 | } 134 | } 135 | 136 | func hiMain(b *lazlo.Broker){ 137 | b.Say("Hi!") 138 | cb := b.MessageCallback(`(?i)(foo|bar)`, true) 139 | for { 140 | pm := <- cb.Chan 141 | response:=fooOrBar(pm.Match[1]) 142 | pm.Event.Reply(response) 143 | } 144 | } 145 | ``` 146 | 147 | Now we're using the *PatternMatch.Match* slice to detect whether the user 148 | actually said *foo* or *bar*, and responding appropriately (note: our fooOrBar 149 | function will eventually incorrectly answer *biz* because it doesn't handle 150 | case insensitivity properly). 151 | 152 | Alternatively, we could have implemented these options as two separate callbacks: 153 | 154 | ``` 155 | func hiMain(b *lazlo.Broker){ 156 | b.Say("Hi!") 157 | fooCB := b.MessageCallback(`(?i)foo`, true) 158 | barCB := b.MessageCallback(`(?i)bar`, true) 159 | for { 160 | select{ 161 | case pm := <- cbFoo.Chan: 162 | pm.Event.Reply("bar") 163 | case pm := <- cbBar.Chan: 164 | pm.Event.Reply("biz") 165 | } 166 | } 167 | } 168 | ``` 169 | Now we have one callback each for foo and bar, and we're using a select wrapped 170 | in a for loop to choose between them. That's more often the sort of pattern 171 | you'll use in real life, and it's also pretty nifty. Note we don't have to put 172 | this for-loop in series. We could certainly spin off a go-routine to select on 173 | incoming events while we go and do something else. 174 | 175 | ## 5 types of callback 176 | Lazlo has four other callback types that make things even more interesting. 177 | 178 | ### EventCallbacks 179 | You can use *EventCallback* to get raw events from the Slack RTM API as they 180 | happen. A list of the event types can be found 181 | [here](https://api.slack.com/events). Simply supply a key/value pair to the 182 | callback Function like so: 183 | 184 | ``` 185 | cb := b.EventCallback("type","group_open") 186 | ``` 187 | and any events whose json regexp matches your key and value will be spit out of 188 | cb.Chan as *map[string]interface{}". (pro tip: Check the lib/slacktypes.go file for 189 | native Go implementations of many of the common slack json structures.) 190 | 191 | ### TimerCallback 192 | If you want to periodically check a database, or call out to an API, you can 193 | ask the broker for a *TimerCallback*. You specify a cron-syntax schedule like 194 | so: 195 | 196 | ``` 197 | cb := b.TimerCallback("*/20 * * * * * *`) //every 20 seconds 198 | ``` 199 | And cb.Chan will spit out a [*time.Time*](http://golang.org/pkg/time/#Time) at 200 | the time of the next occurance specified by your schedule. If your schedule is 201 | reoccuring, like the one in the example above, Lazlo will reschedule it for 202 | you. Pro-tip: You can get a time.Time that represents the next occurance for your 203 | *TimerCallback* by reading *cb.Next*. 204 | 205 | Check out modules/rtmping.go for an example of *TimerCallback* in the wild 206 | 207 | ###LinkCallback 208 | Lazlo runs a built-in HTTP server, which is necessary to run on certain PaaS 209 | providers like Heroku, but otherwise isn't really required or functional. 210 | *LinkCallback*'s change that, allowing you to register new API paths that link 211 | back to Lazlo's built-in HTTP server. This enables some interesting out-of-band 212 | signaling patterns. 213 | 214 | We can, for example register a few links, and present them to our users in the 215 | form of a multiple-choice question. 216 | 217 | ![](screenshots/link_choice.png) 218 | 219 | Then when the user clicks on one link or the other, a corrisponding HTTP GET 220 | request is sent from the users client to Lazlo's HTTP server, which in turn 221 | spits it out the corresponding callback channel (*cb.Chan*) as an 222 | [*http.Response*](http://golang.org/pkg/net/http/#Response) value . My plugin 223 | can then ingest the user's decision, de-register the callbacks, and respond to 224 | the user via the chat-channel. 225 | 226 | ![](screenshots/link_choice1.png) 227 | 228 | Check out modules/linktest.go for an examplel of *LinkCallback* in the wild. 229 | 230 | ### stuff that works fine that still needs to be documented here 231 | * getting slack meta-info 232 | * in-memory and redis-backed Persistence (lazlo brain) 233 | * our interface for making web-api calls into slack 234 | -------------------------------------------------------------------------------- /docs/questioncb.md: -------------------------------------------------------------------------------- 1 | # The question callback 2 | 3 | Imagine for a moment that you want to ask a user a question in chat. The 4 | problem is, there's no way for the bot to discern between a user-response, and 5 | users just talking. We can DM the user, and assume that the next thing they say 6 | is the reply. In practice this works pretty well, but think about what you need 7 | to do programatically to make that happen.. you have to: 8 | 9 | 1. Grab a DM channel ID with the user 10 | 2. Send the user a question 11 | 3. Setup an event-callback to listen on the DM channel for the user's response 12 | 4. Parse the user's response out of the callback 13 | 14 | The question callback does all this for you. You provide a UserID and the question (both in the form of strings), like this: 15 | 16 | ``` 17 | cb := broker.QuestionCallback(user.ID, `why is the sky blue?`) 18 | ``` 19 | 20 | 21 | You'll get back an object of type lazlo.QuestionCallback, which looks like this: 22 | 23 | ``` 24 | type QuestionCallback struct { 25 | ID string 26 | User string 27 | DMChan string 28 | Question string 29 | Answer chan string 30 | } 31 | ``` 32 | 33 | *ID* uniquely identifies this callback with the broker. Currently, because of 34 | how questions work internally, you can't deregister them using their ID (well 35 | you can, but the question will live on, in the serialization queue (more on 36 | that below). I'm working on this sorry. 37 | 38 | *User* is the User ID you provided when you asked for the callback 39 | 40 | *DMChan* is the ID of the DM channel lazlo setup with the user. 41 | 42 | *Question* is the string you provided as the question. 43 | 44 | *Answer* is a channel that you can listen on for the user's response. 45 | 46 | ## Serialization 47 | Because the question callback is available to any plugin that wants to use it, 48 | and more than one plugin might decide to ask the same user a question at 49 | roughly the same time, the Question-Callback subsystem works as a serializer 50 | service; it automatically serializes questions in a first-in, first-out manner, 51 | making sure that each question only gets asked once, and a user only has one 52 | question to answer at a time. 53 | 54 | A caveat of this behavior is that it breaks the broker.Deregister() function 55 | (because questions are tracked in a separate serialization queue internally. 56 | This might be a problem if you want to time-out a question, if, for example a 57 | user goes on vacation or otherwise just isn't around to answer. I'm still 58 | thinking about the cleanest way to implement question cancelations. Feel free 59 | to throw me an issue if you really need it meow. 60 | 61 | ## Ask and you shall receive 62 | Question callbacks are actually pretty neat to work with. You can, for example implement decision tree type stuff: 63 | 64 | ``` 65 | q1 := broker.QuestionCallback(user.ID,`WHAT is your name?` 66 | ans := <- q1.Answer 67 | if ans == user.Name { 68 | q2:= broker.QuestionCallback(user.ID,`WHAT is your quest?`) 69 | ans = <- q2.Answer 70 | if ans == `I seek the grail`{ 71 | q3:= broker.QuestionCallback(user.ID, randQuestion) 72 | ans = <- q3.Answer 73 | if checkAnswer(ans){ 74 | broker.Say(`You may pass`) 75 | return 0 76 | } 77 | } 78 | } 79 | killUserWithLightening(user.ID) 80 | 81 | ``` 82 | 83 | Or you can rely on the nature of the serialization service to front-load all of 84 | your questions: 85 | 86 | ``` 87 | q1:=broker.QuestionCallback(user.ID,`WHAT is your name?`) 88 | q2:=broker.QuestionCallback(user.ID,`WHAT is your quest?`) 89 | q3:=broker.QuestionCallback(user.ID,`WHAT is the airspeed of a swallow?`) 90 | 91 | name:=<-q1.Answer 92 | quest:=<-q2.Answer 93 | airSpeedOfASwallow:=<-q3.Answer 94 | ``` 95 | 96 | ## Beware the limit 97 | The serialization queue is itself a buffered channel with a hard-coded limit of 98 | 100. In other words, there can only be 100 open questions at a time in the 99 | queue. In the future I might add an environment variable for this, but it's pretty easy to change in broker.go (hint: grep for "LIMIT ALERT"). 100 | 101 | ## In the Future 102 | Currently there's no way to de-register a question callback, which is a problem 103 | if, for example you ask a question from someone who went on vacation for 104 | example, or you want to timeout a question for any other reason. So yeah, I 105 | need to fix that. 106 | -------------------------------------------------------------------------------- /docs/screenshots/add_bot_integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djosephsen/lazlo/634bdfdf7b231dc1d9cce2bb4687a6965b056ee1/docs/screenshots/add_bot_integration.png -------------------------------------------------------------------------------- /docs/screenshots/lazlo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djosephsen/lazlo/634bdfdf7b231dc1d9cce2bb4687a6965b056ee1/docs/screenshots/lazlo.png -------------------------------------------------------------------------------- /docs/screenshots/link_choice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djosephsen/lazlo/634bdfdf7b231dc1d9cce2bb4687a6965b056ee1/docs/screenshots/link_choice.png -------------------------------------------------------------------------------- /docs/screenshots/link_choice1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djosephsen/lazlo/634bdfdf7b231dc1d9cce2bb4687a6965b056ee1/docs/screenshots/link_choice1.png -------------------------------------------------------------------------------- /docs/screenshots/ping_lazlo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djosephsen/lazlo/634bdfdf7b231dc1d9cce2bb4687a6965b056ee1/docs/screenshots/ping_lazlo.png -------------------------------------------------------------------------------- /docs/timercb.md: -------------------------------------------------------------------------------- 1 | # The Timer Callback 2 | 3 | Timer Callbacks let you arrange for Lazlo to wake your module up every so 4 | often. There are many different reasons you might want to implement a timer. I 5 | often use them to periodically query an external service like an RSS feed. They also make it easy to create bot's that appear to "randomly" say things in channel. 6 | 7 | ``` 8 | cb := broker.TimerCallback(`0 */1 * * * * *`) 9 | ``` 10 | 11 | The formal definition is [here](https://github.com/djosephsen/lazlo/blob/master/lib/callbacks.go#L147) in callbacks.go 12 | 13 | As you can see, *TimerCallback()* is a method on the lazlo.Broker type. 14 | Whenever you write a lazlo module, you'll always be passed a pointer to the 15 | broker ([more about that here](plugins.md)) 16 | 17 | *TimerCallback()* requires one argument: the schedule, which is a string that 18 | specifies a cron-syntax schedule. 19 | 20 | Lazlo will give you back an object of type lazlo.TimerCallback. It looks like 21 | this: 22 | 23 | ``` 24 | type TimerCallback struct { 25 | ID string 26 | Schedule string 27 | State string 28 | Next time.Time 29 | Chan chan time.Time 30 | } 31 | ``` 32 | 33 | *ID* uniquely identifies this callback with the broker. It's set automatically 34 | when you register the module and you can use it to deregister the module later 35 | if you want. 36 | 37 | *Schedule* is the cron schedule you specified 38 | 39 | *State* is a human-readable string that describes the current state of the 40 | timer (it will have an error if something went wrong with the schedule you specified) 41 | 42 | *Next* is a [time.Time]() value that specifies the next time this timer will 43 | fire. 44 | 45 | *Chan* is a Channel of type [time.Time](). When the "alarm" goes off so to 46 | speak, Lazlo notifies you using this channel. 47 | 48 | ## Waiting for something to happen 49 | A common pattern is to block on the callback's Chan attribute, waiting for the 50 | timer to fire. 51 | 52 | ``` 53 | alarm := broker.TimerCallback(`0 0 14 * * * *`) //wake me up at 2pm every day 54 | for{ 55 | alarm := <-timer.Chan 56 | broker.Say(`welp it's 2pm, time to update the DB`) 57 | update_db_from_RSS_feed() 58 | ``` 59 | 60 | You can combine timers with other callbacks to achieve more advanced patterns, 61 | like verifying dangerous chatops commands 62 | 63 | 64 | ``` 65 | shieldsUp := broker.MessageCallback(`(?i)shields up$`, true) 66 | 67 | for{ 68 | // block waiting for a shields up command 69 | scb := <-shieldsUp.Chan 70 | 71 | // register new verify and timeout callbacks 72 | scb.Reply(`Verify shields up by pasting this verification code: 234567`) 73 | verify := broker.MessageCallback(`234567`) 74 | timeout := broker.TimerCallback(`30 * * * * * *`) 75 | 76 | // Give the user 30 seconds to verify it wants the shields up 77 | select{ 78 | case v := <- verify.Chan: 79 | go putTheShieldsUP() 80 | case v := <-timeout.Chan: 81 | go nevermind() 82 | } 83 | broker.DeRegister(verify) 84 | broker.DeRegister(timeout) 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /importfix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #attempt to fix broken Go import paths as a consequence of forking the repo 3 | 4 | #sanity check 5 | [ "${GOPATH}" ] || fail 'Sorry you need to set GOPATH' 6 | 7 | #fix sed for mactards (like me) 8 | if [ $(uname) == 'darwin' ] 9 | then 10 | s='sed -E' 11 | else 12 | s='sed' 13 | fi 14 | 15 | # setup some functions 16 | function fail { 17 | echo "$@" 18 | exit 42 19 | } 20 | 21 | function pathEscape { 22 | echo "${@}" | sed -e 's/\//\\\//g' 23 | } 24 | 25 | function ZOMGDOIT { 26 | # cross your fingers 27 | for FILE in ${FLIST} 28 | do 29 | cat ${FILE} | sed -e "s/$(pathEscape ${OGPATH})/$(pathEscape ${OUR_PATH})/"> ${TMP} && mv ${TMP} ${FILE} 30 | done 31 | rm -f ${TMP} 32 | } 33 | 34 | function dryRun { 35 | echo dryRun baby 36 | for FILE in ${FLIST} 37 | do 38 | LINES=$(grep "${OGPATH}" ${FILE}) 39 | if [ -n "${LINES}" ] 40 | then 41 | echo "In file: ${FILE}:" 42 | echo "${LINES}"| while read LINE 43 | do 44 | echo "I'd replace: ${LINE}" 45 | echo "with: "$(echo ${LINE} | sed -e "s/$(pathEscape ${OGPATH})/$(pathEscape ${OUR_PATH})/") 46 | done 47 | echo 48 | fi 49 | done 50 | } 51 | 52 | #ok lets see here.. 53 | TMP='/tmp/SFTEMPFILE' 54 | PACKAGE=$(basename $(pwd)) 55 | FLIST=$(find . -name '*.go') 56 | MINUS_THIS="${GOPATH}/src/" 57 | OGPATH=$(echo ${GOPATH}/src/github.com/djosephsen/lazlo| sed "s/$(pathEscape ${MINUS_THIS})//") 58 | OUR_PATH=$(pwd | sed -e "s/$(pathEscape ${MINUS_THIS})//") 59 | 60 | if [ -z ${1} ] 61 | then 62 | ZOMGDOIT 63 | else 64 | dryRun 65 | fi 66 | -------------------------------------------------------------------------------- /lib/api.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/fatih/structs" 7 | "github.com/gorilla/websocket" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | //ApiRequest contains everything we need to make.. well an api request 15 | type ApiRequest struct { 16 | URL string 17 | Values url.Values 18 | Broker *Broker 19 | } 20 | 21 | //MakeAPIReq takes an ApiRequest, adds auth if necessary and POSTs it to 22 | //the slack web-api. 23 | func MakeAPIReq(req ApiRequest) (*ApiResponse, error) { 24 | if req.Values.Get(`token`) == `` { 25 | req.Values.Set(`token`, req.Broker.Config.Token) 26 | } 27 | if req.Values.Get(`as_user`) == `` { 28 | req.Values.Set(`as_user`, req.Broker.Config.Name) 29 | } 30 | 31 | resp := new(ApiResponse) 32 | reply, err := http.PostForm(req.URL, req.Values) 33 | if err != nil { 34 | return resp, err 35 | } 36 | defer reply.Body.Close() 37 | 38 | dec := json.NewDecoder(reply.Body) 39 | err = dec.Decode(resp) 40 | if err != nil { 41 | return resp, fmt.Errorf("Couldn't decode json. ERR: %v", err) 42 | } 43 | return resp, nil 44 | } 45 | 46 | // getASocket calls MakeApiRequest() to get a websocket for the slack RTM 47 | // interface. 48 | func (b *Broker) getASocket() (*websocket.Conn, *ApiResponse, error) { 49 | var req = ApiRequest{ 50 | URL: `https://slack.com/api/rtm.start`, 51 | Values: make(url.Values), 52 | Broker: b, 53 | } 54 | authResp, err := MakeAPIReq(req) 55 | if err != nil { 56 | return nil, nil, err 57 | } 58 | 59 | if authResp.URL == "" { 60 | return nil, nil, fmt.Errorf("Auth failure") 61 | } 62 | wsURL := strings.Split(authResp.URL, "/") 63 | wsURL[2] = wsURL[2] + ":443" 64 | authResp.URL = strings.Join(wsURL, "/") 65 | Logger.Debug(`Team Wesocket URL: `, authResp.URL) 66 | 67 | var Dialer websocket.Dialer 68 | header := make(http.Header) 69 | header.Add("Origin", "http://localhost/") 70 | 71 | ws, _, err := Dialer.Dial(authResp.URL, header) 72 | if err != nil { 73 | return nil, nil, fmt.Errorf("no dice dialing that websocket: %v", err) 74 | } 75 | 76 | //yay we're websocketing 77 | return ws, authResp, nil 78 | } 79 | 80 | // GetUserName is a convience function to return a user's Name field 81 | // given its ID. 82 | func (meta *ApiResponse) GetUserName(id string) string { 83 | for _, user := range meta.Users { 84 | if user.ID == id { 85 | return user.Name 86 | } 87 | } 88 | return `` 89 | } 90 | 91 | // GetUser is a convienence function to return a pointer to a user object 92 | // given its ID. 93 | func (meta *ApiResponse) GetUser(id string) *User { 94 | for _, user := range meta.Users { 95 | if user.ID == id { 96 | return &user 97 | } 98 | } 99 | return nil 100 | } 101 | 102 | // GetUserByName is a convience function to return a pointer to a user 103 | // object given its Name 104 | func (meta *ApiResponse) GetUserByName(name string) *User { 105 | for _, user := range meta.Users { 106 | if user.Name == name { 107 | return &user 108 | } 109 | } 110 | return nil 111 | } 112 | 113 | // GetChannel is a convienence function to fetch a pointer to a channel 114 | // object given its ID 115 | func (meta *ApiResponse) GetChannel(id string) *Channel { 116 | for _, channel := range meta.Channels { 117 | if channel.ID == id { 118 | return &channel 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | // GetChannel is a convienence function to fetch a pointer to a channel 125 | // object given its Name 126 | func (meta *ApiResponse) GetChannelByName(name string) *Channel { 127 | for _, channel := range meta.Channels { 128 | if channel.Name == name { 129 | return &channel 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | // Reply is a convienence function to REPLY to a given event object 136 | func (event *Event) Reply(s string) chan map[string]interface{} { 137 | replyText := fmt.Sprintf(`%s: %s`, event.Broker.SlackMeta.GetUserName(event.User), s) 138 | return event.Respond(replyText) 139 | } 140 | 141 | // Respond is a convienence function to RESPOND to a given event object 142 | func (event *Event) Respond(s string) chan map[string]interface{} { 143 | return event.Broker.Send(&Event{ 144 | Type: event.Type, 145 | Channel: event.Channel, 146 | Text: s, 147 | }) 148 | } 149 | 150 | // RespondAttachments is a function to RESPOND WITH ATTACHMENTS to a given event object 151 | func (event *Event) RespondAttachments(a []Attachment) chan map[string]interface{} { 152 | return event.Broker.Send(&Event{ 153 | Type: event.Type, 154 | Channel: event.Channel, 155 | Text: "", 156 | Attachments: a, 157 | }) 158 | } 159 | 160 | // Get a Direct-Message Channel to the user from a given event 161 | func (event *Event) GetDM(s string) string { 162 | return event.Broker.GetDM(event.User) 163 | } 164 | 165 | // this is a confusing hack that I'm using because slack's RTM websocket 166 | // doesn't seem to support their own markup syntax. So anything that looks 167 | // like it has markup in it is sent into this function by the write thread 168 | // instead of into the websocket where it belongs. 169 | func apiPostMessage(e Event) { 170 | Logger.Debug(`Posting through api`) 171 | var req = ApiRequest{ 172 | URL: `https://slack.com/api/chat.postMessage`, 173 | Values: make(url.Values), 174 | Broker: e.Broker, 175 | } 176 | req.Values.Set(`channel`, e.Channel) 177 | req.Values.Set(`text`, e.Text) 178 | if e.Attachments != nil { 179 | aJson, _ := json.Marshal(e.Attachments) 180 | req.Values.Set(`attachments`, string(aJson)) 181 | } 182 | req.Values.Set(`id`, strconv.Itoa(int(e.ID))) 183 | req.Values.Set(`as_user`, e.Broker.Config.Name) 184 | req.Values.Set(`pretty`, `1`) 185 | authResp, _ := MakeAPIReq(req) 186 | s := structs.New(authResp) // convert this to a map[string]interface{} why not? hax. 187 | resp := s.Map() 188 | if replyVal, isReply := resp[`reply_to`]; isReply { 189 | if replyVal != nil { 190 | e.Broker.handleApiReply(resp) 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /lib/brain.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "github.com/garyburd/redigo/redis" 6 | "net/url" 7 | ) 8 | 9 | // Top-level exported Store interface for storage backends to implement 10 | type Brain interface { 11 | Open() error 12 | Close() error 13 | Get(string) ([]byte, error) 14 | Set(key string, data []byte) error 15 | Delete(string) error 16 | } 17 | 18 | // NewStore returns an initialized store 19 | func (b *Broker) newBrain() (Brain, error) { 20 | var brain Brain 21 | var err error 22 | if b.Config.RedisURL != `` { 23 | Logger.Debug(`Brain:: setting up a Redis Brain to: `, b.Config.RedisURL) 24 | if brain, err = newRedisBrain(b); err != nil { 25 | return brain, err 26 | } 27 | } else { 28 | Logger.Debug(`Brain:: setting up an in-memory Brain`) 29 | if brain, err = newRAMBrain(b); err != nil { 30 | return brain, err 31 | } 32 | } 33 | return brain, nil 34 | } 35 | 36 | //rambrain storage implementation 37 | type ramBrain struct { 38 | data map[string][]byte 39 | } 40 | 41 | // New returns a new initialized ramBrain that implements Brain 42 | func newRAMBrain(b *Broker) (Brain, error) { 43 | rb := &ramBrain{ 44 | data: map[string][]byte{}, 45 | } 46 | return rb, nil 47 | } 48 | 49 | func (rb *ramBrain) Open() error { 50 | return nil 51 | } 52 | 53 | func (rb *ramBrain) Close() error { 54 | return nil 55 | } 56 | 57 | func (rb *ramBrain) Get(key string) ([]byte, error) { 58 | if val, ok := rb.data[key]; ok { 59 | return val, nil 60 | } 61 | 62 | return nil, fmt.Errorf("key %s was not found", key) 63 | } 64 | 65 | func (rb *ramBrain) Set(key string, data []byte) error { 66 | rb.data[key] = data 67 | return nil 68 | } 69 | 70 | func (rb *ramBrain) Delete(key string) error { 71 | if _, ok := rb.data[key]; !ok { 72 | return fmt.Errorf("key %s was not found", key) 73 | } 74 | delete(rb.data, key) 75 | return nil 76 | } 77 | 78 | //redisbrain backend storage implementation 79 | type redisBrain struct { 80 | url string 81 | pw string 82 | nameSpace string 83 | client redis.Conn 84 | } 85 | 86 | // New returns an new initialized store 87 | func newRedisBrain(b *Broker) (Brain, error) { 88 | s := &redisBrain{ 89 | url: b.Config.RedisURL, 90 | nameSpace: b.Config.Name, 91 | } 92 | if b.Config.RedisPW != `` { 93 | s.pw = b.Config.RedisPW 94 | } 95 | return s, nil 96 | } 97 | 98 | func (rb *redisBrain) Open() error { 99 | uri, err := url.Parse(rb.url) 100 | if err != nil { 101 | Logger.Error(err) 102 | } 103 | 104 | conn, err := redis.Dial("tcp", uri.Host) 105 | if err != nil { 106 | Logger.Error(err) 107 | return err 108 | } 109 | 110 | rb.client = conn 111 | 112 | if rb.pw != `` { 113 | if _, err := rb.client.Do("AUTH", rb.pw); err != nil { 114 | return err 115 | } 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (rb *redisBrain) Close() error { 122 | if err := rb.client.Close(); err != nil { 123 | Logger.Error(err) 124 | return err 125 | } 126 | return nil 127 | } 128 | 129 | func (rb *redisBrain) Get(key string) ([]byte, error) { 130 | args := rb.namespace(key) 131 | data, err := rb.client.Do("GET", args) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | if data == nil { 137 | return []byte{}, fmt.Errorf("%s not found", key) 138 | } 139 | return data.([]byte), nil 140 | } 141 | 142 | func (rb *redisBrain) Set(key string, data []byte) error { 143 | if _, err := rb.client.Do("SET", rb.namespace(key), data); err != nil { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | func (rb *redisBrain) Delete(key string) error { 150 | res, err := rb.client.Do("DEL", rb.namespace(key)) 151 | if err != nil { 152 | return err 153 | } 154 | if res.(int64) < 1 { 155 | return fmt.Errorf("%s not found", key) 156 | } 157 | return nil 158 | } 159 | 160 | func (rb *redisBrain) namespace(key string) string { 161 | return fmt.Sprintf("%s:%s", rb.nameSpace, key) 162 | } 163 | -------------------------------------------------------------------------------- /lib/broker.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "os" 9 | "regexp" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/ccding/go-logging/logging" 15 | "github.com/gorilla/websocket" 16 | ) 17 | 18 | // Logger is a global reference to our logging object 19 | var Logger = newLogger() 20 | 21 | // These contstats define the four types of callbacks that lazlo can hand you 22 | const M = "messages" 23 | const E = "events" 24 | const T = "timers" 25 | const L = "links" 26 | const Q = "questions" 27 | 28 | type cbIndex struct { 29 | Index map[string]interface{} // Index[id]=pointer 30 | *sync.Mutex 31 | } 32 | 33 | // Broker is the all-knowing repository of references 34 | type Broker struct { 35 | SlackMeta *ApiResponse 36 | Config *Config 37 | Socket *websocket.Conn 38 | Modules map[string]*Module 39 | Brain Brain 40 | ApiResponses map[int32]chan map[string]interface{} 41 | callbacks map[string]cbIndex //callbacks[type]Index 42 | ReadFilters []*ReadFilter 43 | WriteFilters []*WriteFilter 44 | MID int32 45 | WriteThread *WriteThread 46 | QuestionThread *QuestionThread 47 | SigChan chan os.Signal 48 | SyncChan chan bool 49 | ThreadCount int32 50 | } 51 | 52 | // The Module type represents a user-defined plug-in. Build one of these 53 | // and add it to loadModules.go for Lazlo to run your thingy on startup 54 | type Module struct { 55 | Name string 56 | Usage string 57 | Run func(*Broker) 58 | } 59 | 60 | // The WriteThread serielizes and sends messages to the slack RTM interface 61 | type WriteThread struct { 62 | broker *Broker 63 | Chan chan Event 64 | SyncChan chan bool 65 | } 66 | 67 | // The QuestionThread serializes questions and sends questions to users 68 | type QuestionThread struct { 69 | broker *Broker 70 | userdex map[string]QuestionQueue 71 | } 72 | 73 | // ReadFilter is a yet-to-be-implemented hook run on all inbound 74 | // events from slack before the broker gets a hold of them 75 | type ReadFilter struct { 76 | Name string 77 | Usage string 78 | Run func(thingy map[string]interface{}) map[string]interface{} 79 | } 80 | 81 | // WriteFilter is a yet-to-be-implemented hook run on all outbound 82 | // events from slack before the broker gets a hold of them 83 | type WriteFilter struct { 84 | Name string 85 | Usage string 86 | Run func(e *Event) 87 | } 88 | 89 | // NewBroker instantiates a new broker 90 | func NewBroker() (*Broker, error) { 91 | 92 | broker := &Broker{ 93 | MID: 0, 94 | Config: newConfig(), 95 | Modules: make(map[string]*Module), 96 | ApiResponses: make(map[int32]chan map[string]interface{}), 97 | WriteThread: &WriteThread{ 98 | Chan: make(chan Event), 99 | SyncChan: make(chan bool), 100 | }, 101 | QuestionThread: &QuestionThread{ 102 | userdex: make(map[string]QuestionQueue), 103 | }, 104 | SigChan: make(chan os.Signal), 105 | SyncChan: make(chan bool), 106 | } 107 | //correctly set the log level 108 | Logger.SetLevel(logging.GetLevelValue(strings.ToUpper(broker.Config.LogLevel))) 109 | broker.callbacks = make(map[string]cbIndex) 110 | for _, a := range []string{M, E, T, L, Q} { 111 | broker.callbacks[a] = cbIndex{ 112 | Index: make(map[string]interface{}), 113 | Mutex: new(sync.Mutex), 114 | } 115 | } 116 | broker.WriteThread.broker = broker 117 | broker.QuestionThread.broker = broker 118 | 119 | //connect to slack and establish an RTM websocket 120 | socket, meta, err := broker.getASocket() 121 | if err != nil { 122 | return nil, err 123 | } 124 | broker.Socket = socket 125 | broker.SlackMeta = meta 126 | 127 | broker.Brain, err = broker.newBrain() 128 | if err != nil { 129 | return nil, err 130 | } 131 | // broker.Brain = brain 132 | if err = broker.Brain.Open(); err != nil { 133 | Logger.Error(`couldn't open mah brain! `, err) 134 | return broker, err 135 | } 136 | return broker, nil 137 | } 138 | 139 | // Stop gracefully stops lazlo 140 | func (broker *Broker) Stop() { 141 | // make sure the write thread finishes before we stop 142 | broker.WriteThread.SyncChan <- true 143 | } 144 | 145 | // Broker.Start() starts the broker 146 | func (broker *Broker) Start() { 147 | go broker.StartHttp() 148 | go broker.WriteThread.Start() 149 | go broker.QuestionThread.Start() 150 | Logger.Debug(`Broker:: entering read-loop`) 151 | for { 152 | thingy := make(map[string]interface{}) 153 | if err := broker.Socket.ReadJSON(&thingy); err != nil { 154 | Logger.Fatal(`Loop:: Error `, err) 155 | os.Exit(1) 156 | } 157 | go broker.This(thingy) 158 | } 159 | } 160 | 161 | // StartModules launches each user-provided plugin registered in loadMOdules.go 162 | func (b *Broker) StartModules() { 163 | for _, module := range b.Modules { 164 | go func(m Module) { 165 | m.Run(b) 166 | }(*module) 167 | } 168 | } 169 | 170 | // WriteThread.Start starts the writethread 171 | func (w *WriteThread) Start() { 172 | Logger.Debug(`Write-Thread Started`) 173 | stop := false 174 | for !stop { 175 | select { 176 | case e := <-w.Chan: 177 | Logger.Debug(`WriteThread:: Outbound `, e.Type, ` channel: `, e.Channel, `. text: `, e.Text) 178 | ejson := stupidUTFHack(e) 179 | if len(ejson) >= 16000 { 180 | e = Event{ 181 | ID: e.ID, 182 | Type: e.Type, 183 | Channel: e.Channel, 184 | Text: fmt.Sprintf("ERROR! Response too large. %v Bytes!", len(ejson)), 185 | } 186 | ejson = stupidUTFHack(e) 187 | } 188 | if matches, _ := regexp.MatchString(`<[hH#@].+>`, string(ejson)); matches || e.Attachments != nil { 189 | Logger.Debug(`message formatting detected; sending via api`) 190 | e.Broker = w.broker 191 | apiPostMessage(e) 192 | } else { 193 | if err := w.broker.Socket.WriteMessage(1, ejson); err != nil { 194 | Logger.Error(`cannot send message: `, err) 195 | } 196 | } 197 | Logger.Debug(string(ejson)) 198 | time.Sleep(time.Second) 199 | case stop = <-w.SyncChan: 200 | stop = true 201 | } 202 | } 203 | //signal main that we're done 204 | w.broker.SyncChan <- true 205 | } 206 | 207 | // QuestionThread.Start() starts the question-serializer service 208 | func (qt *QuestionThread) Start() { 209 | for { 210 | // loop if there are no question callbacks 211 | if qt.broker.callbacks[Q].Index == nil { 212 | time.Sleep(time.Second) 213 | continue 214 | } 215 | // create & start new queues if necessary and send the questions 216 | for _, qi := range qt.broker.callbacks[Q].Index { 217 | question := qi.(*QuestionCallback) 218 | user := question.User 219 | if question.asked { 220 | qt.broker.DeRegisterCallback(question) 221 | continue 222 | } 223 | if queue, ok := qt.userdex[user]; ok { 224 | queue.in <- question 225 | question.asked = true 226 | } else { 227 | qt.userdex[user] = QuestionQueue{ 228 | in: make(chan *QuestionCallback, 100), //LIMIT ALERT! 229 | } 230 | newQueue := qt.userdex[user] 231 | go newQueue.Launch(qt.broker) 232 | newQueue.in <- question 233 | } 234 | question.asked = true 235 | } 236 | 237 | time.Sleep(time.Second) 238 | } 239 | } 240 | 241 | //QuestionQueue.Launch is a worker that serializes questions to one person 242 | func (qq *QuestionQueue) Launch(b *Broker) { 243 | for { 244 | question := <-qq.in //block wating for the next QuestionCallback 245 | if question.DMChan == "" { 246 | question.DMChan = b.GetDM(question.User) 247 | } 248 | b.Say(question.Question, question.DMChan) 249 | cb := b.MessageCallback(`.*`, false, question.DMChan) 250 | reply := <-cb.Chan // block waiting for a response from the user 251 | question.Answer <- reply.Match[0] 252 | b.DeRegisterCallback(cb) 253 | } 254 | } 255 | 256 | //This stupid hack un-does the utf-escaping performed by json.Marshal() 257 | //because although Slack correctly parses utf, it doesn't recognize 258 | //utf-escaped markup like 259 | // UPDATE: I can remove this Once I re-figure-out out how the hell it works 260 | func stupidUTFHack(thingy interface{}) []byte { 261 | jThingy, _ := json.Marshal(thingy) 262 | jThingy = bytes.Replace(jThingy, []byte("\\u003c"), []byte("<"), -1) 263 | jThingy = bytes.Replace(jThingy, []byte("\\u003e"), []byte(">"), -1) 264 | jThingy = bytes.Replace(jThingy, []byte("\\u0026"), []byte("&"), -1) 265 | return jThingy 266 | } 267 | 268 | //NextMID() ensures our outbound messages have a unique ID number 269 | // (a requirement of the slack rtm api) 270 | func (b *Broker) NextMID() int32 { 271 | //probably need to make this thread-safe (for now only the write thread uses it) 272 | b.MID += 1 273 | Logger.Debug(`incrementing MID to `, b.MID) 274 | return b.MID 275 | } 276 | 277 | //broker.This() takes an inbound thingy of unknown type and brokers it to wherever 278 | // it needs to go 279 | func (b *Broker) This(thingy map[string]interface{}) { 280 | if b.Modules == nil { 281 | Logger.Debug(`Broker:: Got a `, thingy[`type`], ` , but no modules are loaded!`) 282 | return 283 | } 284 | //run the pre-handeler filters 285 | if b.ReadFilters != nil { 286 | for _, filter := range b.ReadFilters { //run the read filters 287 | thingy = filter.Run(thingy) 288 | } 289 | } 290 | // stop here if a prefilter delted our thingy 291 | if len(thingy) == 0 { 292 | return 293 | } 294 | 295 | Logger.Debug(`broker:: got a `, thingy[`type`]) 296 | // if it's an api response send it to whomever is listening for it 297 | if replyVal, isReply := thingy[`reply_to`]; isReply { 298 | if replyVal != nil { // sometimes the api returns: "reply_to":null 299 | b.handleApiReply(thingy) 300 | } 301 | } 302 | 303 | typeOfThingy := thingy[`type`] 304 | switch typeOfThingy { 305 | case nil: 306 | return 307 | case `message`: 308 | b.handleMessage(thingy) 309 | default: 310 | b.handleEvent(thingy) 311 | } 312 | } 313 | 314 | // broker.Register() registers user-provided plug-ins 315 | func (b *Broker) Register(things ...interface{}) { 316 | for _, thing := range things { 317 | switch t := thing.(type) { 318 | case *Module: 319 | m := thing.(*Module) 320 | Logger.Debug(`registered Module: `, m.Name) 321 | b.Modules[m.Name] = m 322 | case Module: 323 | m := thing.(Module) 324 | Logger.Debug(`registered Module: `, m.Name) 325 | b.Modules[m.Name] = &m 326 | case *ReadFilter: 327 | r := thing.(*ReadFilter) 328 | Logger.Debug(`registered Read Filter: `, r.Name) 329 | b.ReadFilters = append(b.ReadFilters, r) 330 | case *WriteFilter: 331 | w := thing.(*WriteFilter) 332 | Logger.Debug(`registered Write Filter: `, w.Name) 333 | b.WriteFilters = append(b.WriteFilters, w) 334 | default: 335 | weirdType := fmt.Sprintf(`%T`, t) 336 | Logger.Error(`sorry I cant register this handler because I don't know what a `, weirdType, ` is`) 337 | } 338 | } 339 | } 340 | 341 | //broker.handleApiReply() catches API responses and sends them back to the 342 | // requestor if the requestor cares 343 | func (b *Broker) handleApiReply(thingy map[string]interface{}) { 344 | chanID := int32(thingy[`reply_to`].(float64)) 345 | Logger.Debug(`Broker:: caught a reply to: `, chanID) 346 | if callBackChannel, exists := b.ApiResponses[chanID]; exists { 347 | callBackChannel <- thingy 348 | //dont leak channels 349 | Logger.Debug(`deleting callback: `, chanID) 350 | close(callBackChannel) 351 | <-callBackChannel 352 | delete(b.ApiResponses, chanID) 353 | } else { 354 | Logger.Debug(`no such channel: `, chanID) 355 | } 356 | } 357 | 358 | //broker.handleMessage() gets messages from broker.This() and handles them according 359 | // to the user-provided plugins currently loaded. 360 | func (b *Broker) handleMessage(thingy map[string]interface{}) { 361 | if b.callbacks[M].Index == nil { 362 | return 363 | } 364 | message := new(Event) 365 | jthingy, _ := json.Marshal(thingy) 366 | 367 | if err := json.Unmarshal(jthingy, message); err != nil { 368 | Logger.Error("Error in unmarshall", err) 369 | return 370 | } 371 | message.Broker = b 372 | botNamePat := fmt.Sprintf(`^(?:@?%s[:,]?)\s+(?:${1})`, b.Config.Name) 373 | for _, cbInterface := range b.callbacks[M].Index { 374 | callback := cbInterface.(*MessageCallback) 375 | 376 | Logger.Debug(`Broker:: checking callback: `, callback.ID) 377 | if callback.SlackChan != `` { 378 | if callback.SlackChan != message.Channel { 379 | Logger.Debug(`Broker:: dropping message because chan mismatch: `, callback.ID) 380 | continue //skip this message because it doesn't match the cb's channel filter 381 | } else { 382 | Logger.Debug(`Broker:: channel filter match for: `, callback.ID) 383 | } 384 | } 385 | var r *regexp.Regexp 386 | if callback.Respond { 387 | r = regexp.MustCompile(strings.Replace(botNamePat, "${1}", callback.Pattern, 1)) 388 | } else { 389 | r = regexp.MustCompile(callback.Pattern) 390 | } 391 | if r.MatchString(message.Text) { 392 | match := r.FindAllStringSubmatch(message.Text, -1)[0] 393 | Logger.Debug(`Broker:: firing callback: `, callback.ID) 394 | callback.Chan <- PatternMatch{Event: message, Match: match} 395 | } 396 | } 397 | } 398 | 399 | func (b *Broker) handleEvent(thingy map[string]interface{}) { 400 | if b.callbacks[E].Index == nil { 401 | return 402 | } 403 | for _, cbInterface := range b.callbacks[E].Index { 404 | callback := cbInterface.(*EventCallback) 405 | if keyVal, keyExists := thingy[callback.Key]; keyExists && keyVal != nil { 406 | if matches, _ := regexp.MatchString(callback.Val, keyVal.(string)); matches { 407 | Logger.Debug(`Broker:: firing callback: `, callback.ID) 408 | callback.Chan <- thingy 409 | } 410 | } 411 | } 412 | } 413 | 414 | // this is the primary interface to Slack's write socket. Use this to send events. 415 | func (b *Broker) Send(e *Event) chan map[string]interface{} { 416 | e.ID = b.NextMID() 417 | b.ApiResponses[e.ID] = make(chan map[string]interface{}, 1) 418 | Logger.Debug(`created APIResponse: `, e.ID) 419 | b.WriteThread.Chan <- *e 420 | return b.ApiResponses[e.ID] 421 | } 422 | 423 | // Say something in the named channel (or the default channel if none specified) 424 | func (b *Broker) Say(s string, channel ...string) chan map[string]interface{} { 425 | var c string 426 | if channel != nil { 427 | c = channel[0] 428 | } else { 429 | c = b.DefaultChannel() 430 | } 431 | resp := b.Send(&Event{ 432 | Type: `message`, 433 | Channel: c, 434 | Text: s, 435 | }) 436 | return resp 437 | } 438 | 439 | // send a reply to any sort of thingy that contains an ID and Channel attribute 440 | func (b *Broker) Respond(text string, thing *interface{}, isReply bool) chan map[string]interface{} { 441 | var id, channel string 442 | var exists bool 443 | 444 | thingy := *thing 445 | switch thingy.(type) { 446 | case Event: 447 | eThingy := thingy.(Event) 448 | if eThingy.User != `` && eThingy.Channel != `` { 449 | id = eThingy.User 450 | channel = eThingy.Channel 451 | } else { 452 | return nil 453 | } 454 | case map[string]interface{}: 455 | mThingy := thingy.(map[string]interface{}) 456 | if id, exists = mThingy[`id`].(string); !exists || id == `` { 457 | return nil 458 | } 459 | if channel, exists = mThingy[`channel`].(string); !exists || channel == `` { 460 | return nil 461 | } 462 | id = mThingy[`id`].(string) 463 | channel = mThingy[`channel`].(string) 464 | default: 465 | return nil 466 | } 467 | 468 | var replyText string 469 | if isReply { 470 | replyText = fmt.Sprintf(`%s: %s`, b.SlackMeta.GetUserName(id), text) 471 | } else { 472 | replyText = text 473 | } 474 | 475 | return b.Send(&Event{ 476 | Type: `message`, 477 | Channel: channel, 478 | Text: replyText, 479 | }) 480 | } 481 | 482 | //Get a direct message channel ID so we can DM the given user 483 | func (b *Broker) GetDM(ID string) string { 484 | req := ApiRequest{ //use the web api so we don't block waiting for the read thread 485 | URL: `https://slack.com/api/im.open`, 486 | Values: make(url.Values), 487 | Broker: b, 488 | } 489 | req.Values.Set(`user`, ID) 490 | reply, err := MakeAPIReq(req) 491 | if err != nil { 492 | Logger.Error(`error making api request for dm channel: `, err) 493 | return `` 494 | } else { 495 | return reply.Channel.ID 496 | } 497 | } 498 | 499 | //returns the Team's default channel 500 | func (b *Broker) DefaultChannel() string { 501 | for _, c := range b.SlackMeta.Channels { 502 | if c.IsGeneral { 503 | return c.ID 504 | } 505 | } 506 | return b.SlackMeta.Channels[0].ID 507 | } 508 | -------------------------------------------------------------------------------- /lib/callbacks.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/renstrom/shortuuid" 8 | ) 9 | 10 | type MessageCallback struct { 11 | ID string 12 | Pattern string 13 | Respond bool // if true, only respond if the bot is mentioned by name 14 | Chan chan PatternMatch 15 | SlackChan string // if set filter message callbacks to this Slack channel 16 | } 17 | 18 | type PatternMatch struct { 19 | Event *Event 20 | Match []string 21 | } 22 | 23 | type EventCallback struct { 24 | ID string 25 | Key string 26 | Val string 27 | Chan chan map[string]interface{} 28 | } 29 | 30 | type TimerCallback struct { 31 | ID string 32 | Schedule string 33 | State string 34 | Next time.Time 35 | Chan chan time.Time 36 | stop chan bool //send true on this channel to stop the timer 37 | } 38 | 39 | type QuestionCallback struct { 40 | ID string 41 | User string 42 | DMChan string 43 | Question string 44 | Answer chan string 45 | asked bool 46 | } 47 | 48 | type QuestionQueue struct { 49 | in chan *QuestionCallback 50 | } 51 | 52 | func (b *Broker) RegisterCallback(callback interface{}) error { 53 | switch callback.(type) { 54 | case *MessageCallback: 55 | m := callback.(*MessageCallback) 56 | b.callbacks[M].Lock() 57 | defer b.callbacks[M].Unlock() 58 | b.callbacks[M].Index[m.ID] = callback 59 | Logger.Debug("New Callback Registered, id:", m.ID, " *MessageCallback") 60 | case *EventCallback: 61 | e := callback.(*EventCallback) 62 | b.callbacks[E].Lock() 63 | defer b.callbacks[E].Unlock() 64 | b.callbacks[E].Index[e.ID] = callback 65 | Logger.Debug("New Callback Registered, id:", e.ID, " *EventCallback") 66 | case *TimerCallback: 67 | t := callback.(*TimerCallback) 68 | t.Start() 69 | b.callbacks[T].Lock() 70 | defer b.callbacks[T].Unlock() 71 | b.callbacks[T].Index[t.ID] = callback 72 | Logger.Debug("New Callback Registered, id:", t.ID, ":= callback.( *TimerCallback") 73 | case *LinkCallback: 74 | l := callback.(*LinkCallback) 75 | b.callbacks[L].Lock() 76 | defer b.callbacks[L].Unlock() 77 | b.callbacks[L].Index[l.ID] = callback 78 | Logger.Debug("New Callback Registered, id:", l.ID, " *LinkCallback") 79 | case *QuestionCallback: 80 | q := callback.(*QuestionCallback) 81 | b.callbacks[Q].Lock() 82 | defer b.callbacks[Q].Unlock() 83 | b.callbacks[Q].Index[q.ID] = callback 84 | Logger.Debug("New Callback Registered, id:", q.ID, " *QuestionCallback") 85 | default: 86 | err := fmt.Errorf("unknown type in register callback: %T", callback) 87 | Logger.Error(err) 88 | return err 89 | } 90 | return nil 91 | } 92 | 93 | func (b *Broker) DeRegisterCallback(callback interface{}) error { 94 | switch callback.(type) { 95 | case *MessageCallback: 96 | m := callback.(*MessageCallback) 97 | b.callbacks[M].Lock() 98 | defer b.callbacks[M].Unlock() 99 | delete(b.callbacks[M].Index, m.ID) 100 | Logger.Debug("De-Registered callback, id: ", m.ID) 101 | case *EventCallback: 102 | e := callback.(*EventCallback) 103 | b.callbacks[E].Lock() 104 | defer b.callbacks[E].Unlock() 105 | delete(b.callbacks[E].Index, e.ID) 106 | Logger.Debug("De-Registered callback, id: ", e.ID) 107 | case *TimerCallback: 108 | t := callback.(*TimerCallback) 109 | t.Stop() // dont leak timers 110 | b.callbacks[T].Lock() 111 | defer b.callbacks[T].Unlock() 112 | delete(b.callbacks[T].Index, t.ID) 113 | Logger.Debug("De-Registered callback, id: ", t.ID) 114 | case *LinkCallback: 115 | l := callback.(*LinkCallback) 116 | l.Delete() //dont leak httproutes 117 | b.callbacks[L].Lock() 118 | defer b.callbacks[L].Unlock() 119 | delete(b.callbacks[L].Index, l.ID) 120 | Logger.Debug("De-Registered callback, id: ", l.ID) 121 | case *QuestionCallback: 122 | q := callback.(*QuestionCallback) 123 | delete(b.callbacks[Q].Index, q.ID) 124 | Logger.Debug("De-Registered callback, id: ", q.ID) 125 | default: 126 | err := fmt.Errorf("unknown type in de-register callback: %T", callback) 127 | Logger.Error(err) 128 | return err 129 | } 130 | return nil 131 | } 132 | 133 | func (b *Broker) MessageCallback(pattern string, respond bool, channel ...string) *MessageCallback { 134 | callback := &MessageCallback{ 135 | ID: fmt.Sprintf("message:%s", shortuuid.New()), 136 | Pattern: pattern, 137 | Respond: respond, 138 | Chan: make(chan PatternMatch), 139 | } 140 | 141 | if channel != nil { 142 | callback.SlackChan = channel[0] // todo: support an array of channels 143 | } 144 | 145 | if err := b.RegisterCallback(callback); err != nil { 146 | Logger.Debug("error registering callback ", callback.ID, ":: ", err) 147 | return nil 148 | } 149 | return callback 150 | } 151 | 152 | func (b *Broker) EventCallback(key string, val string) *EventCallback { 153 | callback := &EventCallback{ 154 | ID: fmt.Sprintf("event:%s", shortuuid.New()), 155 | Key: key, 156 | Val: val, 157 | Chan: make(chan map[string]interface{}), 158 | } 159 | 160 | if err := b.RegisterCallback(callback); err != nil { 161 | Logger.Debug("error registering callback ", callback.ID, ":: ", err) 162 | return nil 163 | } 164 | return callback 165 | } 166 | 167 | func (b *Broker) TimerCallback(schedule string) *TimerCallback { 168 | callback := &TimerCallback{ 169 | ID: fmt.Sprintf("timer:%s", shortuuid.New()), 170 | Schedule: schedule, 171 | Chan: make(chan time.Time), 172 | } 173 | if err := b.RegisterCallback(callback); err != nil { 174 | Logger.Debug("error registering callback ", callback.ID, ":: ", err) 175 | return nil 176 | } 177 | return callback 178 | } 179 | 180 | func (b *Broker) QuestionCallback(user string, prompt string) *QuestionCallback { 181 | callback := &QuestionCallback{ 182 | ID: fmt.Sprintf("question:%s", shortuuid.New()), 183 | User: user, 184 | Question: prompt, 185 | Answer: make(chan string), 186 | } 187 | if err := b.RegisterCallback(callback); err != nil { 188 | Logger.Debug("error registering callback ", callback.ID, ":: ", err) 189 | return nil 190 | } 191 | return callback 192 | } 193 | 194 | // LinkCallback() def is in httpserver.go because it includes net/http (sorry) 195 | -------------------------------------------------------------------------------- /lib/config.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "github.com/ccding/go-logging/logging" 5 | "github.com/danryan/env" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // Config struct 11 | type Config struct { 12 | Name string `env:"key=LAZLO_NAME default=lazlo"` 13 | Token string `env:"key=LAZLO_TOKEN"` 14 | URL string `env:"key=LAZLO_URL default=http://localhost"` 15 | LogLevel string `env:"key=LAZLO_LOG_LEVEL default=info"` 16 | RedisURL string `env:"key=LAZLO_REDIS_URL"` 17 | RedisPW string `env:"key=LAZLO_REDIS_PW"` 18 | Port string `env:"key=PORT"` 19 | } 20 | 21 | func newConfig() *Config { 22 | c := &Config{} 23 | env.MustProcess(c) 24 | return c 25 | } 26 | 27 | func newLogger() *logging.Logger { 28 | format := "%25s [%s] %8s: %s\n time,name,levelname,message" 29 | timeFormat := time.RFC3339 30 | level := logging.GetLevelValue(`INFO`) 31 | logger, _ := logging.WriterLogger("lazlo", level, format, timeFormat, os.Stdout, true) 32 | return logger 33 | } 34 | -------------------------------------------------------------------------------- /lib/httpserver.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/bmizerany/pat" 8 | ) 9 | 10 | //This map is redundant with the brokers callbacks.Index but there's 11 | //no way to get a reference to the broker into metaHandler so kludge 12 | var httpRoutes = make(map[string]*LinkCallback) 13 | 14 | type LinkCallback struct { 15 | p string // the httpRoutes index value 16 | ID string 17 | Path string // the computed URL 18 | URL string 19 | Handler func(res http.ResponseWriter, req *http.Request) 20 | Chan chan *http.Request 21 | } 22 | 23 | func (b *Broker) StartHttp() { 24 | m := pat.New() 25 | m.Get("/", http.HandlerFunc(metaHandler)) 26 | m.Get("/linkcb/:name", http.HandlerFunc(metaHandler)) 27 | http.Handle("/", m) 28 | err := http.ListenAndServe(":"+b.Config.Port, nil) 29 | if err != nil { 30 | Logger.Error(err) 31 | } 32 | } 33 | 34 | func metaHandler(res http.ResponseWriter, req *http.Request) { 35 | Logger.Debug("entered metaHandler") 36 | path := req.URL.Query().Get(":name") 37 | if path == `` { 38 | Logger.Debug("path is /") 39 | fmt.Fprintln(res, "Hi. I am a Lazlo bot") 40 | } else if cb, ok := httpRoutes[path]; ok { 41 | Logger.Debug("path is known") 42 | if cb.Handler == nil { 43 | go func(cb *LinkCallback) { 44 | cb.Chan <- req 45 | fmt.Fprintln(res, "Path: %s handled. Thanks!", path) 46 | }(cb) 47 | } else { 48 | cb.Handler(res, req) 49 | } 50 | } else { 51 | Logger.Debug("path is unknown (", path, ")") 52 | fmt.Fprintf(res, "sorry, no modules have registered to handle %s\n", path) 53 | } 54 | } 55 | 56 | func (b *Broker) LinkCallback(p string, f ...func(http.ResponseWriter, *http.Request)) *LinkCallback { 57 | path := fmt.Sprintf("linkcb/%s", p) 58 | callback := &LinkCallback{ 59 | p: p, 60 | ID: fmt.Sprintf("link:%d", len(b.callbacks[L].Index)), 61 | Path: path, 62 | URL: fmt.Sprintf("%s:%s/%s", b.Config.URL, b.Config.Port, path), 63 | Chan: make(chan *http.Request), 64 | } 65 | 66 | //user-provided http handler function 67 | if f != nil { 68 | callback.Handler = f[0] 69 | } 70 | 71 | //append the path to the list of routes used by metaHandler() 72 | httpRoutes[p] = callback 73 | 74 | if err := b.RegisterCallback(callback); err != nil { 75 | Logger.Error("error registering callback ", callback.ID, ":: ", err) 76 | return nil 77 | } 78 | return callback 79 | } 80 | 81 | func (l *LinkCallback) Delete() { 82 | delete(httpRoutes, l.p) 83 | } 84 | -------------------------------------------------------------------------------- /lib/slackTypes.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | type ApiResponse struct { 4 | Bots []Bot `json:"bots,omitempty"` 5 | CacheVersion string `json:"cache_version,omitempty"` 6 | Channels []Channel `json:"channels,omitempty"` 7 | Channel Channel `json:"channel,omitempty"` 8 | Groups []Group `json:"groups,omitempty"` 9 | Group Group `json:"group,omitempty"` 10 | IMs []IM `json:"ims,omitempty"` 11 | LatestEventTs string `json:"latest_event_ts,omitempty"` 12 | Latest string `json:"latest,omitempty"` 13 | Ok bool `json:"ok,omitempty"` 14 | ReplyTo int32 `json:"reply_to,omitempty"` 15 | Error string `json:"error,omitempty"` 16 | HasMore bool `json:"has_more,omitempty"` 17 | Self Self `json:"self,omitempty"` 18 | Team Team `json:"team,omitempty"` 19 | URL string `json:"url,omitempty"` 20 | Users []User `json:"users,omitempty"` 21 | User User `json:"user,omitempty"` 22 | Messages []Event `json:"messages,omitempty"` 23 | } 24 | 25 | type Event struct { 26 | ID int32 `json:"id,omitempty"` 27 | Type string `json:"type,omitempty"` 28 | Channel string `json:"channel,omitempty"` 29 | Text string `json:"text,omitempty"` 30 | Attachments []Attachment `json:"attachments,omitempty"` 31 | User string `json:"user,omitempty"` 32 | UserName string `json:"username,omitempty"` 33 | BotID string `json:"bot_id,omitempty"` 34 | Subtype string `json:"subtype,omitempty"` 35 | Ts string `json:"ts,omitempty"` 36 | Broker *Broker 37 | CallBackCode string `json:"callbackcode,omitempty"` 38 | Extra map[string]interface{} 39 | } 40 | 41 | type Attachment struct { 42 | Fallback string `json:"fallback"` 43 | Color string `json:"color,omitempty"` 44 | Pretext string `json:"pretext,omitempty"` 45 | AuthorName string `json:"author_name,omitempty"` 46 | AuthorLink string `json:"author_link,omitempty"` 47 | AuthorIcon string `json:"author_icon,omitempty"` 48 | Title string `json:"title,omitempty"` 49 | TitleLink string `json:"title_link,omitempty"` 50 | Text string `json:"text,omitempty"` 51 | Fields []AttachmentField `json:"fields,omitempty"` 52 | ImageUrl string `json:"image_url,omitempty"` 53 | ThumbUrl string `json:"thumb_url,omitempty"` 54 | MarkdownIn []string `json:"mrkdwn_in,omitempty"` 55 | } 56 | 57 | type AttachmentField struct { 58 | Title string `json:"title"` 59 | Value string `json:"value"` 60 | Short bool `json:"short,omitempty"` 61 | } 62 | 63 | type User struct { 64 | Color string `json:"color,omitempty"` 65 | Deleted bool `json:"deleted,omitempty"` 66 | HasFiles bool `json:"has_files,omitempty"` 67 | ID string `json:"id,omitempty"` 68 | IsAdmin bool `json:"is_admin,omitempty"` 69 | IsBot bool `json:"is_bot,omitempty"` 70 | IsOwner bool `json:"is_owner,omitempty"` 71 | IsPrimaryOwner bool `json:"is_primary_owner,omitempty"` 72 | IsRestricted bool `json:"is_restricted,omitempty"` 73 | IsUltraRestricted bool `json:"is_ultra_restricted,omitempty"` 74 | Name string `json:"name,omitempty"` 75 | Phone interface{} `json:"phone,omitempty"` 76 | Presence string `json:"presence,omitempty"` 77 | Profile struct { 78 | Email string `json:"email,omitempty"` 79 | FirstName string `json:"first_name,omitempty"` 80 | Image192 string `json:"image_192,omitempty"` 81 | Image24 string `json:"image_24,omitempty"` 82 | Image32 string `json:"image_32,omitempty"` 83 | Image48 string `json:"image_48,omitempty"` 84 | Image72 string `json:"image_72,omitempty"` 85 | LastName string `json:"last_name,omitempty"` 86 | Phone string `json:"phone,omitempty"` 87 | RealName string `json:"real_name,omitempty"` 88 | RealNameNormalized string `json:"real_name_normalized,omitempty"` 89 | } `json:"profile,omitempty"` 90 | RealName string `json:"real_name,omitempty"` 91 | Skype string `json:"skype,omitempty"` 92 | Status interface{} `json:"status,omitempty"` 93 | Tz string `json:"tz,omitempty"` 94 | TzLabel string `json:"tz_label,omitempty"` 95 | TzOffset float64 `json:"tz_offset,omitempty"` 96 | Extra map[string]interface{} 97 | } 98 | 99 | type Channel struct { 100 | Created float64 `json:"created,omitempty"` 101 | Creator string `json:"creator,omitempty"` 102 | ID string `json:"id,omitempty"` 103 | IsArchived bool `json:"is_archived,omitempty"` 104 | IsChannel bool `json:"is_channel,omitempty"` 105 | IsGeneral bool `json:"is_general,omitempty"` 106 | IsMember bool `json:"is_member,omitempty"` 107 | LastRead string `json:"last_read,omitempty"` 108 | Latest Event `json:"latest,omitempty"` 109 | Members []string `json:"members,omitempty"` 110 | Name string `json:"name,omitempty"` 111 | Purpose Topic `json:"purpose,omitempty"` 112 | Topic Topic `json:"topic,omitempty"` 113 | UnreadCount float64 `json:"unread_count,omitempty"` 114 | Extra map[string]interface{} 115 | } 116 | 117 | type Group struct { 118 | Created int64 `json:"created,omitempty"` 119 | Creator string `json:"creator,omitempty"` 120 | ID string `json:"id,omitempty"` 121 | IsArchived bool `json:"is_archived,omitempty"` 122 | IsGroup bool `json:"is_group,omitempty"` 123 | Members []string `json:"members,omitempty"` 124 | Name string `json:"name,omitempty"` 125 | Purpose Topic `json:"purpose,omitempty"` 126 | Topic Topic `json:"topic,omitempty"` 127 | Extra map[string]interface{} 128 | } 129 | 130 | type Topic struct { 131 | Creator string `json:"creator,omitempty"` 132 | LastSet float64 `json:"last_set,omitempty"` 133 | Value string `json:"value,omitempty"` 134 | } 135 | 136 | type IM struct { 137 | Created int64 `json:"created,omitempty"` 138 | ID string `json:"id,omitempty"` 139 | IsIm bool `json:"is_im,omitempty"` 140 | IsUserDeleted bool `json:"is_user_deleted,omitempty"` 141 | Latest Event `json:"latest,omitempty"` 142 | User string `json:"user,omitempty"` 143 | Extra map[string]interface{} 144 | } 145 | 146 | type Bot struct { 147 | Created float64 `json:"created,omitempty"` 148 | Deleted bool `json:"deleted,omitempty"` 149 | Icons Icon `json:"icons,omitempty"` 150 | ID string `json:"id,omitempty"` 151 | IsIm bool `json:"is_im,omitempty"` 152 | IsUserDeleted bool `json:"is_user_deleted,omitempty"` 153 | User string `json:"user,omitempty"` 154 | Name string `json:"name,omitempty"` 155 | Extra map[string]interface{} 156 | } 157 | 158 | type Icon struct { 159 | Image192 string `json:"image_132,omitempty"` 160 | Image132 string `json:"image_132,omitempty"` 161 | Image102 string `json:"image_102,omitempty"` 162 | Image88 string `json:"image_88,omitempty"` 163 | Image68 string `json:"image_68,omitempty"` 164 | Image48 string `json:"image_48,omitempty"` 165 | Image44 string `json:"image_44,omitempty"` 166 | Image34 string `json:"image_34,omitempty"` 167 | ImageDefault bool `json:"image_default,omitempty"` 168 | } 169 | 170 | type Self struct { 171 | Created float64 `json:"created,omitempty"` 172 | ID string `json:"id,omitempty"` 173 | ManualPresence string `json:"manual_presence,omitempty"` 174 | Name string `json:"name,omitempty"` 175 | Prefs struct { 176 | AllChannelsLoud bool `json:"all_channels_loud,omitempty"` 177 | ArrowHistory bool `json:"arrow_history,omitempty"` 178 | AtChannelSuppressedChannels string `json:"at_channel_suppressed_channels,omitempty"` 179 | AutoplayChatSounds bool `json:"autoplay_chat_sounds,omitempty"` 180 | Collapsible bool `json:"collapsible,omitempty"` 181 | CollapsibleByClick bool `json:"collapsible_by_click,omitempty"` 182 | ColorNamesInList bool `json:"color_names_in_list,omitempty"` 183 | CommaKeyPrefs bool `json:"comma_key_prefs,omitempty"` 184 | ConvertEmoticons bool `json:"convert_emoticons,omitempty"` 185 | DisplayRealNamesOverride float64 `json:"display_real_names_override,omitempty"` 186 | DropboxEnabled bool `json:"dropbox_enabled,omitempty"` 187 | EmailAlerts string `json:"email_alerts,omitempty"` 188 | EmailAlertsSleepUntil float64 `json:"email_alerts_sleep_until,omitempty"` 189 | EmailMisc bool `json:"email_misc,omitempty"` 190 | EmailWeekly bool `json:"email_weekly,omitempty"` 191 | EmojiMode string `json:"emoji_mode,omitempty"` 192 | EnterIsSpecialInTbt bool `json:"enter_is_special_in_tbt,omitempty"` 193 | ExpandInlineImgs bool `json:"expand_inline_imgs,omitempty"` 194 | ExpandInternalInlineImgs bool `json:"expand_internal_inline_imgs,omitempty"` 195 | ExpandNonMediaAttachments bool `json:"expand_non_media_attachments,omitempty"` 196 | ExpandSnippets bool `json:"expand_snippets,omitempty"` 197 | FKeySearch bool `json:"f_key_search,omitempty"` 198 | FullTextExtracts bool `json:"full_text_extracts,omitempty"` 199 | FuzzyMatching bool `json:"fuzzy_matching,omitempty"` 200 | GraphicEmoticons bool `json:"graphic_emoticons,omitempty"` 201 | GrowlsEnabled bool `json:"growls_enabled,omitempty"` 202 | HasCreatedChannel bool `json:"has_created_channel,omitempty"` 203 | HasInvited bool `json:"has_invited,omitempty"` 204 | HasUploaded bool `json:"has_uploaded,omitempty"` 205 | HighlightWords string `json:"highlight_words,omitempty"` 206 | KKeyOmnibox bool `json:"k_key_omnibox,omitempty"` 207 | LastSnippetType string `json:"last_snippet_type,omitempty"` 208 | LoudChannels string `json:"loud_channels,omitempty"` 209 | LoudChannelsSet string `json:"loud_channels_set,omitempty"` 210 | LsDisabled bool `json:"ls_disabled,omitempty"` 211 | MacSpeakSpeed float64 `json:"mac_speak_speed,omitempty"` 212 | MacSpeakVoice string `json:"mac_speak_voice,omitempty"` 213 | MacSsbBounce string `json:"mac_ssb_bounce,omitempty"` 214 | MacSsbBullet bool `json:"mac_ssb_bullet,omitempty"` 215 | MarkMsgsReadImmediately bool `json:"mark_msgs_read_immediately,omitempty"` 216 | MessagesTheme string `json:"messages_theme,omitempty"` 217 | MuteSounds bool `json:"mute_sounds,omitempty"` 218 | MutedChannels string `json:"muted_channels,omitempty"` 219 | NeverChannels string `json:"never_channels,omitempty"` 220 | NewMsgSnd string `json:"new_msg_snd,omitempty"` 221 | NoCreatedOverlays bool `json:"no_created_overlays,omitempty"` 222 | NoJoinedOverlays bool `json:"no_joined_overlays,omitempty"` 223 | NoMacssb1Banner bool `json:"no_macssb1_banner,omitempty"` 224 | NoTextInNotifications bool `json:"no_text_in_notifications,omitempty"` 225 | ObeyInlineImgLimit bool `json:"obey_inline_img_limit,omitempty"` 226 | PagekeysHandled bool `json:"pagekeys_handled,omitempty"` 227 | PostsFormattingGuide bool `json:"posts_formatting_guide,omitempty"` 228 | PrivacyPolicySeen bool `json:"privacy_policy_seen,omitempty"` 229 | PromptedForEmailDisabling bool `json:"prompted_for_email_disabling,omitempty"` 230 | PushAtChannelSuppressedChannels string `json:"push_at_channel_suppressed_channels,omitempty"` 231 | PushDmAlert bool `json:"push_dm_alert,omitempty"` 232 | PushEverything bool `json:"push_everything,omitempty"` 233 | PushIdleWait float64 `json:"push_idle_wait,omitempty"` 234 | PushLoudChannels string `json:"push_loud_channels,omitempty"` 235 | PushLoudChannelsSet string `json:"push_loud_channels_set,omitempty"` 236 | PushMentionAlert bool `json:"push_mention_alert,omitempty"` 237 | PushMentionChannels string `json:"push_mention_channels,omitempty"` 238 | PushSound string `json:"push_sound,omitempty"` 239 | RequireAt bool `json:"require_at,omitempty"` 240 | SearchExcludeBots bool `json:"search_exclude_bots,omitempty"` 241 | SearchExcludeChannels string `json:"search_exclude_channels,omitempty"` 242 | SearchOnlyMyChannels bool `json:"search_only_my_channels,omitempty"` 243 | SearchSort string `json:"search_sort,omitempty"` 244 | SeenChannelMenuTipCard bool `json:"seen_channel_menu_tip_card,omitempty"` 245 | SeenChannelsTipCard bool `json:"seen_channels_tip_card,omitempty"` 246 | SeenDomainInviteReminder bool `json:"seen_domain_invite_reminder,omitempty"` 247 | SeenFlexpaneTipCard bool `json:"seen_flexpane_tip_card,omitempty"` 248 | SeenMemberInviteReminder bool `json:"seen_member_invite_reminder,omitempty"` 249 | SeenMessageInputTipCard bool `json:"seen_message_input_tip_card,omitempty"` 250 | SeenSearchInputTipCard bool `json:"seen_search_input_tip_card,omitempty"` 251 | SeenSsbPrompt bool `json:"seen_ssb_prompt,omitempty"` 252 | SeenTeamMenuTipCard bool `json:"seen_team_menu_tip_card,omitempty"` 253 | SeenUserMenuTipCard bool `json:"seen_user_menu_tip_card,omitempty"` 254 | SeenWelcome2 bool `json:"seen_welcome_2,omitempty"` 255 | ShowMemberPresence bool `json:"show_member_presence,omitempty"` 256 | ShowTyping bool `json:"show_typing,omitempty"` 257 | SidebarBehavior string `json:"sidebar_behavior,omitempty"` 258 | SidebarTheme string `json:"sidebar_theme,omitempty"` 259 | SidebarThemeCustomValues string `json:"sidebar_theme_custom_values,omitempty"` 260 | SnippetEditorWrapLongLines bool `json:"snippet_editor_wrap_long_lines,omitempty"` 261 | SpeakGrowls bool `json:"speak_growls,omitempty"` 262 | SsEmojis bool `json:"ss_emojis,omitempty"` 263 | StartScrollAtOldest bool `json:"start_scroll_at_oldest,omitempty"` 264 | TabUiReturnSelects bool `json:"tab_ui_return_selects,omitempty"` 265 | Time24 bool `json:"time24,omitempty"` 266 | Tz string `json:"tz,omitempty"` 267 | UserColors string `json:"user_colors,omitempty"` 268 | WebappSpellcheck bool `json:"webapp_spellcheck,omitempty"` 269 | WelcomeMessageHidden bool `json:"welcome_message_hidden,omitempty"` 270 | WinSsbBullet bool `json:"win_ssb_bullet,omitempty"` 271 | } `json:"prefs,omitempty"` 272 | } 273 | 274 | type Team struct { 275 | Domain string `json:"domain,omitempty"` 276 | EmailDomain string `json:"email_domain,omitempty"` 277 | Icon Icon `json:"icon,omitempty"` 278 | ID string `json:"id,omitempty"` 279 | MsgEditWindowMins float64 `json:"msg_edit_window_mins,omitempty"` 280 | Name string `json:"name,omitempty"` 281 | OverStorageLimit bool `json:"over_storage_limit,omitempty"` 282 | Prefs struct { 283 | AllowMessageDeletion bool `json:"allow_message_deletion,omitempty"` 284 | DefaultChannels []string `json:"default_channels,omitempty"` 285 | DisplayRealNames bool `json:"display_real_names,omitempty"` 286 | DmRetentionDuration float64 `json:"dm_retention_duration,omitempty"` 287 | DmRetentionType float64 `json:"dm_retention_type,omitempty"` 288 | GatewayAllowIrcPlain float64 `json:"gateway_allow_irc_plain,omitempty"` 289 | GatewayAllowIrcSsl float64 `json:"gateway_allow_irc_ssl,omitempty"` 290 | GatewayAllowXmppSsl float64 `json:"gateway_allow_xmpp_ssl,omitempty"` 291 | GroupRetentionDuration float64 `json:"group_retention_duration,omitempty"` 292 | GroupRetentionType float64 `json:"group_retention_type,omitempty"` 293 | HideReferers bool `json:"hide_referers,omitempty"` 294 | MsgEditWindowMins float64 `json:"msg_edit_window_mins,omitempty"` 295 | RequireAtForMention bool `json:"require_at_for_mention,omitempty"` 296 | RetentionDuration float64 `json:"retention_duration,omitempty"` 297 | RetentionType float64 `json:"retention_type,omitempty"` 298 | WhoCanArchiveChannels string `json:"who_can_archive_channels,omitempty"` 299 | WhoCanAtChannel string `json:"who_can_at_channel,omitempty"` 300 | WhoCanAtEveryone string `json:"who_can_at_everyone,omitempty"` 301 | WhoCanCreateChannels string `json:"who_can_create_channels,omitempty"` 302 | WhoCanCreateGroups string `json:"who_can_create_groups,omitempty"` 303 | WhoCanKickChannels string `json:"who_can_kick_channels,omitempty"` 304 | WhoCanKickGroups string `json:"who_can_kick_groups,omitempty"` 305 | WhoCanPostGeneral string `json:"who_can_post_general,omitempty"` 306 | } `json:"prefs,omitempty"` 307 | Extra map[string]interface{} 308 | } 309 | -------------------------------------------------------------------------------- /lib/timers.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gorhill/cronexpr" 6 | "time" 7 | ) 8 | 9 | // verify the schedule and start the timer 10 | func (t *TimerCallback) Start() error { 11 | expr := cronexpr.MustParse(t.Schedule) 12 | if expr.Next(time.Now()).IsZero() { 13 | Logger.Debug("invalid schedule", t.Schedule) 14 | t.State = fmt.Sprintf("NOT Scheduled (invalid Schedule: %s)", t.Schedule) 15 | return fmt.Errorf("invalid schedule", t.Schedule) 16 | } 17 | t.Next = expr.Next(time.Now()) 18 | dur := t.Next.Sub(time.Now()) 19 | if dur > 0 { 20 | go t.Run(dur) 21 | } 22 | return nil 23 | } 24 | 25 | // wait for the timer to expire, callback to the module, and reschedule 26 | func (t *TimerCallback) Run(dur time.Duration) { 27 | Logger.Debug(`scheduling timer `, t.ID, ` for: `, t.Next) 28 | timer := time.NewTimer(dur) 29 | stop := false 30 | for !stop { 31 | select { 32 | case alarm := <-timer.C: 33 | t.Chan <- alarm //signal the module 34 | t.Start() // (potentially) reschedule 35 | case stop = <-t.stop: 36 | stop = true 37 | } 38 | } 39 | } 40 | 41 | func (t *TimerCallback) Stop() { 42 | t.stop <- true 43 | } 44 | -------------------------------------------------------------------------------- /loadModules.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | lazlo "github.com/djosephsen/lazlo/lib" 5 | "github.com/djosephsen/lazlo/modules" 6 | ) 7 | 8 | func initModules(b *lazlo.Broker) error { 9 | b.Register(modules.Syn) 10 | // b.Register(modules.RTMPing) 11 | b.Register(modules.LinkTest) 12 | b.Register(modules.BrainTest) 13 | b.Register(modules.Help) 14 | b.Register(modules.LuaMod) 15 | b.Register(modules.QuestionTest) 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /lua/test.lua: -------------------------------------------------------------------------------- 1 | synfunc = function (msg) msg:Reply("ack") end 2 | robot:Respond("syn",synfunc) 3 | 4 | --respond(robot.ID, "syn", synfunc) 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/signal" 5 | "syscall" 6 | 7 | lazlo "github.com/djosephsen/lazlo/lib" 8 | ) 9 | 10 | func main() { 11 | 12 | lazlo.Logger.Debug(`creating broker`) 13 | //make a broker 14 | broker, err := lazlo.NewBroker() 15 | if err != nil { 16 | lazlo.Logger.Error(err) 17 | return 18 | } 19 | defer broker.Brain.Close() 20 | 21 | lazlo.Logger.Debug(`starting modules`) 22 | // register the modules 23 | if err := initModules(broker); err != nil { 24 | lazlo.Logger.Error(err) 25 | return 26 | } 27 | //start the Modules 28 | broker.StartModules() 29 | 30 | lazlo.Logger.Debug(`starting broker`) 31 | //start the broker 32 | go broker.Start() 33 | // Loop 34 | signal.Notify(broker.SigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 35 | stop := false 36 | for !stop { 37 | select { 38 | case sig := <-broker.SigChan: 39 | switch sig { 40 | case syscall.SIGINT, syscall.SIGTERM: 41 | stop = true 42 | } 43 | } 44 | } 45 | // Stop listening for new signals 46 | signal.Stop(broker.SigChan) 47 | broker.Stop() 48 | 49 | //wait for the write thread to stop (so the shutdown hooks have a chance to run) 50 | <-broker.SyncChan 51 | } 52 | -------------------------------------------------------------------------------- /modules/braintest.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | lazlo "github.com/djosephsen/lazlo/lib" 8 | ) 9 | 10 | var BrainTest = &lazlo.Module{ 11 | Name: `BrainTest`, 12 | Usage: `"%BOTNAME% brain [set|get] ": tests lazlo's persistent storage (aka the brain)`, 13 | Run: func(b *lazlo.Broker) { 14 | callback := b.MessageCallback(`(?i:brain) ((?i)set|get) (\w+) *(\w*)$`, true) 15 | for { 16 | msg := <-callback.Chan 17 | brain := b.Brain 18 | cmd := msg.Match[1] 19 | key := msg.Match[2] 20 | if matched, _ := regexp.MatchString(`(?i)set`, cmd); matched { 21 | val := msg.Match[3] 22 | if err := brain.Set(key, []byte(val)); err != nil { 23 | msg.Event.Reply(fmt.Sprintf("Sorry, something went wrong: %s", err)) 24 | lazlo.Logger.Error(err) 25 | } else { 26 | msg.Event.Reply(fmt.Sprintf("Ok, %s set to %s", key, val)) 27 | } 28 | } else { 29 | val, err := brain.Get(key) 30 | if err != nil { 31 | msg.Event.Reply(fmt.Sprintf("Sorry, something went wrong: %s", err)) 32 | } else { 33 | msg.Event.Reply(string(val)) 34 | } 35 | } 36 | } 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /modules/help.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | lazlo "github.com/djosephsen/lazlo/lib" 8 | ) 9 | 10 | var Help = &lazlo.Module{ 11 | Name: `Help`, 12 | Usage: `"%BOTNAME% help": prints the usage information of every registered plugin`, 13 | Run: helpRun, 14 | } 15 | 16 | func helpRun(b *lazlo.Broker) { 17 | cb := b.MessageCallback(`(?i)help`, true) 18 | for { 19 | pm := <-cb.Chan 20 | go getHelp(b, &pm) 21 | } 22 | } 23 | 24 | func getHelp(b *lazlo.Broker, pm *lazlo.PatternMatch) { 25 | dmChan := b.GetDM(pm.Event.User) 26 | reply := `########## Modules In use: ` 27 | for _, m := range b.Modules { 28 | if strings.Contains(m.Usage, `%HIDDEN%`) { 29 | continue 30 | } 31 | usage := strings.Replace(m.Usage, `%BOTNAME%`, b.Config.Name, -1) 32 | reply = fmt.Sprintf("%s\n%s", reply, usage) 33 | } 34 | b.Say(reply, dmChan) 35 | } 36 | -------------------------------------------------------------------------------- /modules/linktest.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "fmt" 5 | 6 | lazlo "github.com/djosephsen/lazlo/lib" 7 | ) 8 | 9 | var LinkTest = &lazlo.Module{ 10 | Name: `LinkTest`, 11 | Usage: `"%BOTNAME% linkme foo" : creates a clickable link at servername/foo`, 12 | Run: func(b *lazlo.Broker) { 13 | clickChan := make(chan string) 14 | optionChan := make(chan string) 15 | command_cb := b.MessageCallback(`(?i)(link *me) (.*)`, true) 16 | command_cb1 := b.MessageCallback(`(?i)(link *test)`, true) 17 | command_cb2 := b.MessageCallback(`(?i)(link *choice)`, true) 18 | for { 19 | select { 20 | case msg := <-command_cb.Chan: 21 | msg.Event.Reply(newLink(b, msg.Match[2], clickChan)) 22 | case msg := <-command_cb1.Chan: 23 | msg.Event.Reply(``) 24 | case msg := <-command_cb2.Chan: 25 | msg.Event.Reply(newChoice(b, optionChan)) 26 | case click := <-clickChan: 27 | b.Say(fmt.Sprintf("Somebody clicked on %s", click)) 28 | case option := <-optionChan: 29 | if option == `THIS` { 30 | b.Say(fmt.Sprintf("I knew you'd get with this.. cause this is kinda phat")) 31 | } else { 32 | b.Say(fmt.Sprintf("Not a Blacksheep fan eh? bummer.")) 33 | } 34 | } 35 | } 36 | }, 37 | } 38 | 39 | func newLink(b *lazlo.Broker, path string, clickChan chan string) string { 40 | link_cb := b.LinkCallback(path) 41 | go func(link_cb *lazlo.LinkCallback, clickChan chan string) { 42 | for { 43 | <-link_cb.Chan 44 | clickChan <- link_cb.Path 45 | } 46 | }(link_cb, clickChan) 47 | return fmt.Sprintf("Ok, <%s|here> is a link on %s", link_cb.URL, path) 48 | } 49 | 50 | func newChoice(b *lazlo.Broker, clickChan chan string) string { 51 | opt1 := b.LinkCallback(`option1`) 52 | opt2 := b.LinkCallback(`option2`) 53 | go func(opt1 *lazlo.LinkCallback, opt2 *lazlo.LinkCallback, clickChan chan string) { 54 | for { 55 | select { 56 | case <-opt1.Chan: 57 | clickChan <- `THIS` 58 | case <-opt2.Chan: 59 | clickChan <- `THAT` 60 | } 61 | } 62 | }(opt1, opt2, clickChan) 63 | return fmt.Sprintf("you can get with <%s|THIS> or you can get with <%s|THAT>", opt1.URL, opt2.URL) 64 | } 65 | -------------------------------------------------------------------------------- /modules/luaMod.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "reflect" 8 | "time" 9 | 10 | lazlo "github.com/djosephsen/lazlo/lib" 11 | luar "github.com/layeh/gopher-luar" 12 | lua "github.com/yuin/gopher-lua" 13 | ) 14 | 15 | //luaMod implements a lua-parsing plugin for Lazlo. 16 | //this enables lazlo to be scripted via lua instead of GO, which is 17 | //preferable in some contexts (simpler(?), no recompiles for changes etc..). 18 | var LuaMod = &lazlo.Module{ 19 | Name: `LuaMod`, 20 | Usage: `%HIDDEN% this module implements lua scripting of lazlo`, 21 | Run: luaMain, 22 | } 23 | 24 | //Each LuaScript represents a single lua script/state machine 25 | type LuaScript struct { 26 | Robot *Robot 27 | State *lua.LState 28 | } 29 | 30 | //Keep a local version of lazlo.Patternmatch so we can add methods to it 31 | type LocalPatternMatch lazlo.PatternMatch 32 | 33 | //A CBMap maps a specific callback Case to it's respective lua function and 34 | //parent lua script 35 | type CBMap struct { 36 | Func lua.LValue 37 | Callback reflect.Value 38 | Script *LuaScript 39 | } 40 | 41 | type Robot struct { 42 | ID int 43 | } 44 | 45 | func (r *Robot) GetID() int { 46 | return r.ID 47 | } 48 | 49 | //LuaScripts allows us to Retrieve lua.LState by LuaScript.Robot.ID 50 | var LuaScripts []LuaScript 51 | 52 | //CBtable allows us to Retrieve lua.LState by callback case index 53 | var CBTable []CBMap 54 | 55 | //Cases is used by reflect.Select to deliver events from lazlo 56 | var Cases []reflect.SelectCase 57 | 58 | //Broker is a global pointer back to our lazlo broker 59 | var broker *lazlo.Broker 60 | 61 | //luaMain creates a new lua state for each file in ./lua 62 | //and hands them the globals they need to interact with lazlo 63 | func luaMain(b *lazlo.Broker) { 64 | broker = b 65 | var luaDir *os.File 66 | luaDirName := "lua" 67 | if luaDirInfo, err := os.Stat(luaDirName); err == nil && luaDirInfo.IsDir() { 68 | luaDir, _ = os.Open(luaDirName) 69 | } else { 70 | lazlo.Logger.Error("Couldn't open the Lua Plugin dir: ", err) 71 | } 72 | luaFiles, _ := luaDir.Readdir(0) 73 | for _, f := range luaFiles { 74 | if f.IsDir() { 75 | continue 76 | } 77 | 78 | file := fmt.Sprintf("%s/%s", luaDirName, f.Name()) 79 | 80 | //make a new script entry 81 | script := LuaScript{ 82 | Robot: &Robot{ 83 | ID: len(LuaScripts), 84 | }, 85 | State: lua.NewState(), 86 | } 87 | defer script.State.Close() 88 | 89 | // register hear and respond inside this lua state 90 | script.State.SetGlobal("robot", luar.New(script.State, script.Robot)) 91 | //script.State.SetGlobal("respond", luar.New(script.State, Respond)) 92 | //script.State.SetGlobal("hear", luar.New(script.State, Hear)) 93 | LuaScripts = append(LuaScripts, script) 94 | 95 | // the lua script will register callbacks to the Cases 96 | if err := script.State.DoFile(file); err != nil { 97 | panic(err) 98 | } 99 | } 100 | //block waiting on events from the broker 101 | for { 102 | index, value, _ := reflect.Select(Cases) 103 | handle(index, value.Interface()) 104 | } 105 | } 106 | 107 | //pmTranslate makes a localized version of patternmatch so we can export it to lua 108 | //with a few additional methods. 109 | func pmTranslate(in lazlo.PatternMatch) LocalPatternMatch { 110 | return LocalPatternMatch{ 111 | Event: in.Event, 112 | Match: in.Match, 113 | } 114 | } 115 | 116 | //handle takes the index and value of an event from lazlo, 117 | //typifies the value and calls the right function to push the data back 118 | //to whatever lua script asked for it. 119 | func handle(index int, val interface{}) { 120 | switch val.(type) { 121 | case lazlo.PatternMatch: 122 | handleMessageCB(index, pmTranslate(val.(lazlo.PatternMatch))) 123 | case time.Time: 124 | handleTimerCB(index, val.(time.Time)) 125 | case map[string]interface{}: 126 | handleEventCB(index, val.(map[string]interface{})) 127 | case *http.Request: 128 | handleLinkCB(index, val.(*http.Response)) 129 | default: 130 | err := fmt.Errorf("luaMod handle:: unknown type: %T", val) 131 | lazlo.Logger.Error(err) 132 | } 133 | } 134 | 135 | //handleMessageCB brokers messages back to the lua script that asked for them 136 | func handleMessageCB(index int, message LocalPatternMatch) { 137 | l := CBTable[index].Script.State 138 | lmsg := luar.New(l, message) 139 | 140 | if err := l.CallByParam(lua.P{ 141 | Fn: CBTable[index].Func, 142 | NRet: 0, 143 | Protect: false, 144 | }, lmsg); err != nil { 145 | panic(err) 146 | } 147 | } 148 | 149 | //handleTimerCB brokers timer alarms back to the lua script that asked for them 150 | func handleTimerCB(index int, t time.Time) { 151 | return 152 | } 153 | 154 | //handleEventCB brokers slack rtm events back to the lua script that asked for them 155 | func handleEventCB(index int, event map[string]interface{}) { 156 | return 157 | } 158 | 159 | //handleLinkCB brokers http GET requests back to the lua script that asked for them 160 | func handleLinkCB(index int, resp *http.Response) { 161 | return 162 | } 163 | 164 | //creates a new message callback from robot.hear/respond 165 | func newMsgCallback(RID int, pat string, lfunc lua.LValue, isResponse bool) { 166 | // cbtable and cases indexes have to match 167 | if len(CBTable) != len(Cases) { 168 | panic(`cbtable != cases`) 169 | } 170 | cb := broker.MessageCallback(pat, isResponse) 171 | cbEntry := CBMap{ 172 | Func: lfunc, 173 | Callback: reflect.ValueOf(cb), 174 | Script: &LuaScripts[RID], 175 | } 176 | caseEntry := reflect.SelectCase{ 177 | Dir: reflect.SelectRecv, 178 | Chan: reflect.ValueOf(cb.Chan), 179 | } 180 | CBTable = append(CBTable, cbEntry) 181 | Cases = append(Cases, caseEntry) 182 | } 183 | 184 | //functions exported to the lua runtime below here 185 | 186 | //lua function to overhear a message 187 | func (r Robot) Hear(pat string, lfunc lua.LValue) { 188 | newMsgCallback(r.ID, pat, lfunc, false) 189 | } 190 | 191 | /*func Hear(id int, pat string, lfunc lua.LValue){ 192 | newMsgCallback(id, pat, lfunc, false) 193 | }*/ 194 | 195 | //lua function to process a command 196 | func (r Robot) Respond(pat string, lfunc lua.LValue) { 197 | newMsgCallback(r.ID, pat, lfunc, true) 198 | } 199 | 200 | /*func Respond(id int, pat string, lfunc lua.LValue){ 201 | newMsgCallback(id, pat, lfunc, true) 202 | }*/ 203 | 204 | //lua function to reply to a message passed to a lua-side callback 205 | func (pm LocalPatternMatch) Reply(words string) { 206 | pm.Event.Reply(words) 207 | } 208 | -------------------------------------------------------------------------------- /modules/ping.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | lazlo "github.com/djosephsen/lazlo/lib" 8 | ) 9 | 10 | var Syn = &lazlo.Module{ 11 | Name: `Ping`, 12 | Usage: `"%BOTNAME% (ping|syn)" : Test that the bot is currently running`, 13 | Run: pingRun, 14 | } 15 | 16 | func pingRun(b *lazlo.Broker) { 17 | cb := b.MessageCallback(`(?i)(ping|syn)`, true) 18 | for { 19 | pm := <-cb.Chan 20 | pm.Event.Reply(randReply()) 21 | } 22 | } 23 | 24 | func randReply() string { 25 | now := time.Now() 26 | rand.Seed(int64(now.Unix())) 27 | replies := []string{ 28 | "yeah um.. pong?", 29 | "WHAT?! jeeze.", 30 | "what? oh, um SYNACKSYN? ENOSPEAKTCP.", 31 | "RST (lulz)", 32 | "64 bytes from go.away.your.annoying icmp_seq=0 ttl=42 time=42.596 ms", 33 | "hmm?", 34 | "ack. what?", 35 | "pong. what?", 36 | "yup. still here.", 37 | "super busy just now.. Can I get back to you in like 5min?", 38 | } 39 | return replies[rand.Intn(len(replies)-1)] 40 | } 41 | -------------------------------------------------------------------------------- /modules/qtest.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "fmt" 5 | 6 | lazlo "github.com/djosephsen/lazlo/lib" 7 | ) 8 | 9 | var QuestionTest = &lazlo.Module{ 10 | Name: `QuestionTest`, 11 | Usage: `"%BOTNAME% askme foo" : replies with the question: foo? 12 | %BOTNAME% qtest : runs an automated question test`, 13 | Run: func(b *lazlo.Broker) { 14 | cb1 := b.MessageCallback(`(?i)(ask *me) (.*)`, true) 15 | cb2 := b.MessageCallback(`(?i)(qtest)`, true) 16 | for { 17 | select { 18 | case newReq := <-cb1.Chan: 19 | go newQuestion(b, newReq) // an example of dynamic question/response 20 | case newReq := <-cb2.Chan: 21 | go runTest(b, newReq) // an example of scripted question/response 22 | } 23 | } 24 | }, 25 | } 26 | 27 | func newQuestion(b *lazlo.Broker, req lazlo.PatternMatch) { 28 | lazlo.Logger.Info("new question") 29 | qcb := b.QuestionCallback(req.Event.User, req.Match[2]) 30 | answer := <-qcb.Answer 31 | response := fmt.Sprintf("You answered: '%s'", answer) 32 | b.Say(response, qcb.DMChan) 33 | } 34 | 35 | func runTest(b *lazlo.Broker, req lazlo.PatternMatch) { 36 | dmChan := b.GetDM(req.Event.User) 37 | user := b.SlackMeta.GetUserName(req.Event.User) 38 | b.Say(fmt.Sprintf(`hi %s! I'm going to ask you a few questions.`, user), dmChan) 39 | qcb := b.QuestionCallback(req.Event.User, `what is your name?`) 40 | name := <-qcb.Answer 41 | qcb = b.QuestionCallback(req.Event.User, `what is your quest?`) 42 | quest := <-qcb.Answer 43 | qcb = b.QuestionCallback(req.Event.User, `what is your favorite color?`) 44 | color := <-qcb.Answer 45 | b.Say(fmt.Sprintf(`awesome. you said your name is %s, your quest is %s and your favorite color is %s`, name, quest, color), dmChan) 46 | } 47 | -------------------------------------------------------------------------------- /modules/rtmping.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | lazlo "github.com/djosephsen/lazlo/lib" 5 | ) 6 | 7 | var RTMPing = &lazlo.Module{ 8 | Name: `RTMPing`, 9 | Usage: `Automatially sends an RTM Ping to SlackHQ every 20 seconds`, 10 | Run: rtmrun, 11 | } 12 | 13 | func rtmrun(b *lazlo.Broker) { 14 | for { 15 | // get a timer callback 16 | timer := b.TimerCallback(`*/20 * * * * * *`) 17 | 18 | // block waiting for an alarm from the timer 19 | <-timer.Chan 20 | 21 | //send a ping 22 | b.Send(&lazlo.Event{ 23 | Type: `ping`, 24 | Text: `just pingin`, 25 | }) 26 | } 27 | } 28 | --------------------------------------------------------------------------------