├── .env ├── .travis.yml ├── Godeps ├── Godeps.json ├── Readme └── _workspace │ ├── .gitignore │ └── src │ └── golang.org │ └── x │ └── net │ ├── LICENSE │ ├── PATENTS │ └── websocket │ ├── client.go │ ├── hybi.go │ ├── server.go │ └── websocket.go ├── Procfile ├── README.md ├── app.json ├── main.go ├── slack-api.go └── wwdc-api.go /.env: -------------------------------------------------------------------------------- 1 | SLACK_BOT_TOKEN= 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.1 5 | - 1.4 6 | - 1.5 7 | - 1.6 8 | 9 | before_install: 10 | - go get github.com/axw/gocov/gocov 11 | - go get github.com/mattn/goveralls 12 | - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 13 | 14 | script: 15 | - go vet 16 | - go test -v 17 | - $HOME/gopath/bin/goveralls -service=travis-ci -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/sger/wwdc-bot", 3 | "GoVersion": "go1.5", 4 | "Packages": [ 5 | "./..." 6 | ], 7 | "Deps": [ 8 | { 9 | "ImportPath": "golang.org/x/net/websocket", 10 | "Rev": "68a055e15f0ce90989da5564fd947fb72ce67513" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /Godeps/_workspace/.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | /bin 3 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Go project. 5 | 6 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this section) 8 | patent license to make, have made, use, offer to sell, sell, import, 9 | transfer and otherwise run, modify and propagate the contents of this 10 | implementation of Go, where such license applies only to those patent 11 | claims, both currently owned or controlled by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by this 13 | implementation of Go. This grant does not include claims that would be 14 | infringed only as a consequence of further modification of this 15 | implementation. If you or your agent or exclusive licensee institute or 16 | order or agree to the institution of patent litigation against any 17 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 18 | that this implementation of Go or any code incorporated within this 19 | implementation of Go constitutes direct or contributory patent 20 | infringement, or inducement of patent infringement, then any patent 21 | rights granted to you under this License for this implementation of Go 22 | shall terminate as of the date such litigation is filed. 23 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/websocket/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package websocket 6 | 7 | import ( 8 | "bufio" 9 | "crypto/tls" 10 | "io" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | ) 15 | 16 | // DialError is an error that occurs while dialling a websocket server. 17 | type DialError struct { 18 | *Config 19 | Err error 20 | } 21 | 22 | func (e *DialError) Error() string { 23 | return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error() 24 | } 25 | 26 | // NewConfig creates a new WebSocket config for client connection. 27 | func NewConfig(server, origin string) (config *Config, err error) { 28 | config = new(Config) 29 | config.Version = ProtocolVersionHybi13 30 | config.Location, err = url.ParseRequestURI(server) 31 | if err != nil { 32 | return 33 | } 34 | config.Origin, err = url.ParseRequestURI(origin) 35 | if err != nil { 36 | return 37 | } 38 | config.Header = http.Header(make(map[string][]string)) 39 | return 40 | } 41 | 42 | // NewClient creates a new WebSocket client connection over rwc. 43 | func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) { 44 | br := bufio.NewReader(rwc) 45 | bw := bufio.NewWriter(rwc) 46 | err = hybiClientHandshake(config, br, bw) 47 | if err != nil { 48 | return 49 | } 50 | buf := bufio.NewReadWriter(br, bw) 51 | ws = newHybiClientConn(config, buf, rwc) 52 | return 53 | } 54 | 55 | // Dial opens a new client connection to a WebSocket. 56 | func Dial(url_, protocol, origin string) (ws *Conn, err error) { 57 | config, err := NewConfig(url_, origin) 58 | if err != nil { 59 | return nil, err 60 | } 61 | if protocol != "" { 62 | config.Protocol = []string{protocol} 63 | } 64 | return DialConfig(config) 65 | } 66 | 67 | var portMap = map[string]string{ 68 | "ws": "80", 69 | "wss": "443", 70 | } 71 | 72 | func parseAuthority(location *url.URL) string { 73 | if _, ok := portMap[location.Scheme]; ok { 74 | if _, _, err := net.SplitHostPort(location.Host); err != nil { 75 | return net.JoinHostPort(location.Host, portMap[location.Scheme]) 76 | } 77 | } 78 | return location.Host 79 | } 80 | 81 | // DialConfig opens a new client connection to a WebSocket with a config. 82 | func DialConfig(config *Config) (ws *Conn, err error) { 83 | var client net.Conn 84 | if config.Location == nil { 85 | return nil, &DialError{config, ErrBadWebSocketLocation} 86 | } 87 | if config.Origin == nil { 88 | return nil, &DialError{config, ErrBadWebSocketOrigin} 89 | } 90 | switch config.Location.Scheme { 91 | case "ws": 92 | client, err = net.Dial("tcp", parseAuthority(config.Location)) 93 | 94 | case "wss": 95 | client, err = tls.Dial("tcp", parseAuthority(config.Location), config.TlsConfig) 96 | 97 | default: 98 | err = ErrBadScheme 99 | } 100 | if err != nil { 101 | goto Error 102 | } 103 | 104 | ws, err = NewClient(config, client) 105 | if err != nil { 106 | client.Close() 107 | goto Error 108 | } 109 | return 110 | 111 | Error: 112 | return nil, &DialError{config, err} 113 | } 114 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/websocket/hybi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package websocket 6 | 7 | // This file implements a protocol of hybi draft. 8 | // http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 9 | 10 | import ( 11 | "bufio" 12 | "bytes" 13 | "crypto/rand" 14 | "crypto/sha1" 15 | "encoding/base64" 16 | "encoding/binary" 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "net/http" 21 | "net/url" 22 | "strings" 23 | ) 24 | 25 | const ( 26 | websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 27 | 28 | closeStatusNormal = 1000 29 | closeStatusGoingAway = 1001 30 | closeStatusProtocolError = 1002 31 | closeStatusUnsupportedData = 1003 32 | closeStatusFrameTooLarge = 1004 33 | closeStatusNoStatusRcvd = 1005 34 | closeStatusAbnormalClosure = 1006 35 | closeStatusBadMessageData = 1007 36 | closeStatusPolicyViolation = 1008 37 | closeStatusTooBigData = 1009 38 | closeStatusExtensionMismatch = 1010 39 | 40 | maxControlFramePayloadLength = 125 41 | ) 42 | 43 | var ( 44 | ErrBadMaskingKey = &ProtocolError{"bad masking key"} 45 | ErrBadPongMessage = &ProtocolError{"bad pong message"} 46 | ErrBadClosingStatus = &ProtocolError{"bad closing status"} 47 | ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"} 48 | ErrNotImplemented = &ProtocolError{"not implemented"} 49 | 50 | handshakeHeader = map[string]bool{ 51 | "Host": true, 52 | "Upgrade": true, 53 | "Connection": true, 54 | "Sec-Websocket-Key": true, 55 | "Sec-Websocket-Origin": true, 56 | "Sec-Websocket-Version": true, 57 | "Sec-Websocket-Protocol": true, 58 | "Sec-Websocket-Accept": true, 59 | } 60 | ) 61 | 62 | // A hybiFrameHeader is a frame header as defined in hybi draft. 63 | type hybiFrameHeader struct { 64 | Fin bool 65 | Rsv [3]bool 66 | OpCode byte 67 | Length int64 68 | MaskingKey []byte 69 | 70 | data *bytes.Buffer 71 | } 72 | 73 | // A hybiFrameReader is a reader for hybi frame. 74 | type hybiFrameReader struct { 75 | reader io.Reader 76 | 77 | header hybiFrameHeader 78 | pos int64 79 | length int 80 | } 81 | 82 | func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) { 83 | n, err = frame.reader.Read(msg) 84 | if err != nil { 85 | return 0, err 86 | } 87 | if frame.header.MaskingKey != nil { 88 | for i := 0; i < n; i++ { 89 | msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4] 90 | frame.pos++ 91 | } 92 | } 93 | return n, err 94 | } 95 | 96 | func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode } 97 | 98 | func (frame *hybiFrameReader) HeaderReader() io.Reader { 99 | if frame.header.data == nil { 100 | return nil 101 | } 102 | if frame.header.data.Len() == 0 { 103 | return nil 104 | } 105 | return frame.header.data 106 | } 107 | 108 | func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil } 109 | 110 | func (frame *hybiFrameReader) Len() (n int) { return frame.length } 111 | 112 | // A hybiFrameReaderFactory creates new frame reader based on its frame type. 113 | type hybiFrameReaderFactory struct { 114 | *bufio.Reader 115 | } 116 | 117 | // NewFrameReader reads a frame header from the connection, and creates new reader for the frame. 118 | // See Section 5.2 Base Framing protocol for detail. 119 | // http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2 120 | func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) { 121 | hybiFrame := new(hybiFrameReader) 122 | frame = hybiFrame 123 | var header []byte 124 | var b byte 125 | // First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits) 126 | b, err = buf.ReadByte() 127 | if err != nil { 128 | return 129 | } 130 | header = append(header, b) 131 | hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0 132 | for i := 0; i < 3; i++ { 133 | j := uint(6 - i) 134 | hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0 135 | } 136 | hybiFrame.header.OpCode = header[0] & 0x0f 137 | 138 | // Second byte. Mask/Payload len(7bits) 139 | b, err = buf.ReadByte() 140 | if err != nil { 141 | return 142 | } 143 | header = append(header, b) 144 | mask := (b & 0x80) != 0 145 | b &= 0x7f 146 | lengthFields := 0 147 | switch { 148 | case b <= 125: // Payload length 7bits. 149 | hybiFrame.header.Length = int64(b) 150 | case b == 126: // Payload length 7+16bits 151 | lengthFields = 2 152 | case b == 127: // Payload length 7+64bits 153 | lengthFields = 8 154 | } 155 | for i := 0; i < lengthFields; i++ { 156 | b, err = buf.ReadByte() 157 | if err != nil { 158 | return 159 | } 160 | if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits 161 | b &= 0x7f 162 | } 163 | header = append(header, b) 164 | hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b) 165 | } 166 | if mask { 167 | // Masking key. 4 bytes. 168 | for i := 0; i < 4; i++ { 169 | b, err = buf.ReadByte() 170 | if err != nil { 171 | return 172 | } 173 | header = append(header, b) 174 | hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b) 175 | } 176 | } 177 | hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length) 178 | hybiFrame.header.data = bytes.NewBuffer(header) 179 | hybiFrame.length = len(header) + int(hybiFrame.header.Length) 180 | return 181 | } 182 | 183 | // A HybiFrameWriter is a writer for hybi frame. 184 | type hybiFrameWriter struct { 185 | writer *bufio.Writer 186 | 187 | header *hybiFrameHeader 188 | } 189 | 190 | func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) { 191 | var header []byte 192 | var b byte 193 | if frame.header.Fin { 194 | b |= 0x80 195 | } 196 | for i := 0; i < 3; i++ { 197 | if frame.header.Rsv[i] { 198 | j := uint(6 - i) 199 | b |= 1 << j 200 | } 201 | } 202 | b |= frame.header.OpCode 203 | header = append(header, b) 204 | if frame.header.MaskingKey != nil { 205 | b = 0x80 206 | } else { 207 | b = 0 208 | } 209 | lengthFields := 0 210 | length := len(msg) 211 | switch { 212 | case length <= 125: 213 | b |= byte(length) 214 | case length < 65536: 215 | b |= 126 216 | lengthFields = 2 217 | default: 218 | b |= 127 219 | lengthFields = 8 220 | } 221 | header = append(header, b) 222 | for i := 0; i < lengthFields; i++ { 223 | j := uint((lengthFields - i - 1) * 8) 224 | b = byte((length >> j) & 0xff) 225 | header = append(header, b) 226 | } 227 | if frame.header.MaskingKey != nil { 228 | if len(frame.header.MaskingKey) != 4 { 229 | return 0, ErrBadMaskingKey 230 | } 231 | header = append(header, frame.header.MaskingKey...) 232 | frame.writer.Write(header) 233 | data := make([]byte, length) 234 | for i := range data { 235 | data[i] = msg[i] ^ frame.header.MaskingKey[i%4] 236 | } 237 | frame.writer.Write(data) 238 | err = frame.writer.Flush() 239 | return length, err 240 | } 241 | frame.writer.Write(header) 242 | frame.writer.Write(msg) 243 | err = frame.writer.Flush() 244 | return length, err 245 | } 246 | 247 | func (frame *hybiFrameWriter) Close() error { return nil } 248 | 249 | type hybiFrameWriterFactory struct { 250 | *bufio.Writer 251 | needMaskingKey bool 252 | } 253 | 254 | func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) { 255 | frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType} 256 | if buf.needMaskingKey { 257 | frameHeader.MaskingKey, err = generateMaskingKey() 258 | if err != nil { 259 | return nil, err 260 | } 261 | } 262 | return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil 263 | } 264 | 265 | type hybiFrameHandler struct { 266 | conn *Conn 267 | payloadType byte 268 | } 269 | 270 | func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) { 271 | if handler.conn.IsServerConn() { 272 | // The client MUST mask all frames sent to the server. 273 | if frame.(*hybiFrameReader).header.MaskingKey == nil { 274 | handler.WriteClose(closeStatusProtocolError) 275 | return nil, io.EOF 276 | } 277 | } else { 278 | // The server MUST NOT mask all frames. 279 | if frame.(*hybiFrameReader).header.MaskingKey != nil { 280 | handler.WriteClose(closeStatusProtocolError) 281 | return nil, io.EOF 282 | } 283 | } 284 | if header := frame.HeaderReader(); header != nil { 285 | io.Copy(ioutil.Discard, header) 286 | } 287 | switch frame.PayloadType() { 288 | case ContinuationFrame: 289 | frame.(*hybiFrameReader).header.OpCode = handler.payloadType 290 | case TextFrame, BinaryFrame: 291 | handler.payloadType = frame.PayloadType() 292 | case CloseFrame: 293 | return nil, io.EOF 294 | case PingFrame, PongFrame: 295 | b := make([]byte, maxControlFramePayloadLength) 296 | n, err := io.ReadFull(frame, b) 297 | if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { 298 | return nil, err 299 | } 300 | io.Copy(ioutil.Discard, frame) 301 | if frame.PayloadType() == PingFrame { 302 | if _, err := handler.WritePong(b[:n]); err != nil { 303 | return nil, err 304 | } 305 | } 306 | return nil, nil 307 | } 308 | return frame, nil 309 | } 310 | 311 | func (handler *hybiFrameHandler) WriteClose(status int) (err error) { 312 | handler.conn.wio.Lock() 313 | defer handler.conn.wio.Unlock() 314 | w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame) 315 | if err != nil { 316 | return err 317 | } 318 | msg := make([]byte, 2) 319 | binary.BigEndian.PutUint16(msg, uint16(status)) 320 | _, err = w.Write(msg) 321 | w.Close() 322 | return err 323 | } 324 | 325 | func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) { 326 | handler.conn.wio.Lock() 327 | defer handler.conn.wio.Unlock() 328 | w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame) 329 | if err != nil { 330 | return 0, err 331 | } 332 | n, err = w.Write(msg) 333 | w.Close() 334 | return n, err 335 | } 336 | 337 | // newHybiConn creates a new WebSocket connection speaking hybi draft protocol. 338 | func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 339 | if buf == nil { 340 | br := bufio.NewReader(rwc) 341 | bw := bufio.NewWriter(rwc) 342 | buf = bufio.NewReadWriter(br, bw) 343 | } 344 | ws := &Conn{config: config, request: request, buf: buf, rwc: rwc, 345 | frameReaderFactory: hybiFrameReaderFactory{buf.Reader}, 346 | frameWriterFactory: hybiFrameWriterFactory{ 347 | buf.Writer, request == nil}, 348 | PayloadType: TextFrame, 349 | defaultCloseStatus: closeStatusNormal} 350 | ws.frameHandler = &hybiFrameHandler{conn: ws} 351 | return ws 352 | } 353 | 354 | // generateMaskingKey generates a masking key for a frame. 355 | func generateMaskingKey() (maskingKey []byte, err error) { 356 | maskingKey = make([]byte, 4) 357 | if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil { 358 | return 359 | } 360 | return 361 | } 362 | 363 | // generateNonce generates a nonce consisting of a randomly selected 16-byte 364 | // value that has been base64-encoded. 365 | func generateNonce() (nonce []byte) { 366 | key := make([]byte, 16) 367 | if _, err := io.ReadFull(rand.Reader, key); err != nil { 368 | panic(err) 369 | } 370 | nonce = make([]byte, 24) 371 | base64.StdEncoding.Encode(nonce, key) 372 | return 373 | } 374 | 375 | // removeZone removes IPv6 zone identifer from host. 376 | // E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080" 377 | func removeZone(host string) string { 378 | if !strings.HasPrefix(host, "[") { 379 | return host 380 | } 381 | i := strings.LastIndex(host, "]") 382 | if i < 0 { 383 | return host 384 | } 385 | j := strings.LastIndex(host[:i], "%") 386 | if j < 0 { 387 | return host 388 | } 389 | return host[:j] + host[i:] 390 | } 391 | 392 | // getNonceAccept computes the base64-encoded SHA-1 of the concatenation of 393 | // the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string. 394 | func getNonceAccept(nonce []byte) (expected []byte, err error) { 395 | h := sha1.New() 396 | if _, err = h.Write(nonce); err != nil { 397 | return 398 | } 399 | if _, err = h.Write([]byte(websocketGUID)); err != nil { 400 | return 401 | } 402 | expected = make([]byte, 28) 403 | base64.StdEncoding.Encode(expected, h.Sum(nil)) 404 | return 405 | } 406 | 407 | // Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17 408 | func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { 409 | bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") 410 | 411 | // According to RFC 6874, an HTTP client, proxy, or other 412 | // intermediary must remove any IPv6 zone identifier attached 413 | // to an outgoing URI. 414 | bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n") 415 | bw.WriteString("Upgrade: websocket\r\n") 416 | bw.WriteString("Connection: Upgrade\r\n") 417 | nonce := generateNonce() 418 | if config.handshakeData != nil { 419 | nonce = []byte(config.handshakeData["key"]) 420 | } 421 | bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n") 422 | bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") 423 | 424 | if config.Version != ProtocolVersionHybi13 { 425 | return ErrBadProtocolVersion 426 | } 427 | 428 | bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n") 429 | if len(config.Protocol) > 0 { 430 | bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n") 431 | } 432 | // TODO(ukai): send Sec-WebSocket-Extensions. 433 | err = config.Header.WriteSubset(bw, handshakeHeader) 434 | if err != nil { 435 | return err 436 | } 437 | 438 | bw.WriteString("\r\n") 439 | if err = bw.Flush(); err != nil { 440 | return err 441 | } 442 | 443 | resp, err := http.ReadResponse(br, &http.Request{Method: "GET"}) 444 | if err != nil { 445 | return err 446 | } 447 | if resp.StatusCode != 101 { 448 | return ErrBadStatus 449 | } 450 | if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" || 451 | strings.ToLower(resp.Header.Get("Connection")) != "upgrade" { 452 | return ErrBadUpgrade 453 | } 454 | expectedAccept, err := getNonceAccept(nonce) 455 | if err != nil { 456 | return err 457 | } 458 | if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) { 459 | return ErrChallengeResponse 460 | } 461 | if resp.Header.Get("Sec-WebSocket-Extensions") != "" { 462 | return ErrUnsupportedExtensions 463 | } 464 | offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol") 465 | if offeredProtocol != "" { 466 | protocolMatched := false 467 | for i := 0; i < len(config.Protocol); i++ { 468 | if config.Protocol[i] == offeredProtocol { 469 | protocolMatched = true 470 | break 471 | } 472 | } 473 | if !protocolMatched { 474 | return ErrBadWebSocketProtocol 475 | } 476 | config.Protocol = []string{offeredProtocol} 477 | } 478 | 479 | return nil 480 | } 481 | 482 | // newHybiClientConn creates a client WebSocket connection after handshake. 483 | func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn { 484 | return newHybiConn(config, buf, rwc, nil) 485 | } 486 | 487 | // A HybiServerHandshaker performs a server handshake using hybi draft protocol. 488 | type hybiServerHandshaker struct { 489 | *Config 490 | accept []byte 491 | } 492 | 493 | func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) { 494 | c.Version = ProtocolVersionHybi13 495 | if req.Method != "GET" { 496 | return http.StatusMethodNotAllowed, ErrBadRequestMethod 497 | } 498 | // HTTP version can be safely ignored. 499 | 500 | if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" || 501 | !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") { 502 | return http.StatusBadRequest, ErrNotWebSocket 503 | } 504 | 505 | key := req.Header.Get("Sec-Websocket-Key") 506 | if key == "" { 507 | return http.StatusBadRequest, ErrChallengeResponse 508 | } 509 | version := req.Header.Get("Sec-Websocket-Version") 510 | switch version { 511 | case "13": 512 | c.Version = ProtocolVersionHybi13 513 | default: 514 | return http.StatusBadRequest, ErrBadWebSocketVersion 515 | } 516 | var scheme string 517 | if req.TLS != nil { 518 | scheme = "wss" 519 | } else { 520 | scheme = "ws" 521 | } 522 | c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI()) 523 | if err != nil { 524 | return http.StatusBadRequest, err 525 | } 526 | protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol")) 527 | if protocol != "" { 528 | protocols := strings.Split(protocol, ",") 529 | for i := 0; i < len(protocols); i++ { 530 | c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i])) 531 | } 532 | } 533 | c.accept, err = getNonceAccept([]byte(key)) 534 | if err != nil { 535 | return http.StatusInternalServerError, err 536 | } 537 | return http.StatusSwitchingProtocols, nil 538 | } 539 | 540 | // Origin parses the Origin header in req. 541 | // If the Origin header is not set, it returns nil and nil. 542 | func Origin(config *Config, req *http.Request) (*url.URL, error) { 543 | var origin string 544 | switch config.Version { 545 | case ProtocolVersionHybi13: 546 | origin = req.Header.Get("Origin") 547 | } 548 | if origin == "" { 549 | return nil, nil 550 | } 551 | return url.ParseRequestURI(origin) 552 | } 553 | 554 | func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) { 555 | if len(c.Protocol) > 0 { 556 | if len(c.Protocol) != 1 { 557 | // You need choose a Protocol in Handshake func in Server. 558 | return ErrBadWebSocketProtocol 559 | } 560 | } 561 | buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n") 562 | buf.WriteString("Upgrade: websocket\r\n") 563 | buf.WriteString("Connection: Upgrade\r\n") 564 | buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n") 565 | if len(c.Protocol) > 0 { 566 | buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n") 567 | } 568 | // TODO(ukai): send Sec-WebSocket-Extensions. 569 | if c.Header != nil { 570 | err := c.Header.WriteSubset(buf, handshakeHeader) 571 | if err != nil { 572 | return err 573 | } 574 | } 575 | buf.WriteString("\r\n") 576 | return buf.Flush() 577 | } 578 | 579 | func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 580 | return newHybiServerConn(c.Config, buf, rwc, request) 581 | } 582 | 583 | // newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol. 584 | func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 585 | return newHybiConn(config, buf, rwc, request) 586 | } 587 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/websocket/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package websocket 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | ) 13 | 14 | func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) { 15 | var hs serverHandshaker = &hybiServerHandshaker{Config: config} 16 | code, err := hs.ReadHandshake(buf.Reader, req) 17 | if err == ErrBadWebSocketVersion { 18 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 19 | fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion) 20 | buf.WriteString("\r\n") 21 | buf.WriteString(err.Error()) 22 | buf.Flush() 23 | return 24 | } 25 | if err != nil { 26 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 27 | buf.WriteString("\r\n") 28 | buf.WriteString(err.Error()) 29 | buf.Flush() 30 | return 31 | } 32 | if handshake != nil { 33 | err = handshake(config, req) 34 | if err != nil { 35 | code = http.StatusForbidden 36 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 37 | buf.WriteString("\r\n") 38 | buf.Flush() 39 | return 40 | } 41 | } 42 | err = hs.AcceptHandshake(buf.Writer) 43 | if err != nil { 44 | code = http.StatusBadRequest 45 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 46 | buf.WriteString("\r\n") 47 | buf.Flush() 48 | return 49 | } 50 | conn = hs.NewServerConn(buf, rwc, req) 51 | return 52 | } 53 | 54 | // Server represents a server of a WebSocket. 55 | type Server struct { 56 | // Config is a WebSocket configuration for new WebSocket connection. 57 | Config 58 | 59 | // Handshake is an optional function in WebSocket handshake. 60 | // For example, you can check, or don't check Origin header. 61 | // Another example, you can select config.Protocol. 62 | Handshake func(*Config, *http.Request) error 63 | 64 | // Handler handles a WebSocket connection. 65 | Handler 66 | } 67 | 68 | // ServeHTTP implements the http.Handler interface for a WebSocket 69 | func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { 70 | s.serveWebSocket(w, req) 71 | } 72 | 73 | func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { 74 | rwc, buf, err := w.(http.Hijacker).Hijack() 75 | if err != nil { 76 | panic("Hijack failed: " + err.Error()) 77 | } 78 | // The server should abort the WebSocket connection if it finds 79 | // the client did not send a handshake that matches with protocol 80 | // specification. 81 | defer rwc.Close() 82 | conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) 83 | if err != nil { 84 | return 85 | } 86 | if conn == nil { 87 | panic("unexpected nil conn") 88 | } 89 | s.Handler(conn) 90 | } 91 | 92 | // Handler is a simple interface to a WebSocket browser client. 93 | // It checks if Origin header is valid URL by default. 94 | // You might want to verify websocket.Conn.Config().Origin in the func. 95 | // If you use Server instead of Handler, you could call websocket.Origin and 96 | // check the origin in your Handshake func. So, if you want to accept 97 | // non-browser clients, which do not send an Origin header, set a 98 | // Server.Handshake that does not check the origin. 99 | type Handler func(*Conn) 100 | 101 | func checkOrigin(config *Config, req *http.Request) (err error) { 102 | config.Origin, err = Origin(config, req) 103 | if err == nil && config.Origin == nil { 104 | return fmt.Errorf("null origin") 105 | } 106 | return err 107 | } 108 | 109 | // ServeHTTP implements the http.Handler interface for a WebSocket 110 | func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 111 | s := Server{Handler: h, Handshake: checkOrigin} 112 | s.serveWebSocket(w, req) 113 | } 114 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/websocket/websocket.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package websocket implements a client and server for the WebSocket protocol 6 | // as specified in RFC 6455. 7 | package websocket 8 | 9 | import ( 10 | "bufio" 11 | "crypto/tls" 12 | "encoding/json" 13 | "errors" 14 | "io" 15 | "io/ioutil" 16 | "net" 17 | "net/http" 18 | "net/url" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | const ( 24 | ProtocolVersionHybi13 = 13 25 | ProtocolVersionHybi = ProtocolVersionHybi13 26 | SupportedProtocolVersion = "13" 27 | 28 | ContinuationFrame = 0 29 | TextFrame = 1 30 | BinaryFrame = 2 31 | CloseFrame = 8 32 | PingFrame = 9 33 | PongFrame = 10 34 | UnknownFrame = 255 35 | ) 36 | 37 | // ProtocolError represents WebSocket protocol errors. 38 | type ProtocolError struct { 39 | ErrorString string 40 | } 41 | 42 | func (err *ProtocolError) Error() string { return err.ErrorString } 43 | 44 | var ( 45 | ErrBadProtocolVersion = &ProtocolError{"bad protocol version"} 46 | ErrBadScheme = &ProtocolError{"bad scheme"} 47 | ErrBadStatus = &ProtocolError{"bad status"} 48 | ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"} 49 | ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"} 50 | ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"} 51 | ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"} 52 | ErrBadWebSocketVersion = &ProtocolError{"missing or bad WebSocket Version"} 53 | ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"} 54 | ErrBadFrame = &ProtocolError{"bad frame"} 55 | ErrBadFrameBoundary = &ProtocolError{"not on frame boundary"} 56 | ErrNotWebSocket = &ProtocolError{"not websocket protocol"} 57 | ErrBadRequestMethod = &ProtocolError{"bad method"} 58 | ErrNotSupported = &ProtocolError{"not supported"} 59 | ) 60 | 61 | // Addr is an implementation of net.Addr for WebSocket. 62 | type Addr struct { 63 | *url.URL 64 | } 65 | 66 | // Network returns the network type for a WebSocket, "websocket". 67 | func (addr *Addr) Network() string { return "websocket" } 68 | 69 | // Config is a WebSocket configuration 70 | type Config struct { 71 | // A WebSocket server address. 72 | Location *url.URL 73 | 74 | // A Websocket client origin. 75 | Origin *url.URL 76 | 77 | // WebSocket subprotocols. 78 | Protocol []string 79 | 80 | // WebSocket protocol version. 81 | Version int 82 | 83 | // TLS config for secure WebSocket (wss). 84 | TlsConfig *tls.Config 85 | 86 | // Additional header fields to be sent in WebSocket opening handshake. 87 | Header http.Header 88 | 89 | handshakeData map[string]string 90 | } 91 | 92 | // serverHandshaker is an interface to handle WebSocket server side handshake. 93 | type serverHandshaker interface { 94 | // ReadHandshake reads handshake request message from client. 95 | // Returns http response code and error if any. 96 | ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) 97 | 98 | // AcceptHandshake accepts the client handshake request and sends 99 | // handshake response back to client. 100 | AcceptHandshake(buf *bufio.Writer) (err error) 101 | 102 | // NewServerConn creates a new WebSocket connection. 103 | NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) (conn *Conn) 104 | } 105 | 106 | // frameReader is an interface to read a WebSocket frame. 107 | type frameReader interface { 108 | // Reader is to read payload of the frame. 109 | io.Reader 110 | 111 | // PayloadType returns payload type. 112 | PayloadType() byte 113 | 114 | // HeaderReader returns a reader to read header of the frame. 115 | HeaderReader() io.Reader 116 | 117 | // TrailerReader returns a reader to read trailer of the frame. 118 | // If it returns nil, there is no trailer in the frame. 119 | TrailerReader() io.Reader 120 | 121 | // Len returns total length of the frame, including header and trailer. 122 | Len() int 123 | } 124 | 125 | // frameReaderFactory is an interface to creates new frame reader. 126 | type frameReaderFactory interface { 127 | NewFrameReader() (r frameReader, err error) 128 | } 129 | 130 | // frameWriter is an interface to write a WebSocket frame. 131 | type frameWriter interface { 132 | // Writer is to write payload of the frame. 133 | io.WriteCloser 134 | } 135 | 136 | // frameWriterFactory is an interface to create new frame writer. 137 | type frameWriterFactory interface { 138 | NewFrameWriter(payloadType byte) (w frameWriter, err error) 139 | } 140 | 141 | type frameHandler interface { 142 | HandleFrame(frame frameReader) (r frameReader, err error) 143 | WriteClose(status int) (err error) 144 | } 145 | 146 | // Conn represents a WebSocket connection. 147 | type Conn struct { 148 | config *Config 149 | request *http.Request 150 | 151 | buf *bufio.ReadWriter 152 | rwc io.ReadWriteCloser 153 | 154 | rio sync.Mutex 155 | frameReaderFactory 156 | frameReader 157 | 158 | wio sync.Mutex 159 | frameWriterFactory 160 | 161 | frameHandler 162 | PayloadType byte 163 | defaultCloseStatus int 164 | } 165 | 166 | // Read implements the io.Reader interface: 167 | // it reads data of a frame from the WebSocket connection. 168 | // if msg is not large enough for the frame data, it fills the msg and next Read 169 | // will read the rest of the frame data. 170 | // it reads Text frame or Binary frame. 171 | func (ws *Conn) Read(msg []byte) (n int, err error) { 172 | ws.rio.Lock() 173 | defer ws.rio.Unlock() 174 | again: 175 | if ws.frameReader == nil { 176 | frame, err := ws.frameReaderFactory.NewFrameReader() 177 | if err != nil { 178 | return 0, err 179 | } 180 | ws.frameReader, err = ws.frameHandler.HandleFrame(frame) 181 | if err != nil { 182 | return 0, err 183 | } 184 | if ws.frameReader == nil { 185 | goto again 186 | } 187 | } 188 | n, err = ws.frameReader.Read(msg) 189 | if err == io.EOF { 190 | if trailer := ws.frameReader.TrailerReader(); trailer != nil { 191 | io.Copy(ioutil.Discard, trailer) 192 | } 193 | ws.frameReader = nil 194 | goto again 195 | } 196 | return n, err 197 | } 198 | 199 | // Write implements the io.Writer interface: 200 | // it writes data as a frame to the WebSocket connection. 201 | func (ws *Conn) Write(msg []byte) (n int, err error) { 202 | ws.wio.Lock() 203 | defer ws.wio.Unlock() 204 | w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType) 205 | if err != nil { 206 | return 0, err 207 | } 208 | n, err = w.Write(msg) 209 | w.Close() 210 | if err != nil { 211 | return n, err 212 | } 213 | return n, err 214 | } 215 | 216 | // Close implements the io.Closer interface. 217 | func (ws *Conn) Close() error { 218 | err := ws.frameHandler.WriteClose(ws.defaultCloseStatus) 219 | err1 := ws.rwc.Close() 220 | if err != nil { 221 | return err 222 | } 223 | return err1 224 | } 225 | 226 | func (ws *Conn) IsClientConn() bool { return ws.request == nil } 227 | func (ws *Conn) IsServerConn() bool { return ws.request != nil } 228 | 229 | // LocalAddr returns the WebSocket Origin for the connection for client, or 230 | // the WebSocket location for server. 231 | func (ws *Conn) LocalAddr() net.Addr { 232 | if ws.IsClientConn() { 233 | return &Addr{ws.config.Origin} 234 | } 235 | return &Addr{ws.config.Location} 236 | } 237 | 238 | // RemoteAddr returns the WebSocket location for the connection for client, or 239 | // the Websocket Origin for server. 240 | func (ws *Conn) RemoteAddr() net.Addr { 241 | if ws.IsClientConn() { 242 | return &Addr{ws.config.Location} 243 | } 244 | return &Addr{ws.config.Origin} 245 | } 246 | 247 | var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn") 248 | 249 | // SetDeadline sets the connection's network read & write deadlines. 250 | func (ws *Conn) SetDeadline(t time.Time) error { 251 | if conn, ok := ws.rwc.(net.Conn); ok { 252 | return conn.SetDeadline(t) 253 | } 254 | return errSetDeadline 255 | } 256 | 257 | // SetReadDeadline sets the connection's network read deadline. 258 | func (ws *Conn) SetReadDeadline(t time.Time) error { 259 | if conn, ok := ws.rwc.(net.Conn); ok { 260 | return conn.SetReadDeadline(t) 261 | } 262 | return errSetDeadline 263 | } 264 | 265 | // SetWriteDeadline sets the connection's network write deadline. 266 | func (ws *Conn) SetWriteDeadline(t time.Time) error { 267 | if conn, ok := ws.rwc.(net.Conn); ok { 268 | return conn.SetWriteDeadline(t) 269 | } 270 | return errSetDeadline 271 | } 272 | 273 | // Config returns the WebSocket config. 274 | func (ws *Conn) Config() *Config { return ws.config } 275 | 276 | // Request returns the http request upgraded to the WebSocket. 277 | // It is nil for client side. 278 | func (ws *Conn) Request() *http.Request { return ws.request } 279 | 280 | // Codec represents a symmetric pair of functions that implement a codec. 281 | type Codec struct { 282 | Marshal func(v interface{}) (data []byte, payloadType byte, err error) 283 | Unmarshal func(data []byte, payloadType byte, v interface{}) (err error) 284 | } 285 | 286 | // Send sends v marshaled by cd.Marshal as single frame to ws. 287 | func (cd Codec) Send(ws *Conn, v interface{}) (err error) { 288 | data, payloadType, err := cd.Marshal(v) 289 | if err != nil { 290 | return err 291 | } 292 | ws.wio.Lock() 293 | defer ws.wio.Unlock() 294 | w, err := ws.frameWriterFactory.NewFrameWriter(payloadType) 295 | if err != nil { 296 | return err 297 | } 298 | _, err = w.Write(data) 299 | w.Close() 300 | return err 301 | } 302 | 303 | // Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores in v. 304 | func (cd Codec) Receive(ws *Conn, v interface{}) (err error) { 305 | ws.rio.Lock() 306 | defer ws.rio.Unlock() 307 | if ws.frameReader != nil { 308 | _, err = io.Copy(ioutil.Discard, ws.frameReader) 309 | if err != nil { 310 | return err 311 | } 312 | ws.frameReader = nil 313 | } 314 | again: 315 | frame, err := ws.frameReaderFactory.NewFrameReader() 316 | if err != nil { 317 | return err 318 | } 319 | frame, err = ws.frameHandler.HandleFrame(frame) 320 | if err != nil { 321 | return err 322 | } 323 | if frame == nil { 324 | goto again 325 | } 326 | payloadType := frame.PayloadType() 327 | data, err := ioutil.ReadAll(frame) 328 | if err != nil { 329 | return err 330 | } 331 | return cd.Unmarshal(data, payloadType, v) 332 | } 333 | 334 | func marshal(v interface{}) (msg []byte, payloadType byte, err error) { 335 | switch data := v.(type) { 336 | case string: 337 | return []byte(data), TextFrame, nil 338 | case []byte: 339 | return data, BinaryFrame, nil 340 | } 341 | return nil, UnknownFrame, ErrNotSupported 342 | } 343 | 344 | func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) { 345 | switch data := v.(type) { 346 | case *string: 347 | *data = string(msg) 348 | return nil 349 | case *[]byte: 350 | *data = msg 351 | return nil 352 | } 353 | return ErrNotSupported 354 | } 355 | 356 | /* 357 | Message is a codec to send/receive text/binary data in a frame on WebSocket connection. 358 | To send/receive text frame, use string type. 359 | To send/receive binary frame, use []byte type. 360 | 361 | Trivial usage: 362 | 363 | import "websocket" 364 | 365 | // receive text frame 366 | var message string 367 | websocket.Message.Receive(ws, &message) 368 | 369 | // send text frame 370 | message = "hello" 371 | websocket.Message.Send(ws, message) 372 | 373 | // receive binary frame 374 | var data []byte 375 | websocket.Message.Receive(ws, &data) 376 | 377 | // send binary frame 378 | data = []byte{0, 1, 2} 379 | websocket.Message.Send(ws, data) 380 | 381 | */ 382 | var Message = Codec{marshal, unmarshal} 383 | 384 | func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) { 385 | msg, err = json.Marshal(v) 386 | return msg, TextFrame, err 387 | } 388 | 389 | func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) { 390 | return json.Unmarshal(msg, v) 391 | } 392 | 393 | /* 394 | JSON is a codec to send/receive JSON data in a frame from a WebSocket connection. 395 | 396 | Trivial usage: 397 | 398 | import "websocket" 399 | 400 | type T struct { 401 | Msg string 402 | Count int 403 | } 404 | 405 | // receive JSON type T 406 | var data T 407 | websocket.JSON.Receive(ws, &data) 408 | 409 | // send JSON type T 410 | websocket.JSON.Send(ws, data) 411 | */ 412 | var JSON = Codec{jsonMarshal, jsonUnmarshal} 413 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: wwdc-bot $SLACK_BOT_TOKEN -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/sger/wwdc-bot.svg?branch=master)](https://travis-ci.org/sger/wwdc-bot) 2 | [![Coverage Status](https://coveralls.io/repos/github/sger/wwdc-bot/badge.svg?branch=master)](https://coveralls.io/github/sger/wwdc-bot?branch=master) 3 | 4 | # WWDC Slack Bot 5 | 6 | Searchable full-text transcripts of WWDC sessions 7 | 8 | WWDC Slack Bot uses [https://github.com/ASCIIwwdc/asciiwwdc.com](https://github.com/ASCIIwwdc/asciiwwdc.com) API 9 | 10 | Usage: 11 | 12 | First create a new bot in Slack more info here [https://api.slack.com/bot-users](https://api.slack.com/bot-users) then get project with go get command: 13 | 14 | ``` 15 | $ go get https://github.com/sger/wwdc-bot 16 | $ ./wwdc-bot my-bot-token 17 | ``` 18 | open up your slack client and send a message to you bot 19 | for example find a session with id 101 and year 2015 20 | 21 | my-bot-name: 101 2015 22 | 23 | or search for uiview 24 | 25 | my-bot-name: uiview 26 | 27 | see all sessions here [http://asciiwwdc.com](http://asciiwwdc.com) 28 | 29 | maybe you want to deploy this go app to heroku from command line type: 30 | 31 | ``` 32 | $ heroku create 33 | $ git add . 34 | $ git commit -m "adding project to heroku service" 35 | $ git push heroku master 36 | ``` 37 | 38 | or 39 | 40 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 41 | 42 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wwdc-bot", 3 | "description": "Slack Bot written in Go", 4 | "keywords": [ 5 | "go", 6 | "slack" 7 | ], 8 | "image": "heroku/go", 9 | "mount_dir": "src/github.com/sger/wwdc-bot", 10 | "website": "", 11 | "repository": "https://github.com/sger/wwdc-bot", 12 | "env": { 13 | "SLACK_BOT_TOKEN": { 14 | "description": "Slack auth token. See https://api.slack.com/tokens", 15 | "value": "TOKEN" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func main() { 11 | 12 | if len(os.Args) != 2 { 13 | fmt.Fprintf(os.Stderr, "usage: wwdc-bot token\n @bot-name uiview or @bot-name 101 2015") 14 | os.Exit(1) 15 | } 16 | 17 | ws, r, err := Connect(os.Args[1]) 18 | fmt.Println("wwdc-bot is running hit ^C to exit") 19 | 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | for { 25 | m, err := GetMessage(ws) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | if m.Type == "message" && strings.HasPrefix(m.Text, "<@"+r.Self.Id+">") { 31 | fields := strings.Fields(m.Text) 32 | if len(fields) == 3 { 33 | go func(m Message) { 34 | 35 | resp, err := getSession(fields[1], fields[2]) 36 | if err != nil { 37 | m.Text = "no results" 38 | PostMessage(ws, m) 39 | } else { 40 | downloadUrl := fmt.Sprintf("https://developer.apple.com/videos/wwdc/%.0f/?id=%.0f", resp.Year, resp.Number) 41 | m.Text = "Title: " + resp.Title + "\nDescription: " + resp.Description + "\nDownload url: " + downloadUrl 42 | PostMessage(ws, m) 43 | } 44 | 45 | }(m) 46 | } else if len(fields) == 2 { 47 | go func(m Message) { 48 | 49 | resp, err := search(fields[1]) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | var result string 54 | 55 | if len(resp.Results) == 0 { 56 | m.Text = "no results" 57 | PostMessage(ws, m) 58 | } else { 59 | for _, element := range resp.Results[0:4] { 60 | downloadUrl := fmt.Sprintf("https://developer.apple.com/videos/wwdc/%.0f/?id=%.0f", element.Year, element.Number) 61 | result += element.Title + "\n" + element.Description + "\n" + downloadUrl + "\n\n" 62 | } 63 | m.Text = result 64 | PostMessage(ws, m) 65 | } 66 | }(m) 67 | } else { 68 | go func(m Message) { 69 | m.Text = "sorry " + "<@" + m.User + ">" + " wrong usage please write @" + r.Self.Name + ": 101 2015 or search @" + r.Self.Name + ": uiview \n 101 is the id of session and 2015 the year\nmore info here http://asciiwwdc.com" 70 | PostMessage(ws, m) 71 | }(m) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /slack-api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/sger/wwdc-bot/Godeps/_workspace/src/golang.org/x/net/websocket" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "sync/atomic" 11 | ) 12 | 13 | const URL_SLACK_API = "https://api.slack.com/" 14 | const URL_SLACK_API_RTM_START = "https://slack.com/api/rtm.start" 15 | 16 | var counter uint64 17 | 18 | type RtmStartResponse struct { 19 | Ok bool `json:"ok"` 20 | Error string `json:"error"` 21 | Url string `json:"url"` 22 | Self SelfResponse `json:"self"` 23 | } 24 | 25 | type SelfResponse struct { 26 | Id string `json:"id"` 27 | Name string `json:"name"` 28 | } 29 | 30 | type Message struct { 31 | Id uint64 `json:"id"` 32 | Type string `json:"type"` 33 | Channel string `json:"channel"` 34 | Text string `json:"text"` 35 | User string `json:"user"` 36 | } 37 | 38 | func GetMessage(ws *websocket.Conn) (m Message, err error) { 39 | err = websocket.JSON.Receive(ws, &m) 40 | return 41 | } 42 | 43 | func PostMessage(ws *websocket.Conn, m Message) error { 44 | m.Id = atomic.AddUint64(&counter, 1) 45 | return websocket.JSON.Send(ws, m) 46 | } 47 | 48 | func Connect(token string) (*websocket.Conn, *RtmStartResponse, error) { 49 | r, err := Start(token) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | ws, err := websocket.Dial(r.Url, "", URL_SLACK_API) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | return ws, r, err 60 | } 61 | 62 | func Start(token string) (*RtmStartResponse, error) { 63 | 64 | url := fmt.Sprintf(URL_SLACK_API_RTM_START+"?token=%s", token) 65 | resp, err := http.Get(url) 66 | 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | if resp.StatusCode != http.StatusOK { 72 | defer resp.Body.Close() 73 | } 74 | 75 | defer resp.Body.Close() 76 | 77 | body, err := ioutil.ReadAll(resp.Body) 78 | 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | var result RtmStartResponse 84 | 85 | err = json.Unmarshal(body, &result) 86 | 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | return &result, nil 92 | } 93 | -------------------------------------------------------------------------------- /wwdc-api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | const URL_ASCII_WWDC_API = "http://asciiwwdc.com" 11 | 12 | type SessionResponse struct { 13 | Title string `json:"title"` 14 | Description string `json:"description"` 15 | Year float64 `json:"year"` 16 | Number float64 `json:"number"` 17 | } 18 | 19 | type SearchResponse struct { 20 | Results []ResultsData `json:"results"` 21 | } 22 | 23 | type ResultsData struct { 24 | Title string `json:"title"` 25 | Description string `json:"description"` 26 | Year float64 `json:"year"` 27 | Number float64 `json:"number"` 28 | } 29 | 30 | func getSession(sessionID string, year string) (*SessionResponse, error) { 31 | 32 | url := fmt.Sprintf(URL_ASCII_WWDC_API+"/%s/sessions/%s", year, sessionID) 33 | 34 | client := &http.Client{} 35 | 36 | req, err := http.NewRequest("GET", url, nil) 37 | req.Header.Add("Accept", `application/json`) 38 | 39 | resp, err := client.Do(req) 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | if resp.StatusCode != http.StatusOK { 46 | defer resp.Body.Close() 47 | } 48 | 49 | defer resp.Body.Close() 50 | 51 | body, err := ioutil.ReadAll(resp.Body) 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | var result SessionResponse 58 | 59 | err = json.Unmarshal(body, &result) 60 | 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return &result, nil 66 | } 67 | 68 | func search(query string) (*SearchResponse, error) { 69 | url := fmt.Sprintf(URL_ASCII_WWDC_API+"/search?q=%s", query) 70 | 71 | client := &http.Client{} 72 | 73 | req, err := http.NewRequest("GET", url, nil) 74 | req.Header.Add("Accept", `application/json`) 75 | 76 | resp, err := client.Do(req) 77 | 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | if resp.StatusCode != http.StatusOK { 83 | defer resp.Body.Close() 84 | } 85 | 86 | defer resp.Body.Close() 87 | 88 | body, err := ioutil.ReadAll(resp.Body) 89 | 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | var result SearchResponse 95 | 96 | err = json.Unmarshal(body, &result) 97 | 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | return &result, nil 103 | } 104 | --------------------------------------------------------------------------------