├── .gitignore ├── Godeps ├── _workspace │ ├── .gitignore │ └── src │ │ ├── github.com │ │ └── nlopes │ │ │ └── slack │ │ │ ├── .gitignore │ │ │ ├── examples │ │ │ ├── files │ │ │ │ ├── example.txt │ │ │ │ └── files.go │ │ │ ├── users │ │ │ │ └── users.go │ │ │ ├── groups │ │ │ │ └── groups.go │ │ │ ├── messages │ │ │ │ └── messages.go │ │ │ └── websocket │ │ │ │ └── websocket.go │ │ │ ├── TODO.txt │ │ │ ├── config.go │ │ │ ├── websocket_stars.go │ │ │ ├── slack_test.go │ │ │ ├── websocket_dm.go │ │ │ ├── pagination.go │ │ │ ├── emoji.go │ │ │ ├── websocket_groups.go │ │ │ ├── websocket_teams.go │ │ │ ├── oauth.go │ │ │ ├── websocket_files.go │ │ │ ├── history.go │ │ │ ├── emoji_test.go │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── websocket_misc.go │ │ │ ├── slack.go │ │ │ ├── websocket_channels.go │ │ │ ├── admin.go │ │ │ ├── stars_test.go │ │ │ ├── stars.go │ │ │ ├── messages.go │ │ │ ├── misc_test.go │ │ │ ├── misc.go │ │ │ ├── dm.go │ │ │ ├── search.go │ │ │ ├── users.go │ │ │ ├── info.go │ │ │ ├── chat.go │ │ │ ├── files.go │ │ │ ├── websocket.go │ │ │ ├── channels.go │ │ │ └── groups.go │ │ └── golang.org │ │ └── x │ │ └── net │ │ └── websocket │ │ ├── examplehandler_test.go │ │ ├── exampledial_test.go │ │ ├── client.go │ │ ├── server.go │ │ ├── websocket_test.go │ │ ├── websocket.go │ │ ├── hybi.go │ │ └── hybi_test.go ├── Readme └── Godeps.json ├── app.json ├── LICENSE ├── README.md └── bot.go /.gitignore: -------------------------------------------------------------------------------- 1 | slack-archivebot 2 | -------------------------------------------------------------------------------- /Godeps/_workspace/.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | /bin 3 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/examples/files/example.txt: -------------------------------------------------------------------------------- 1 | Nan Nan Nan Nan Nan Nan Nan Nan Batman 2 | -------------------------------------------------------------------------------- /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/src/github.com/nlopes/slack/TODO.txt: -------------------------------------------------------------------------------- 1 | - Add more tests!!! 2 | - Add timeouts 3 | - Fix the Websocket mess 4 | - Add support to have markdown hints 5 | - See section Message Formatting at https://api.slack.com/docs/formatting 6 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/config.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // Config contains some config parameters needed 4 | // Token always needs to be set for the api to function 5 | // Origin and Protocol are optional and only needed for websocket 6 | type Config struct { 7 | token string 8 | origin string 9 | protocol string 10 | } 11 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/timblair/slack-archivebot", 3 | "GoVersion": "go1.4.2", 4 | "Deps": [ 5 | { 6 | "ImportPath": "github.com/nlopes/slack", 7 | "Rev": "1da32686c2e7143de764729a1d52732047f7aa84" 8 | }, 9 | { 10 | "ImportPath": "golang.org/x/net/websocket", 11 | "Rev": "d9558e5c97f85372afee28cf2b6059d7d3818919" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/websocket_stars.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type starEvent struct { 4 | Type string `json:"type"` 5 | UserId string `json:"user"` 6 | Item StarredItem `json:"item"` 7 | EventTimestamp JSONTimeString `json:"event_ts"` 8 | } 9 | type StarAddedEvent starEvent 10 | type StarRemovedEvent starEvent 11 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/examples/users/users.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nlopes/slack" 7 | ) 8 | 9 | func main() { 10 | api := slack.New("YOUR_TOKEN_HERE") 11 | user, err := api.GetUserInfo("U023BECGF") 12 | if err != nil { 13 | fmt.Printf("%s\n", err) 14 | return 15 | } 16 | fmt.Printf("Id: %s, Fullname: %s, Email: %s\n", user.Id, user.Profile.RealName, user.Profile.Email) 17 | } 18 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/slack_test.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "log" 5 | "net/http/httptest" 6 | "sync" 7 | ) 8 | 9 | const ( 10 | validToken = "testing-token" 11 | ) 12 | 13 | var ( 14 | serverAddr string 15 | once sync.Once 16 | ) 17 | 18 | func startServer() { 19 | server := httptest.NewServer(nil) 20 | serverAddr = server.Listener.Addr().String() 21 | log.Print("Test WebSocket server listening on ", serverAddr) 22 | } 23 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/websocket_dm.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type IMCreatedEvent struct { 4 | Type string `json:"type"` 5 | UserId string `json:"user"` 6 | Channel ChannelCreatedInfo `json:"channel"` 7 | } 8 | 9 | type IMHistoryChangedEvent ChannelHistoryChangedEvent 10 | type IMOpenEvent ChannelInfoEvent 11 | type IMCloseEvent ChannelInfoEvent 12 | type IMMarkedEvent ChannelInfoEvent 13 | type IMMarkedHistoryChanged ChannelInfoEvent 14 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/examples/groups/groups.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nlopes/slack" 7 | ) 8 | 9 | func main() { 10 | api := slack.New("YOUR_TOKEN_HERE") 11 | // If you set debugging, it will log all requests to the console 12 | // Useful when encountering issues 13 | // api.SetDebug(true) 14 | groups, err := api.GetGroups(false) 15 | if err != nil { 16 | fmt.Printf("%s\n", err) 17 | return 18 | } 19 | for _, group := range groups { 20 | fmt.Printf("Id: %s, Name: %s\n", group.Id, group.Name) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/pagination.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // Paging contains paging information 4 | type Paging struct { 5 | Count int `json:"count"` 6 | Total int `json:"total"` 7 | Page int `json:"page"` 8 | Pages int `json:"pages"` 9 | } 10 | 11 | // Pagination contains pagination information 12 | // This is different from Paging in that it contains additional details 13 | type Pagination struct { 14 | TotalCount int `json:"total_count"` 15 | Page int `json:"page"` 16 | PerPage int `json:"per_page"` 17 | PageCount int `json:"page_count"` 18 | First int `json:"first"` 19 | Last int `json:"last"` 20 | } 21 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/emoji.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | ) 7 | 8 | type emojiResponseFull struct { 9 | Emoji map[string]string `json:"emoji"` 10 | SlackResponse 11 | } 12 | 13 | // GetEmoji retrieves all the emojis 14 | func (api *Slack) GetEmoji() (map[string]string, error) { 15 | values := url.Values{ 16 | "token": {api.config.token}, 17 | } 18 | response := &emojiResponseFull{} 19 | err := parseResponse("emoji.list", values, response, api.debug) 20 | if err != nil { 21 | return nil, err 22 | } 23 | if !response.Ok { 24 | return nil, errors.New(response.Error) 25 | } 26 | return response.Emoji, nil 27 | } 28 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/websocket/examplehandler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 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_test 6 | 7 | import ( 8 | "io" 9 | "net/http" 10 | 11 | "golang.org/x/net/websocket" 12 | ) 13 | 14 | // Echo the data received on the WebSocket. 15 | func EchoServer(ws *websocket.Conn) { 16 | io.Copy(ws, ws) 17 | } 18 | 19 | // This example demonstrates a trivial echo server. 20 | func ExampleHandler() { 21 | http.Handle("/echo", websocket.Handler(EchoServer)) 22 | err := http.ListenAndServe(":12345", nil) 23 | if err != nil { 24 | panic("ListenAndServe: " + err.Error()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/examples/files/files.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nlopes/slack" 7 | ) 8 | 9 | func main() { 10 | api := slack.New("YOUR_TOKEN_HERE") 11 | params := slack.FileUploadParameters{ 12 | Title: "Batman Example", 13 | //Filetype: "txt", 14 | File: "example.txt", 15 | //Content: "Nan Nan Nan Nan Nan Nan Nan Nan Batman", 16 | } 17 | file, err := api.UploadFile(params) 18 | if err != nil { 19 | fmt.Printf("%s\n", err) 20 | return 21 | } 22 | fmt.Printf("Name: %s, Url: %s\n", file.Name, file.URL) 23 | 24 | err = api.DeleteFile(file.Id) 25 | if err != nil { 26 | fmt.Printf("%s\n", err) 27 | return 28 | } 29 | fmt.Printf("File %s deleted successfully.\n", file.Name) 30 | } 31 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/websocket_groups.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type GroupCreatedEvent struct { 4 | Type string `json:"type"` 5 | UserId string `json:"user"` 6 | Channel ChannelCreatedInfo `json:"channel"` 7 | } 8 | 9 | // XXX: Should we really do this? event.Group is probably nicer than event.Channel 10 | // even though the api returns "channel" 11 | type GroupMarkedEvent ChannelInfoEvent 12 | type GroupOpenEvent ChannelInfoEvent 13 | type GroupCloseEvent ChannelInfoEvent 14 | type GroupArchiveEvent ChannelInfoEvent 15 | type GroupUnarchiveEvent ChannelInfoEvent 16 | type GroupLeftEvent ChannelInfoEvent 17 | type GroupJoinedEvent ChannelJoinedEvent 18 | type GroupRenameEvent ChannelRenameEvent 19 | type GroupHistoryChangedEvent ChannelHistoryChangedEvent 20 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/websocket_teams.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type TeamJoinEvent struct { 4 | Type string `json:"type"` 5 | User *User `json:"user,omitempty"` 6 | } 7 | 8 | type TeamRenameEvent struct { 9 | Type string `json:"type"` 10 | Name string `json:"name,omitempty"` 11 | EventTimestamp *JSONTimeString `json:"event_ts,omitempty"` 12 | } 13 | 14 | type TeamPrefChangeEvent struct { 15 | Type string `json:"type"` 16 | Name string `json:"name,omitempty"` 17 | Value []string `json:"value,omitempty"` 18 | } 19 | 20 | type TeamDomainChangeEvent struct { 21 | Type string `json:"type"` 22 | Url string `json:"url"` 23 | Domain string `json:"domain"` 24 | } 25 | 26 | type TeamMigrationStartedEvent struct { 27 | Type string `json:"type"` 28 | } 29 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/websocket/exampledial_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 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_test 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | 11 | "golang.org/x/net/websocket" 12 | ) 13 | 14 | // This example demonstrates a trivial client. 15 | func ExampleDial() { 16 | origin := "http://localhost/" 17 | url := "ws://localhost:12345/ws" 18 | ws, err := websocket.Dial(url, "", origin) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | if _, err := ws.Write([]byte("hello, world!\n")); err != nil { 23 | log.Fatal(err) 24 | } 25 | var msg = make([]byte, 512) 26 | var n int 27 | if n, err = ws.Read(msg); err != nil { 28 | log.Fatal(err) 29 | } 30 | fmt.Printf("Received: %s.\n", msg[:n]) 31 | } 32 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/examples/messages/messages.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nlopes/slack" 7 | ) 8 | 9 | func main() { 10 | api := slack.New("YOUR_TOKEN_HERE") 11 | params := slack.PostMessageParameters{} 12 | attachment := slack.Attachment{ 13 | Pretext: "some pretext", 14 | Text: "some text", 15 | // Uncomment the following part to send a field too 16 | /* 17 | Fields: []slack.AttachmentField{ 18 | slack.AttachmentField{ 19 | Title: "a", 20 | Value: "no", 21 | }, 22 | }, 23 | */ 24 | } 25 | params.Attachments = []slack.Attachment{attachment} 26 | channelId, timestamp, err := api.PostMessage("CHANNEL_ID", "Some text", params) 27 | if err != nil { 28 | fmt.Printf("%s\n", err) 29 | return 30 | } 31 | fmt.Printf("Message successfully sent to channel %s at %s", channelId, timestamp) 32 | } 33 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/oauth.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | ) 7 | 8 | type oAuthResponseFull struct { 9 | AccessToken string `json:"access_token"` 10 | Scope string `json:"scope"` 11 | SlackResponse 12 | } 13 | 14 | // GetOAuthToken retrieves an AccessToken 15 | func GetOAuthToken(clientId, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { 16 | values := url.Values{ 17 | "client_id": {clientId}, 18 | "client_secret": {clientSecret}, 19 | "code": {code}, 20 | "redirect_uri": {redirectURI}, 21 | } 22 | response := &oAuthResponseFull{} 23 | err = parseResponse("oauth.access", values, response, debug) 24 | if err != nil { 25 | return "", "", err 26 | } 27 | if !response.Ok { 28 | return "", "", errors.New(response.Error) 29 | } 30 | return response.AccessToken, response.Scope, nil 31 | } 32 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-archivebot", 3 | "description": "Automatically archive empty and inactive public channels.", 4 | "repository": "https://github.com/timblair/slack-archivebot", 5 | "logo": "https://babeljs.io/images/users/slack.svg", 6 | "success_url": "/welcome", 7 | "env": { 8 | "ARCHIVEBOT_SLACK_TOKEN": { 9 | "description": "The Slack Web API key to use (must be a regular user account, not a bot)." 10 | }, 11 | "ARCHIVEBOT_INACTIVITY_DAYS": { 12 | "description": "The number of days' inactivity after which to archive a channel.", 13 | "value": "30" 14 | }, 15 | "ARCHIVEBOT_NOTIFY": { 16 | "description": "A Slack user or channel (e.g. #general or @tblair) to notify when something goes wrong." 17 | } 18 | }, 19 | "addons": [ 20 | "scheduler:standard" 21 | ], 22 | "buildpacks": [ 23 | { 24 | "url": "https://github.com/heroku/heroku-buildpack-go.git" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/websocket_files.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type fileActionEvent struct { 4 | Type string `json:"type"` 5 | EventTimestamp JSONTimeString `json:"event_ts"` 6 | File File `json:"file"` 7 | // FileId is used for FileDeletedEvent 8 | FileId string `json:"file_id,omitempty"` 9 | } 10 | 11 | type FileCreatedEvent fileActionEvent 12 | type FileSharedEvent fileActionEvent 13 | type FilePublicEvent fileActionEvent 14 | type FileUnsharedEvent fileActionEvent 15 | type FileChangeEvent fileActionEvent 16 | type FileDeletedEvent fileActionEvent 17 | type FilePrivateEvent fileActionEvent 18 | 19 | type FileCommentAddedEvent struct { 20 | fileActionEvent 21 | Comment Comment `json:"comment"` 22 | } 23 | 24 | type FileCommentEditedEvent struct { 25 | fileActionEvent 26 | Comment Comment `json:"comment"` 27 | } 28 | 29 | type FileCommentDeletedEvent struct { 30 | fileActionEvent 31 | CommentId string `json:"comment"` 32 | } 33 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/history.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | const ( 4 | DEFAULT_HISTORY_LATEST = "" 5 | DEFAULT_HISTORY_OLDEST = "0" 6 | DEFAULT_HISTORY_COUNT = 100 7 | DEFAULT_HISTORY_INCLUSIVE = false 8 | ) 9 | 10 | // HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs 11 | type HistoryParameters struct { 12 | Latest string 13 | Oldest string 14 | Count int 15 | Inclusive bool 16 | } 17 | 18 | // History contains message history information needed to navigate a Channel / Group / DM history 19 | type History struct { 20 | Latest string `json:"latest"` 21 | Messages []Message `json:"messages"` 22 | HasMore bool `json:"has_more"` 23 | } 24 | 25 | // NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set 26 | func NewHistoryParameters() HistoryParameters { 27 | return HistoryParameters{ 28 | Latest: DEFAULT_HISTORY_LATEST, 29 | Oldest: DEFAULT_HISTORY_OLDEST, 30 | Count: DEFAULT_HISTORY_COUNT, 31 | Inclusive: DEFAULT_HISTORY_INCLUSIVE, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Tim Blair 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/emoji_test.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func getEmojiHandler(rw http.ResponseWriter, r *http.Request) { 10 | rw.Header().Set("Content-Type", "application/json") 11 | response := []byte(`{"ok": true, "emoji": { 12 | "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png", 13 | "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", 14 | "shipit": "alias:squirrel" 15 | }}`) 16 | rw.Write(response) 17 | } 18 | 19 | func TestGetEmoji(t *testing.T) { 20 | http.HandleFunc("/emoji.list", getEmojiHandler) 21 | 22 | once.Do(startServer) 23 | SLACK_API = "http://" + serverAddr + "/" 24 | api := New("testing-token") 25 | emojisResponse := map[string]string{ 26 | "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png", 27 | "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", 28 | "shipit": "alias:squirrel", 29 | } 30 | 31 | emojis, err := api.GetEmoji() 32 | if err != nil { 33 | t.Errorf("Unexpected error: %s", err) 34 | return 35 | } 36 | eq := reflect.DeepEqual(emojis, emojisResponse) 37 | if !eq { 38 | t.Errorf("got %v; want %v", emojis, emojisResponse) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Norberto Lopes 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/examples/websocket/websocket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/nlopes/slack" 8 | ) 9 | 10 | func main() { 11 | chSender := make(chan slack.OutgoingMessage) 12 | chReceiver := make(chan slack.SlackEvent) 13 | 14 | api := slack.New("YOUR TOKEN HERE") 15 | api.SetDebug(true) 16 | wsAPI, err := api.StartRTM("", "http://example.com") 17 | if err != nil { 18 | fmt.Errorf("%s\n", err) 19 | } 20 | go wsAPI.HandleIncomingEvents(chReceiver) 21 | go wsAPI.Keepalive(20 * time.Second) 22 | go func(wsAPI *slack.SlackWS, chSender chan slack.OutgoingMessage) { 23 | for { 24 | select { 25 | case msg := <-chSender: 26 | wsAPI.SendMessage(&msg) 27 | } 28 | } 29 | }(wsAPI, chSender) 30 | for { 31 | select { 32 | case msg := <-chReceiver: 33 | fmt.Print("Event Received: ") 34 | switch msg.Data.(type) { 35 | case slack.HelloEvent: 36 | // Ignore hello 37 | case *slack.MessageEvent: 38 | a := msg.Data.(*slack.MessageEvent) 39 | fmt.Printf("Message: %v\n", a) 40 | case *slack.PresenceChangeEvent: 41 | a := msg.Data.(*slack.PresenceChangeEvent) 42 | fmt.Printf("Presence Change: %v\n", a) 43 | case slack.LatencyReport: 44 | a := msg.Data.(slack.LatencyReport) 45 | fmt.Printf("Current latency: %v\n", a.Value) 46 | case *slack.SlackWSError: 47 | error := msg.Data.(*slack.SlackWSError) 48 | fmt.Printf("Error: %d - %s\n", error.Code, error.Msg) 49 | default: 50 | fmt.Printf("Unexpected: %v\n", msg.Data) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/README.md: -------------------------------------------------------------------------------- 1 | # Slack API in Go 2 | 3 | [![GoDoc](https://godoc.org/github.com/nlopes/slack?status.png)](https://godoc.org/github.com/nlopes/slack) 4 | 5 | ## Installing 6 | 7 | ### *go get* 8 | 9 | $ go get github.com/nlopes/slack 10 | 11 | ## Example 12 | 13 | ### Getting all groups 14 | 15 | import ( 16 | "fmt" 17 | 18 | "github.com/nlopes/slack" 19 | ) 20 | 21 | func main() { 22 | api := slack.New("YOUR_TOKEN_HERE") 23 | // If you set debugging, it will log all requests to the console 24 | // Useful when encountering issues 25 | // api.SetDebug(true) 26 | groups, err := api.GetGroups(false) 27 | if err != nil { 28 | fmt.Printf("%s\n", err) 29 | return 30 | } 31 | for _, group := range groups { 32 | fmt.Printf("Id: %s, Name: %s\n", group.Id, group.Name) 33 | } 34 | } 35 | 36 | ### Getting User Information 37 | 38 | import ( 39 | "fmt" 40 | 41 | "github.com/nlopes/slack" 42 | ) 43 | 44 | func main() { 45 | api := slack.New("YOUR_TOKEN_HERE") 46 | user, err := api.GetUserInfo("U023BECGF") 47 | if err != nil { 48 | fmt.Printf("%s\n", err) 49 | return 50 | } 51 | fmt.Printf("Id: %s, Fullname: %s, Email: %s\n", user.Id, user.Profile.RealName, user.Profile.Email) 52 | } 53 | 54 | ## Why? 55 | I am currently learning Go and this seemed like a good idea. 56 | 57 | ## Stability 58 | As with any other piece of software expect bugs. Also, the design isn't finalized yet because I am not happy with how I laid out some things. Especially the websocket stuff. It is functional but very incomplete and buggy. 59 | 60 | ## Help 61 | Anyone is welcome to contribute. Either open a PR or create an issue. 62 | 63 | ## License 64 | BSD 2 Clause license -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/websocket_misc.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // TODO: Probably need an error event 9 | 10 | type HelloEvent struct{} 11 | 12 | type PresenceChangeEvent struct { 13 | Type string `json:"type"` 14 | Presence string `json:"presence"` 15 | UserId string `json:"user"` 16 | } 17 | 18 | type UserTypingEvent struct { 19 | Type string `json:"type"` 20 | UserId string `json:"user"` 21 | ChannelId string `json:"channel"` 22 | } 23 | 24 | type LatencyReport struct { 25 | Value time.Duration 26 | } 27 | 28 | type PrefChangeEvent struct { 29 | Type string `json:"type"` 30 | Name string `json:"name"` 31 | Value json.RawMessage `json:"value"` 32 | } 33 | 34 | type ManualPresenceChangeEvent struct { 35 | Type string `json:"type"` 36 | Presence string `json:"presence"` 37 | } 38 | type UserChangeEvent struct { 39 | Type string `json:"type"` 40 | User User `json:"user"` 41 | } 42 | type EmojiChangedEvent struct { 43 | Type string `json:"type"` 44 | EventTimestamp JSONTimeString `json:"event_ts"` 45 | } 46 | type CommandsChangedEvent struct { 47 | Type string `json:"type"` 48 | EventTimestamp JSONTimeString `json:"event_ts"` 49 | } 50 | type EmailDomainChangedEvent struct { 51 | Type string `json:"type"` 52 | EventTimestamp JSONTimeString `json:"event_ts"` 53 | EmailDomain string `json:"email_domain"` 54 | } 55 | type BotAddedEvent struct { 56 | Type string `json:"type"` 57 | Bot Bot `json:"bot"` 58 | } 59 | type BotChangedEvent struct { 60 | Type string `json:"type"` 61 | Bot Bot `json:"bot"` 62 | } 63 | type AccountsChangedEvent struct { 64 | Type string `json:"type"` 65 | } 66 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | ) 7 | 8 | /* 9 | Added as a var so that we can change this for testing purposes 10 | */ 11 | var SLACK_API string = "https://slack.com/api/" 12 | var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s" 13 | 14 | type SlackResponse struct { 15 | Ok bool `json:"ok"` 16 | Error string `json:"error"` 17 | } 18 | 19 | type AuthTestResponse struct { 20 | Url string `json:"url"` 21 | Team string `json:"team"` 22 | User string `json:"user"` 23 | TeamId string `json:"team_id"` 24 | UserId string `json:"user_id"` 25 | } 26 | 27 | type authTestResponseFull struct { 28 | SlackResponse 29 | AuthTestResponse 30 | } 31 | 32 | type Slack struct { 33 | config Config 34 | info Info 35 | debug bool 36 | } 37 | 38 | func New(token string) *Slack { 39 | return &Slack{ 40 | config: Config{token: token}, 41 | } 42 | } 43 | 44 | func (api *Slack) GetInfo() Info { 45 | return api.info 46 | } 47 | 48 | // AuthTest tests if the user is able to do authenticated requests or not 49 | func (api *Slack) AuthTest() (response *AuthTestResponse, error error) { 50 | responseFull := &authTestResponseFull{} 51 | err := parseResponse("auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug) 52 | if err != nil { 53 | return nil, err 54 | } 55 | if !responseFull.Ok { 56 | return nil, errors.New(responseFull.Error) 57 | } 58 | return &responseFull.AuthTestResponse, nil 59 | } 60 | 61 | // SetDebug switches the api into debug mode 62 | // When in debug mode, it logs various info about what its doing 63 | // If you ever use this in production, don't call SetDebug(true) 64 | func (api *Slack) SetDebug(debug bool) { 65 | api.debug = debug 66 | } 67 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/websocket_channels.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type ChannelCreatedEvent struct { 4 | Type string `json:"type"` 5 | Channel ChannelCreatedInfo `json:"channel"` 6 | EventTimestamp JSONTimeString `json:"event_ts"` 7 | } 8 | 9 | type ChannelCreatedInfo struct { 10 | Id string `json:"id"` 11 | IsChannel bool `json:"is_channel"` 12 | Name string `json:"name"` 13 | Created JSONTimeString `json:"created"` 14 | Creator string `json:"creator"` 15 | } 16 | 17 | type ChannelJoinedEvent struct { 18 | Type string `json:"type"` 19 | Channel Channel `json:"channel"` 20 | } 21 | 22 | type ChannelInfoEvent struct { 23 | // channel_left 24 | // channel_deleted 25 | // channel_archive 26 | // channel_unarchive 27 | Type string `json:"type"` 28 | ChannelId string `json:"channel"` 29 | UserId string `json:"user,omitempty"` 30 | Timestamp *JSONTimeString `json:"ts,omitempty"` 31 | } 32 | 33 | type ChannelRenameEvent struct { 34 | Type string `json:"type"` 35 | Channel ChannelRenameInfo `json:"channel"` 36 | } 37 | 38 | type ChannelRenameInfo struct { 39 | Id string `json:"id"` 40 | Name string `json:"name"` 41 | Created JSONTime `json:"created"` 42 | } 43 | 44 | type ChannelHistoryChangedEvent struct { 45 | Type string `json:"type"` 46 | Latest JSONTimeString `json:"latest"` 47 | Timestamp JSONTimeString `json:"ts"` 48 | EventTimestamp JSONTimeString `json:"event_ts"` 49 | } 50 | 51 | type ChannelMarkedEvent ChannelInfoEvent 52 | type ChannelLeftEvent ChannelInfoEvent 53 | type ChannelDeletedEvent ChannelInfoEvent 54 | type ChannelArchiveEvent ChannelInfoEvent 55 | type ChannelUnarchiveEvent ChannelInfoEvent 56 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/admin.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type adminResponse struct { 10 | OK bool `json:"ok"` 11 | Error string `json:"error"` 12 | } 13 | 14 | func adminRequest(method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { 15 | adminResponse := &adminResponse{} 16 | err := parseAdminResponse(method, teamName, values, adminResponse, debug) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | if !adminResponse.OK { 22 | return nil, errors.New(adminResponse.Error) 23 | } 24 | 25 | return adminResponse, nil 26 | } 27 | 28 | func (api *Slack) InviteGuest( 29 | teamName string, 30 | channelID string, 31 | firstName string, 32 | lastName string, 33 | emailAddress string, 34 | ) error { 35 | values := url.Values{ 36 | "email": {emailAddress}, 37 | "channels": {channelID}, 38 | "first_name": {firstName}, 39 | "last_name": {lastName}, 40 | "ultra_restricted": {"1"}, 41 | "token": {api.config.token}, 42 | "set_active": {"true"}, 43 | "_attempts": {"1"}, 44 | } 45 | 46 | _, err := adminRequest("invite", teamName, values, api.debug) 47 | if err != nil { 48 | return fmt.Errorf("Failed to invite single-channel guest: %s", err) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (api *Slack) InviteRestricted( 55 | teamName string, 56 | channelID string, 57 | firstName string, 58 | lastName string, 59 | emailAddress string, 60 | ) error { 61 | values := url.Values{ 62 | "email": {emailAddress}, 63 | "channels": {channelID}, 64 | "first_name": {firstName}, 65 | "last_name": {lastName}, 66 | "restricted": {"1"}, 67 | "token": {api.config.token}, 68 | "set_active": {"true"}, 69 | "_attempts": {"1"}, 70 | } 71 | 72 | _, err := adminRequest("invite", teamName, values, api.debug) 73 | if err != nil { 74 | return fmt.Errorf("Failed to restricted account: %s", err) 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/stars_test.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | var starsTests = struct { 10 | in []byte 11 | outItems []StarredItem 12 | outPaging *Paging 13 | }{ 14 | []byte(`{"ok": true, 15 | "items": [ 16 | { 17 | "type": "message", 18 | "channel": "C2147483705" 19 | }, 20 | { 21 | "type": "file" 22 | }, 23 | { 24 | "type": "file_comment" 25 | }, 26 | { 27 | "type": "channel", 28 | "channel": "C2147483705" 29 | } 30 | 31 | ], 32 | "paging": { 33 | "count": 100, 34 | "total": 4, 35 | "page": 1, 36 | "pages": 1 37 | }}`), 38 | []StarredItem{ 39 | { 40 | Type: "message", 41 | ChannelId: "C2147483705", 42 | }, 43 | { 44 | Type: "file", 45 | }, 46 | { 47 | Type: "file_comment", 48 | }, 49 | { 50 | Type: "channel", 51 | ChannelId: "C2147483705", 52 | }, 53 | }, 54 | &Paging{ 55 | Count: 100, 56 | Total: 4, 57 | Page: 1, 58 | Pages: 1, 59 | }, 60 | } 61 | 62 | func getStarredHandler(rw http.ResponseWriter, r *http.Request) { 63 | rw.Header().Set("Content-Type", "application/json") 64 | // XXX: I stripped the actual content just to test this test Oo 65 | // At some point I should add valid content here 66 | rw.Write(starsTests.in) 67 | } 68 | 69 | func TestGetStarred(t *testing.T) { 70 | http.HandleFunc("/stars.list", getStarredHandler) 71 | once.Do(startServer) 72 | SLACK_API = "http://" + serverAddr + "/" 73 | api := New("testing-token") 74 | response_items, response_paging, err := api.GetStarred(NewStarsParameters()) 75 | if err != nil { 76 | t.Errorf("Unexpected error: %s", err) 77 | return 78 | } 79 | eq := reflect.DeepEqual(response_items, starsTests.outItems) 80 | if !eq { 81 | t.Errorf("got %v; want %v", response_items, starsTests.outItems) 82 | } 83 | eq = reflect.DeepEqual(response_paging, starsTests.outPaging) 84 | if !eq { 85 | t.Errorf("got %v; want %v", response_paging, starsTests.outPaging) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slack-archivebot 2 | 3 | A simple Go app that will automatically archive public channels on the 4 | following criteria: 5 | 6 | * if the channel is empty; or 7 | * if the channel has had no activity for the last X days. 8 | 9 | ## Use 10 | 11 | The poject uses [`godep`](https://github.com/tools/godep) to manage 12 | dependencies, so you'll need that. Once you've cloned this repo into your 13 | `$GOPATH`: 14 | 15 | ```sh 16 | cd path/to/slack-archivebot 17 | godep go build 18 | ./slack-archivebot 19 | ``` 20 | 21 | ## Deployment 22 | 23 | Heroku is the simplest option. The script can run quite happily on a free dyno 24 | using the Heroku Scheduler add-on. 25 | 26 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)][herokudeploy] 27 | 28 | Note: the above will deploy the app to your Heroku account, and add the 29 | Scheduler add-on, but _won't_ configure it to run. To do this, go to your 30 | [dashboard](https://dashboard.heroku.com/apps), find the appropriate app, open 31 | the Scheduler add-on, and add a new job that runs `slack-archivebot` every 10 32 | minutes. 33 | 34 | ## Configuration 35 | 36 | The following environment variables are used to configure the script: 37 | 38 | * `ARCHIVEBOT_SLACK_TOKEN`: the Slack [Web API key](https://api.slack.com/web) 39 | for your team. 40 | * `ARCHIVEBOT_INACTIVITY_DAYS`: the number of days' inactivity after which to 41 | archive a channel (default: `30`). 42 | * `ARCHIVEBOT_NOTIFY`: a Slack user or channel (e.g. `#general` or `@tblair`) 43 | to notify when something goes wrong. 44 | 45 | Note: you must use an API key for a regular Slack user account. You _cannot_ 46 | use a bot user account, because bot users don't have permission to archive 47 | channels. 48 | 49 | ## Licensing and Attribution 50 | 51 | slack-archivebot is released under the MIT license as detailed in the LICENSE 52 | file that should be distributed with this library; the source code is [freely 53 | available](http://github.com/timblair/slack-archivebot). 54 | 55 | slack-archivebot was developed by [Tim Blair](http://tim.bla.ir/) during a 56 | [Venntro](http://venntro.com/) hack day. 57 | 58 | [herokudeploy]: https://heroku.com/deploy?template=https://github.com/timblair/slack-archivebot 59 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/stars.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | DEFAULT_STARS_USERID = "" 11 | DEFAULT_STARS_COUNT = 100 12 | DEFAULT_STARS_PAGE = 1 13 | ) 14 | 15 | type StarsParameters struct { 16 | User string 17 | Count int 18 | Page int 19 | } 20 | 21 | // TODO: Verify this. The whole thing is complicated. I don't like the way they mixed things 22 | // It also appears to be a bug in parsing the message 23 | type StarredItem struct { 24 | Type string `json:"type"` 25 | ChannelId string `json:"channel"` 26 | Message `json:"message,omitempty"` 27 | File `json:"file,omitempty"` 28 | Comment `json:"comment,omitempty"` 29 | } 30 | 31 | type starsResponseFull struct { 32 | Items []StarredItem `json:"items"` 33 | Paging `json:"paging"` 34 | SlackResponse 35 | } 36 | 37 | func NewStarsParameters() StarsParameters { 38 | return StarsParameters{ 39 | User: DEFAULT_STARS_USERID, 40 | Count: DEFAULT_STARS_COUNT, 41 | Page: DEFAULT_STARS_PAGE, 42 | } 43 | } 44 | 45 | // GetStarred returns a list of StarredItem items. The user then has to iterate over them and figure out what they should 46 | // be looking at according to what is in the Type. 47 | // for _, item := range items { 48 | // switch c.Type { 49 | // case "file_comment": 50 | // log.Println(c.Comment) 51 | // case "file": 52 | // ... 53 | // } 54 | // } 55 | func (api *Slack) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) { 56 | response := &starsResponseFull{} 57 | values := url.Values{ 58 | "token": {api.config.token}, 59 | } 60 | if params.User != DEFAULT_STARS_USERID { 61 | values.Add("user", params.User) 62 | } 63 | if params.Count != DEFAULT_STARS_COUNT { 64 | values.Add("count", strconv.Itoa(params.Count)) 65 | } 66 | if params.Page != DEFAULT_STARS_PAGE { 67 | values.Add("page", strconv.Itoa(params.Page)) 68 | } 69 | err := parseResponse("stars.list", values, response, api.debug) 70 | if err != nil { 71 | return nil, nil, err 72 | } 73 | if !response.Ok { 74 | return nil, nil, errors.New(response.Error) 75 | } 76 | return response.Items, &response.Paging, nil 77 | } 78 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/messages.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type OutgoingMessage struct { 4 | Id int `json:"id"` 5 | ChannelId string `json:"channel,omitempty"` 6 | Text string `json:"text,omitempty"` 7 | Type string `json:"type,omitempty"` 8 | } 9 | 10 | // Message is an auxiliary type to allow us to have a message containing sub messages 11 | type Message struct { 12 | Msg 13 | SubMessage *Msg `json:"message,omitempty"` 14 | } 15 | 16 | // Msg contains information about a slack message 17 | type Msg struct { 18 | Id string `json:"id"` 19 | BotId string `json:"bot_id,omitempty"` 20 | UserId string `json:"user,omitempty"` 21 | Username string `json:"username,omitempty"` 22 | ChannelId string `json:"channel,omitempty"` 23 | Timestamp string `json:"ts,omitempty"` 24 | Text string `json:"text,omitempty"` 25 | Team string `json:"team,omitempty"` 26 | File *File `json:"file,omitempty"` 27 | // Type may come if it's part of a message list 28 | // e.g.: channel.history 29 | Type string `json:"type,omitempty"` 30 | IsStarred bool `json:"is_starred,omitempty"` 31 | // Submessage 32 | SubType string `json:"subtype,omitempty"` 33 | Hidden bool `json:"bool,omitempty"` 34 | DeletedTimestamp string `json:"deleted_ts,omitempty"` 35 | Attachments []Attachment `json:"attachments,omitempty"` 36 | ReplyTo int `json:"reply_to,omitempty"` 37 | Upload bool `json:"upload,omitempty"` 38 | } 39 | 40 | // Presence XXX: not used yet 41 | type Presence struct { 42 | Presence string `json:"presence"` 43 | UserId string `json:"user"` 44 | } 45 | 46 | // Event contains the event type 47 | type Event struct { 48 | Type string `json:"type,omitempty"` 49 | } 50 | 51 | // Ping contains information about a Ping Event 52 | type Ping struct { 53 | Id int `json:"id"` 54 | Type string `json:"type"` 55 | } 56 | 57 | // Pong contains information about a Pong Event 58 | type Pong struct { 59 | Type string `json:"type"` 60 | ReplyTo int `json:"reply_to"` 61 | } 62 | 63 | // NewOutGoingMessage prepares an OutgoingMessage that the user can use to send a message 64 | func (api *SlackWS) NewOutgoingMessage(text string, channel string) *OutgoingMessage { 65 | api.mutex.Lock() 66 | defer api.mutex.Unlock() 67 | api.messageId++ 68 | return &OutgoingMessage{ 69 | Id: api.messageId, 70 | Type: "message", 71 | ChannelId: channel, 72 | Text: text, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/misc_test.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/url" 7 | "sync" 8 | "testing" 9 | ) 10 | 11 | var ( 12 | parseResponseOnce sync.Once 13 | ) 14 | 15 | func parseResponseHandler(rw http.ResponseWriter, r *http.Request) { 16 | rw.Header().Set("Content-Type", "application/json") 17 | token := r.FormValue("token") 18 | log.Println(token) 19 | if token == "" { 20 | rw.Write([]byte(`{"ok":false,"error":"not_authed"}`)) 21 | return 22 | } 23 | if token != validToken { 24 | rw.Write([]byte(`{"ok":false,"error":"invalid_auth"}`)) 25 | return 26 | } 27 | response := []byte(`{"ok": true}`) 28 | rw.Write(response) 29 | } 30 | 31 | func setParseResponseHandler() { 32 | http.HandleFunc("/parseResponse", parseResponseHandler) 33 | } 34 | 35 | func TestParseResponse(t *testing.T) { 36 | parseResponseOnce.Do(setParseResponseHandler) 37 | once.Do(startServer) 38 | SLACK_API = "http://" + serverAddr + "/" 39 | values := url.Values{ 40 | "token": {validToken}, 41 | } 42 | responsePartial := &SlackResponse{} 43 | err := parseResponse("parseResponse", values, responsePartial, false) 44 | if err != nil { 45 | t.Errorf("Unexpected error: %s", err) 46 | } 47 | } 48 | 49 | func TestParseResponseNoToken(t *testing.T) { 50 | parseResponseOnce.Do(setParseResponseHandler) 51 | once.Do(startServer) 52 | SLACK_API = "http://" + serverAddr + "/" 53 | values := url.Values{} 54 | responsePartial := &SlackResponse{} 55 | err := parseResponse("parseResponse", values, responsePartial, false) 56 | if err != nil { 57 | t.Errorf("Unexpected error: %s", err) 58 | return 59 | } 60 | if responsePartial.Ok == true { 61 | t.Errorf("Unexpected error: %s", err) 62 | } else if responsePartial.Error != "not_authed" { 63 | t.Errorf("got %v; want %v", responsePartial.Error, "not_authed") 64 | } 65 | } 66 | 67 | func TestParseResponseInvalidToken(t *testing.T) { 68 | parseResponseOnce.Do(setParseResponseHandler) 69 | once.Do(startServer) 70 | SLACK_API = "http://" + serverAddr + "/" 71 | values := url.Values{ 72 | "token": {"whatever"}, 73 | } 74 | responsePartial := &SlackResponse{} 75 | err := parseResponse("parseResponse", values, responsePartial, false) 76 | if err != nil { 77 | t.Errorf("Unexpected error: %s", err) 78 | return 79 | } 80 | if responsePartial.Ok == true { 81 | t.Errorf("Unexpected error: %s", err) 82 | } else if responsePartial.Error != "invalid_auth" { 83 | t.Errorf("got %v; want %v", responsePartial.Error, "invalid_auth") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/misc.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "mime/multipart" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "path/filepath" 16 | "time" 17 | ) 18 | 19 | func fileUploadReq(path, fpath string, values url.Values) (*http.Request, error) { 20 | fullpath, err := filepath.Abs(fpath) 21 | if err != nil { 22 | return nil, err 23 | } 24 | file, err := os.Open(fullpath) 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer file.Close() 29 | 30 | body := &bytes.Buffer{} 31 | wr := multipart.NewWriter(body) 32 | 33 | ioWriter, err := wr.CreateFormFile("file", filepath.Base(fullpath)) 34 | if err != nil { 35 | wr.Close() 36 | return nil, err 37 | } 38 | bytes, err := io.Copy(ioWriter, file) 39 | if err != nil { 40 | wr.Close() 41 | return nil, err 42 | } 43 | // Close the multipart writer or the footer won't be written 44 | wr.Close() 45 | stat, err := file.Stat() 46 | if err != nil { 47 | return nil, err 48 | } 49 | if bytes != stat.Size() { 50 | return nil, errors.New("could not read the whole file") 51 | } 52 | req, err := http.NewRequest("POST", path, body) 53 | if err != nil { 54 | return nil, err 55 | } 56 | req.Header.Add("Content-Type", wr.FormDataContentType()) 57 | req.URL.RawQuery = (values).Encode() 58 | return req, nil 59 | } 60 | 61 | func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error { 62 | var decoder *json.Decoder 63 | if debug { 64 | response, err := ioutil.ReadAll(body) 65 | if err != nil { 66 | return err 67 | } 68 | log.Println(string(response)) 69 | decoder = json.NewDecoder(bytes.NewReader(response)) 70 | } else { 71 | decoder = json.NewDecoder(body) 72 | } 73 | if err := decoder.Decode(&intf); err != nil { 74 | return err 75 | } 76 | return nil 77 | 78 | } 79 | 80 | func parseResponseMultipart(path string, filepath string, values url.Values, intf interface{}, debug bool) error { 81 | req, err := fileUploadReq(SLACK_API+path, filepath, values) 82 | client := &http.Client{} 83 | resp, err := client.Do(req) 84 | if err != nil { 85 | return err 86 | } 87 | defer resp.Body.Close() 88 | return parseResponseBody(resp.Body, &intf, debug) 89 | } 90 | 91 | func postForm(endpoint string, values url.Values, intf interface{}, debug bool) error { 92 | resp, err := http.PostForm(endpoint, values) 93 | if err != nil { 94 | return err 95 | } 96 | defer resp.Body.Close() 97 | return parseResponseBody(resp.Body, &intf, debug) 98 | } 99 | 100 | func parseResponse(path string, values url.Values, intf interface{}, debug bool) error { 101 | return postForm(SLACK_API+path, values, intf, debug) 102 | } 103 | 104 | func parseAdminResponse(method string, teamName string, values url.Values, intf interface{}, debug bool) error { 105 | endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix()) 106 | return postForm(endpoint, values, intf, debug) 107 | } 108 | -------------------------------------------------------------------------------- /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/github.com/nlopes/slack/dm.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | type imChannel struct { 10 | Id string `json:"id"` 11 | } 12 | 13 | type imResponseFull struct { 14 | NoOp bool `json:"no_op"` 15 | AlreadyClosed bool `json:"already_closed"` 16 | AlreadyOpen bool `json:"already_open"` 17 | Channel imChannel `json:"channel"` 18 | IMs []IM `json:"ims"` 19 | History 20 | SlackResponse 21 | } 22 | 23 | // IM contains information related to the Direct Message channel 24 | type IM struct { 25 | BaseChannel 26 | IsIM bool `json:"is_im"` 27 | UserId string `json:"user"` 28 | IsUserDeleted bool `json:"is_user_deleted"` 29 | } 30 | 31 | func imRequest(path string, values url.Values, debug bool) (*imResponseFull, error) { 32 | response := &imResponseFull{} 33 | err := parseResponse(path, values, response, debug) 34 | if err != nil { 35 | return nil, err 36 | } 37 | if !response.Ok { 38 | return nil, errors.New(response.Error) 39 | } 40 | return response, nil 41 | } 42 | 43 | // CloseIMChannel closes the direct message channel 44 | func (api *Slack) CloseIMChannel(channelId string) (bool, bool, error) { 45 | values := url.Values{ 46 | "token": {api.config.token}, 47 | "channel": {channelId}, 48 | } 49 | response, err := imRequest("im.close", values, api.debug) 50 | if err != nil { 51 | return false, false, err 52 | } 53 | return response.NoOp, response.AlreadyClosed, nil 54 | } 55 | 56 | // OpenIMChannel opens a direct message channel to the user provided as argument 57 | // Returns some status and the channelId 58 | func (api *Slack) OpenIMChannel(userId string) (bool, bool, string, error) { 59 | values := url.Values{ 60 | "token": {api.config.token}, 61 | "user": {userId}, 62 | } 63 | response, err := imRequest("im.open", values, api.debug) 64 | if err != nil { 65 | return false, false, "", err 66 | } 67 | return response.NoOp, response.AlreadyOpen, response.Channel.Id, nil 68 | } 69 | 70 | // MarkIMChannel sets the read mark of a direct message channel to a specific point 71 | func (api *Slack) MarkIMChannel(channelId, ts string) (err error) { 72 | values := url.Values{ 73 | "token": {api.config.token}, 74 | "channel": {channelId}, 75 | "ts": {ts}, 76 | } 77 | _, err = imRequest("im.mark", values, api.debug) 78 | if err != nil { 79 | return err 80 | } 81 | return 82 | } 83 | 84 | // GetIMHistory retrieves the direct message channel history 85 | func (api *Slack) GetIMHistory(channelId string, params HistoryParameters) (*History, error) { 86 | values := url.Values{ 87 | "token": {api.config.token}, 88 | "channel": {channelId}, 89 | } 90 | if params.Latest != DEFAULT_HISTORY_LATEST { 91 | values.Add("latest", params.Latest) 92 | } 93 | if params.Oldest != DEFAULT_HISTORY_OLDEST { 94 | values.Add("oldest", params.Oldest) 95 | } 96 | if params.Count != DEFAULT_HISTORY_COUNT { 97 | values.Add("count", strconv.Itoa(params.Count)) 98 | } 99 | if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { 100 | if params.Inclusive { 101 | values.Add("inclusive", "1") 102 | } else { 103 | values.Add("inclusive", "0") 104 | } 105 | } 106 | response, err := imRequest("im.history", values, api.debug) 107 | if err != nil { 108 | return nil, err 109 | } 110 | return &response.History, nil 111 | } 112 | 113 | // GetIMChannels returns the list of direct message channels 114 | func (api *Slack) GetIMChannels() ([]IM, error) { 115 | values := url.Values{ 116 | "token": {api.config.token}, 117 | } 118 | response, err := imRequest("im.list", values, api.debug) 119 | if err != nil { 120 | return nil, err 121 | } 122 | return response.IMs, nil 123 | } 124 | -------------------------------------------------------------------------------- /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 | return 78 | } 79 | // The server should abort the WebSocket connection if it finds 80 | // the client did not send a handshake that matches with protocol 81 | // specification. 82 | defer rwc.Close() 83 | conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) 84 | if err != nil { 85 | return 86 | } 87 | if conn == nil { 88 | panic("unexpected nil conn") 89 | } 90 | s.Handler(conn) 91 | } 92 | 93 | // Handler is a simple interface to a WebSocket browser client. 94 | // It checks if Origin header is valid URL by default. 95 | // You might want to verify websocket.Conn.Config().Origin in the func. 96 | // If you use Server instead of Handler, you could call websocket.Origin and 97 | // check the origin in your Handshake func. So, if you want to accept 98 | // non-browser clients, which do not send an Origin header, set a 99 | // Server.Handshake that does not check the origin. 100 | type Handler func(*Conn) 101 | 102 | func checkOrigin(config *Config, req *http.Request) (err error) { 103 | config.Origin, err = Origin(config, req) 104 | if err == nil && config.Origin == nil { 105 | return fmt.Errorf("null origin") 106 | } 107 | return err 108 | } 109 | 110 | // ServeHTTP implements the http.Handler interface for a WebSocket 111 | func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 112 | s := Server{Handler: h, Handshake: checkOrigin} 113 | s.serveWebSocket(w, req) 114 | } 115 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/search.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | DEFAULT_SEARCH_SORT = "score" 11 | DEFAULT_SEARCH_SORT_DIR = "desc" 12 | DEFAULT_SEARCH_HIGHLIGHT = false 13 | DEFAULT_SEARCH_COUNT = 100 14 | DEFAULT_SEARCH_PAGE = 1 15 | ) 16 | 17 | type SearchParameters struct { 18 | Sort string 19 | SortDirection string 20 | Highlight bool 21 | Count int 22 | Page int 23 | } 24 | 25 | type CtxChannel struct { 26 | Id string `json:"id"` 27 | Name string `json:"name"` 28 | } 29 | 30 | type CtxMessage struct { 31 | UserId string `json:"user"` 32 | Username string `json:"username"` 33 | Text string `json:"text"` 34 | Timestamp string `json:"ts"` 35 | Type string `json:"type"` 36 | } 37 | 38 | type SearchMessage struct { 39 | Type string `json:"type"` 40 | Channel CtxChannel `json:"channel"` 41 | UserId string `json:"user"` 42 | Username string `json:"username"` 43 | Timestamp string `json:"ts"` 44 | Text string `json:"text"` 45 | Permalink string `json:"permalink"` 46 | Previous CtxMessage `json:"previous"` 47 | Previous2 CtxMessage `json:"previous_2"` 48 | Next CtxMessage `json:"next"` 49 | Next2 CtxMessage `json:"next_2"` 50 | } 51 | 52 | type SearchMessages struct { 53 | Matches []SearchMessage `json:"matches"` 54 | Paging `json:"paging"` 55 | Pagination `json:"pagination"` 56 | Total int `json:"total"` 57 | } 58 | 59 | type SearchFiles struct { 60 | Matches []File `json:"matches"` 61 | Paging `json:"paging"` 62 | Pagination `json:"pagination"` 63 | Total int `json:"total"` 64 | } 65 | 66 | type searchResponseFull struct { 67 | Query string `json:"query"` 68 | SearchMessages `json:"messages"` 69 | SearchFiles `json:"files"` 70 | SlackResponse 71 | } 72 | 73 | func NewSearchParameters() SearchParameters { 74 | return SearchParameters{ 75 | Sort: DEFAULT_SEARCH_SORT, 76 | SortDirection: DEFAULT_SEARCH_SORT_DIR, 77 | Highlight: DEFAULT_SEARCH_HIGHLIGHT, 78 | Count: DEFAULT_SEARCH_COUNT, 79 | Page: DEFAULT_SEARCH_PAGE, 80 | } 81 | } 82 | 83 | func search(token, path, query string, params SearchParameters, files, messages, debug bool) (response *searchResponseFull, error error) { 84 | values := url.Values{ 85 | "token": {token}, 86 | "query": {query}, 87 | } 88 | if params.Sort != DEFAULT_SEARCH_SORT { 89 | values.Add("sort", params.Sort) 90 | } 91 | if params.SortDirection != DEFAULT_SEARCH_SORT_DIR { 92 | values.Add("sort_dir", params.SortDirection) 93 | } 94 | if params.Highlight != DEFAULT_SEARCH_HIGHLIGHT { 95 | values.Add("highlight", strconv.Itoa(1)) 96 | } 97 | if params.Count != DEFAULT_SEARCH_COUNT { 98 | values.Add("count", strconv.Itoa(params.Count)) 99 | } 100 | if params.Page != DEFAULT_SEARCH_PAGE { 101 | values.Add("page", strconv.Itoa(params.Page)) 102 | } 103 | response = &searchResponseFull{} 104 | err := parseResponse(path, values, response, debug) 105 | if err != nil { 106 | return nil, err 107 | } 108 | if !response.Ok { 109 | return nil, errors.New(response.Error) 110 | } 111 | return response, nil 112 | 113 | } 114 | 115 | func (api *Slack) Search(query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) { 116 | response, err := search(api.config.token, "search.all", query, params, true, true, api.debug) 117 | if err != nil { 118 | return nil, nil, err 119 | } 120 | return &response.SearchMessages, &response.SearchFiles, nil 121 | } 122 | 123 | func (api *Slack) SearchFiles(query string, params SearchParameters) (*SearchFiles, error) { 124 | response, err := search(api.config.token, "search.files", query, params, true, false, api.debug) 125 | if err != nil { 126 | return nil, err 127 | } 128 | return &response.SearchFiles, nil 129 | } 130 | 131 | func (api *Slack) SearchMessages(query string, params SearchParameters) (*SearchMessages, error) { 132 | response, err := search(api.config.token, "search.messages", query, params, false, true, api.debug) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return &response.SearchMessages, nil 137 | } 138 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/nlopes/slack" 13 | ) 14 | 15 | func main() { 16 | slackToken := os.Getenv("ARCHIVEBOT_SLACK_TOKEN") 17 | api := slack.New(slackToken) 18 | //api.SetDebug(true) 19 | 20 | channels, err := api.GetChannels(true) 21 | if err != nil { 22 | log.Printf("Error when loading channels: %s\n", err) 23 | return 24 | } 25 | 26 | var wg sync.WaitGroup 27 | wg.Add(2) 28 | 29 | go func(c []slack.Channel) { 30 | defer wg.Done() 31 | archiveEmptyChannels(api, c) 32 | }(channels) 33 | 34 | go func(c []slack.Channel) { 35 | defer wg.Done() 36 | archiveInactiveChannels(api, c) 37 | }(channels) 38 | 39 | wg.Wait() 40 | } 41 | 42 | func archiveEmptyChannels(api *slack.Slack, c []slack.Channel) { 43 | empty := filterEmptyChannels(api, c) 44 | archiveChannels(api, empty, "emptiness") 45 | } 46 | 47 | func archiveInactiveChannels(api *slack.Slack, c []slack.Channel) { 48 | inactive := filterInactiveChannels(api, c) 49 | archiveChannels(api, inactive, "inactivity") 50 | } 51 | 52 | func archiveChannels(api *slack.Slack, c []slack.Channel, reason string) { 53 | var wg sync.WaitGroup 54 | 55 | for _, channel := range c { 56 | fmt.Printf("Archiving #%s (%s) due to %s\n", channel.Name, channel.Id, reason) 57 | wg.Add(1) 58 | 59 | go func(c slack.Channel) { 60 | defer wg.Done() 61 | if err := api.ArchiveChannel(c.Id); err != nil { 62 | message := fmt.Sprintf( 63 | "Error archiving channel #%s (%s): %s\n", c.Name, c.Id, err) 64 | log.Printf(message) 65 | // send error message in a DM to onErrorNotify user/channel 66 | onErrorNotify := os.Getenv("ARCHIVEBOT_NOTIFY") 67 | if onErrorNotify != "" { 68 | params := slack.PostMessageParameters{} 69 | if _, _, postMessageError := api.PostMessage( 70 | onErrorNotify, message, params); postMessageError != nil { 71 | postMessageErrorMessage := fmt.Sprintf( 72 | "Error posting error message to Slack: %s\n", postMessageError) 73 | log.Printf(postMessageErrorMessage) 74 | } 75 | } 76 | } 77 | }(channel) 78 | } 79 | 80 | wg.Wait() 81 | } 82 | 83 | func filterEmptyChannels(api *slack.Slack, c []slack.Channel) []slack.Channel { 84 | empty := []slack.Channel{} 85 | for _, channel := range c { 86 | if channel.NumMembers == 0 { 87 | empty = append(empty, channel) 88 | } 89 | } 90 | return empty 91 | } 92 | 93 | type LastChannelMessage struct { 94 | Channel slack.Channel 95 | Timestamp int64 96 | } 97 | 98 | func filterInactiveChannels(api *slack.Slack, c []slack.Channel) []slack.Channel { 99 | inactiveDays, _ := strconv.ParseInt(os.Getenv("ARCHIVEBOT_INACTIVE_DAYS"), 10, 32) 100 | if inactiveDays == 0 { 101 | inactiveDays = 30 102 | } 103 | 104 | timeout := int64(time.Now().Unix()) - (86400 * inactiveDays) 105 | channels := []slack.Channel{} 106 | 107 | res := make(chan LastChannelMessage) 108 | for _, channel := range c { 109 | go func(channel slack.Channel) { 110 | timestamp, _ := lastMessageTimestamp(api, channel) 111 | res <- LastChannelMessage{Channel: channel, Timestamp: timestamp} 112 | }(channel) 113 | } 114 | 115 | for i := 0; i < len(c); i++ { 116 | lcm := <-res 117 | if lcm.Timestamp > 0 && lcm.Timestamp < timeout { 118 | channels = append(channels, lcm.Channel) 119 | } 120 | } 121 | 122 | close(res) 123 | return channels 124 | } 125 | 126 | func lastMessageTimestamp(api *slack.Slack, channel slack.Channel) (int64, error) { 127 | var latest string 128 | 129 | for { 130 | historyParams := slack.HistoryParameters{Count: 5} 131 | if latest != "" { 132 | historyParams.Latest = latest 133 | } 134 | 135 | history, err := api.GetChannelHistory(channel.Id, historyParams) 136 | 137 | if err != nil { 138 | return -1, err 139 | } 140 | 141 | if len(history.Messages) == 0 { 142 | return -1, nil 143 | } 144 | 145 | for _, msg := range history.Messages { 146 | latest = msg.Msg.Timestamp 147 | 148 | if msg.SubType != "channel_join" && msg.SubType != "channel_leave" { 149 | msgStamp := strings.Split(msg.Msg.Timestamp, ".") 150 | if timestamp, err := strconv.ParseInt(msgStamp[0], 10, 32); err == nil { 151 | return timestamp, nil 152 | } 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/users.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | ) 7 | 8 | // UserProfile contains all the information details of a given user 9 | type UserProfile struct { 10 | FirstName string `json:"first_name"` 11 | LastName string `json:"last_name"` 12 | RealName string `json:"real_name"` 13 | RealNameNormalized string `json:"real_name_normalized"` 14 | Email string `json:"email"` 15 | Skype string `json:"skype"` 16 | Phone string `json:"phone"` 17 | Image24 string `json:"image_24"` 18 | Image32 string `json:"image_32"` 19 | Image48 string `json:"image_48"` 20 | Image72 string `json:"image_72"` 21 | Image192 string `json:"image_192"` 22 | ImageOriginal string `json:"image_original"` 23 | Title string `json:"title"` 24 | } 25 | 26 | // User contains all the information of a user 27 | type User struct { 28 | Id string `json:"id"` 29 | Name string `json:"name"` 30 | Deleted bool `json:"deleted"` 31 | Color string `json:"color"` 32 | RealName string `json:"real_name"` 33 | TZ string `json:"tz,omitempty"` 34 | TZLabel string `json:"tz_label"` 35 | TZOffset int `json:"tz_offset"` 36 | Profile UserProfile `json:"profile"` 37 | IsBot bool `json:"is_bot"` 38 | IsAdmin bool `json:"is_admin"` 39 | IsOwner bool `json:"is_owner"` 40 | IsPrimaryOwner bool `json:"is_primary_owner"` 41 | IsRestricted bool `json:"is_restricted"` 42 | IsUltraRestricted bool `json:"is_ultra_restricted"` 43 | Has2FA bool `json:"has_2fa"` 44 | HasFiles bool `json:"has_files"` 45 | Presence string `json:"presence"` 46 | } 47 | 48 | // UserPresence contains details about a user online status 49 | type UserPresence struct { 50 | Presence string `json:"presence,omitempty"` 51 | Online bool `json:"online,omitempty"` 52 | AutoAway bool `json:"auto_away,omitempty"` 53 | ManualAway bool `json:"manual_away,omitempty"` 54 | ConnectionCount int `json:"connection_count,omitempty"` 55 | LastActivity JSONTime `json:"last_activity,omitempty"` 56 | } 57 | 58 | type userResponseFull struct { 59 | Members []User `json:"members,omitempty"` // ListUsers 60 | User `json:"user,omitempty"` // GetUserInfo 61 | UserPresence // GetUserPresence 62 | SlackResponse 63 | } 64 | 65 | func userRequest(path string, values url.Values, debug bool) (*userResponseFull, error) { 66 | response := &userResponseFull{} 67 | err := parseResponse(path, values, response, debug) 68 | if err != nil { 69 | return nil, err 70 | } 71 | if !response.Ok { 72 | return nil, errors.New(response.Error) 73 | } 74 | return response, nil 75 | } 76 | 77 | // GetUserPresence will retrieve the current presence status of given user. 78 | func (api *Slack) GetUserPresence(userId string) (*UserPresence, error) { 79 | values := url.Values{ 80 | "token": {api.config.token}, 81 | "user": {userId}, 82 | } 83 | response, err := userRequest("users.getPresence", values, api.debug) 84 | if err != nil { 85 | return nil, err 86 | } 87 | return &response.UserPresence, nil 88 | } 89 | 90 | // GetUserInfo will retrive the complete user information 91 | func (api *Slack) GetUserInfo(userId string) (*User, error) { 92 | values := url.Values{ 93 | "token": {api.config.token}, 94 | "user": {userId}, 95 | } 96 | response, err := userRequest("users.info", values, api.debug) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return &response.User, nil 101 | } 102 | 103 | // GetUsers returns the list of users (with their detailed information) 104 | func (api *Slack) GetUsers() ([]User, error) { 105 | values := url.Values{ 106 | "token": {api.config.token}, 107 | } 108 | response, err := userRequest("users.list", values, api.debug) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return response.Members, nil 113 | } 114 | 115 | // SetUserAsActive marks the currently authenticated user as active 116 | func (api *Slack) SetUserAsActive() error { 117 | values := url.Values{ 118 | "token": {api.config.token}, 119 | } 120 | _, err := userRequest("users.setActive", values, api.debug) 121 | if err != nil { 122 | return err 123 | } 124 | return nil 125 | } 126 | 127 | // SetUserPresence changes the currently authenticated user presence 128 | func (api *Slack) SetUserPresence(presence string) error { 129 | values := url.Values{ 130 | "token": {api.config.token}, 131 | "presence": {presence}, 132 | } 133 | _, err := userRequest("users.setPresence", values, api.debug) 134 | if err != nil { 135 | return err 136 | } 137 | return nil 138 | 139 | } 140 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/info.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // XXX: Need to implement 9 | type UserPrefs struct { 10 | // "highlight_words":"", 11 | // "user_colors":"", 12 | // "color_names_in_list":true, 13 | // "growls_enabled":true, 14 | // "tz":"Europe\/London", 15 | // "push_dm_alert":true, 16 | // "push_mention_alert":true, 17 | // "push_everything":true, 18 | // "push_idle_wait":2, 19 | // "push_sound":"b2.mp3", 20 | // "push_loud_channels":"", 21 | // "push_mention_channels":"", 22 | // "push_loud_channels_set":"", 23 | // "email_alerts":"instant", 24 | // "email_alerts_sleep_until":0, 25 | // "email_misc":false, 26 | // "email_weekly":true, 27 | // "welcome_message_hidden":false, 28 | // "all_channels_loud":true, 29 | // "loud_channels":"", 30 | // "never_channels":"", 31 | // "loud_channels_set":"", 32 | // "show_member_presence":true, 33 | // "search_sort":"timestamp", 34 | // "expand_inline_imgs":true, 35 | // "expand_internal_inline_imgs":true, 36 | // "expand_snippets":false, 37 | // "posts_formatting_guide":true, 38 | // "seen_welcome_2":true, 39 | // "seen_ssb_prompt":false, 40 | // "search_only_my_channels":false, 41 | // "emoji_mode":"default", 42 | // "has_invited":true, 43 | // "has_uploaded":false, 44 | // "has_created_channel":true, 45 | // "search_exclude_channels":"", 46 | // "messages_theme":"default", 47 | // "webapp_spellcheck":true, 48 | // "no_joined_overlays":false, 49 | // "no_created_overlays":true, 50 | // "dropbox_enabled":false, 51 | // "seen_user_menu_tip_card":true, 52 | // "seen_team_menu_tip_card":true, 53 | // "seen_channel_menu_tip_card":true, 54 | // "seen_message_input_tip_card":true, 55 | // "seen_channels_tip_card":true, 56 | // "seen_domain_invite_reminder":false, 57 | // "seen_member_invite_reminder":false, 58 | // "seen_flexpane_tip_card":true, 59 | // "seen_search_input_tip_card":true, 60 | // "mute_sounds":false, 61 | // "arrow_history":false, 62 | // "tab_ui_return_selects":true, 63 | // "obey_inline_img_limit":true, 64 | // "new_msg_snd":"knock_brush.mp3", 65 | // "collapsible":false, 66 | // "collapsible_by_click":true, 67 | // "require_at":false, 68 | // "mac_ssb_bounce":"", 69 | // "mac_ssb_bullet":true, 70 | // "win_ssb_bullet":true, 71 | // "expand_non_media_attachments":true, 72 | // "show_typing":true, 73 | // "pagekeys_handled":true, 74 | // "last_snippet_type":"", 75 | // "display_real_names_override":0, 76 | // "time24":false, 77 | // "enter_is_special_in_tbt":false, 78 | // "graphic_emoticons":false, 79 | // "convert_emoticons":true, 80 | // "autoplay_chat_sounds":true, 81 | // "ss_emojis":true, 82 | // "sidebar_behavior":"", 83 | // "mark_msgs_read_immediately":true, 84 | // "start_scroll_at_oldest":true, 85 | // "snippet_editor_wrap_long_lines":false, 86 | // "ls_disabled":false, 87 | // "sidebar_theme":"default", 88 | // "sidebar_theme_custom_values":"", 89 | // "f_key_search":false, 90 | // "k_key_omnibox":true, 91 | // "speak_growls":false, 92 | // "mac_speak_voice":"com.apple.speech.synthesis.voice.Alex", 93 | // "mac_speak_speed":250, 94 | // "comma_key_prefs":false, 95 | // "at_channel_suppressed_channels":"", 96 | // "push_at_channel_suppressed_channels":"", 97 | // "prompted_for_email_disabling":false, 98 | // "full_text_extracts":false, 99 | // "no_text_in_notifications":false, 100 | // "muted_channels":"", 101 | // "no_macssb1_banner":false, 102 | // "privacy_policy_seen":true, 103 | // "search_exclude_bots":false, 104 | // "fuzzy_matching":false 105 | } 106 | 107 | // UserDetails contains user details coming in the initial response from StartRTM 108 | type UserDetails struct { 109 | Id string `json:"id"` 110 | Name string `json:"name"` 111 | Created JSONTime `json:"created"` 112 | ManualPresence string `json:"manual_presence"` 113 | Prefs UserPrefs `json:"prefs"` 114 | } 115 | 116 | // JSONTime exists so that we can have a String method converting the date 117 | type JSONTime int64 118 | 119 | // String converts the unix timestamp into a string 120 | func (t JSONTime) String() string { 121 | tm := time.Unix(int64(t), 0) 122 | return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) 123 | } 124 | 125 | // Team contains details about a team 126 | type Team struct { 127 | Id string `json:"id"` 128 | Name string `json:"name"` 129 | Domain string `json:"name"` 130 | } 131 | 132 | // Icons XXX: needs further investigation 133 | type Icons struct { 134 | Image48 string `json:"image_48"` 135 | } 136 | 137 | // Bot contains information about a bot 138 | type Bot struct { 139 | Id string `json:"id"` 140 | Name string `json:"name"` 141 | Deleted bool `json:"deleted"` 142 | Icons Icons `json:"icons"` 143 | } 144 | 145 | // Info contains various details about Users, Channels, Bots and the authenticated user 146 | // It is returned by StartRTM 147 | type Info struct { 148 | Url string `json:"url,omitempty"` 149 | User *UserDetails `json:"self,omitempty"` 150 | Team *Team `json:"team,omitempty"` 151 | Users []User `json:"users,omitempty"` 152 | Channels []Channel `json:"channels,omitempty"` 153 | Groups []Group `json:"groups,omitempty"` 154 | Bots []Bot `json:"bots,omitempty"` 155 | IMs []IM `json:"ims,omitempty"` 156 | } 157 | 158 | type infoResponseFull struct { 159 | Info 160 | SlackWSResponse 161 | } 162 | 163 | // GetBotById returns a bot given a bot id 164 | func (info Info) GetBotById(botId string) *Bot { 165 | for _, bot := range info.Bots { 166 | if bot.Id == botId { 167 | return &bot 168 | } 169 | } 170 | return nil 171 | } 172 | 173 | // GetUserById returns a user given a user id 174 | func (info Info) GetUserById(userId string) *User { 175 | for _, user := range info.Users { 176 | if user.Id == userId { 177 | return &user 178 | } 179 | } 180 | return nil 181 | } 182 | 183 | // GetChannelById returns a channel given a channel id 184 | func (info Info) GetChannelById(channelId string) *Channel { 185 | for _, channel := range info.Channels { 186 | if channel.Id == channelId { 187 | return &channel 188 | } 189 | } 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/chat.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | DEFAULT_MESSAGE_USERNAME = "" 12 | DEFAULT_MESSAGE_ASUSER = false 13 | DEFAULT_MESSAGE_PARSE = "" 14 | DEFAULT_MESSAGE_LINK_NAMES = 0 15 | DEFAULT_MESSAGE_UNFURL_LINKS = false 16 | DEFAULT_MESSAGE_UNFURL_MEDIA = true 17 | DEFAULT_MESSAGE_ICON_URL = "" 18 | DEFAULT_MESSAGE_ICON_EMOJI = "" 19 | DEFAULT_MESSAGE_MARKDOWN = true 20 | DEFAULT_MESSAGE_ESCAPE_TEXT = true 21 | ) 22 | 23 | type chatResponseFull struct { 24 | ChannelId string `json:"channel"` 25 | Timestamp string `json:"ts"` 26 | Text string `json:"text"` 27 | SlackResponse 28 | } 29 | 30 | // AttachmentField contains information for an attachment field 31 | // An Attachment can contain multiple of these 32 | type AttachmentField struct { 33 | Title string `json:"title"` 34 | Value string `json:"value"` 35 | Short bool `json:"short"` 36 | } 37 | 38 | // Attachment contains all the information for an attachment 39 | type Attachment struct { 40 | Fallback string `json:"fallback"` 41 | 42 | Color string `json:"color,omitempty"` 43 | 44 | Pretext string `json:"pretext,omitempty"` 45 | 46 | AuthorName string `json:"author_name,omitempty"` 47 | AuthorLink string `json:"author_link,omitempty"` 48 | AuthorIcon string `json:"author_icon,omitempty"` 49 | 50 | Title string `json:"title,omitempty"` 51 | TitleLink string `json:"title_link,omitempty"` 52 | 53 | Text string `json:"text"` 54 | 55 | ImageURL string `json:"image_url,omitempty"` 56 | ThumbURL string `json:"thumb_url,omitempty"` 57 | 58 | Fields []AttachmentField `json:"fields,omitempty"` 59 | 60 | MarkdownIn []string `json:"mrkdwn_in,omitempty"` 61 | } 62 | 63 | // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request 64 | type PostMessageParameters struct { 65 | Text string 66 | Username string 67 | AsUser bool 68 | Parse string 69 | LinkNames int 70 | Attachments []Attachment 71 | UnfurlLinks bool 72 | UnfurlMedia bool 73 | IconURL string 74 | IconEmoji string 75 | Markdown bool `json:"mrkdwn,omitempty"` 76 | EscapeText bool 77 | } 78 | 79 | // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set 80 | func NewPostMessageParameters() PostMessageParameters { 81 | return PostMessageParameters{ 82 | Username: DEFAULT_MESSAGE_USERNAME, 83 | AsUser: DEFAULT_MESSAGE_ASUSER, 84 | Parse: DEFAULT_MESSAGE_PARSE, 85 | LinkNames: DEFAULT_MESSAGE_LINK_NAMES, 86 | Attachments: nil, 87 | UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, 88 | UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, 89 | IconURL: DEFAULT_MESSAGE_ICON_URL, 90 | IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, 91 | Markdown: DEFAULT_MESSAGE_MARKDOWN, 92 | EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, 93 | } 94 | } 95 | 96 | func chatRequest(path string, values url.Values, debug bool) (*chatResponseFull, error) { 97 | response := &chatResponseFull{} 98 | err := parseResponse(path, values, response, debug) 99 | if err != nil { 100 | return nil, err 101 | } 102 | if !response.Ok { 103 | return nil, errors.New(response.Error) 104 | } 105 | return response, nil 106 | } 107 | 108 | // DeleteMessage deletes a message in a channel 109 | func (api *Slack) DeleteMessage(channelId, messageTimestamp string) (string, string, error) { 110 | values := url.Values{ 111 | "token": {api.config.token}, 112 | "channel": {channelId}, 113 | "ts": {messageTimestamp}, 114 | } 115 | response, err := chatRequest("chat.delete", values, api.debug) 116 | if err != nil { 117 | return "", "", err 118 | } 119 | return response.ChannelId, response.Timestamp, nil 120 | } 121 | 122 | func escapeMessage(message string) string { 123 | /* 124 | & replaced with & 125 | < replaced with < 126 | > replaced with > 127 | */ 128 | replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") 129 | return replacer.Replace(message) 130 | } 131 | 132 | // PostMessage sends a message to a channel 133 | // Message is escaped by default according to https://api.slack.com/docs/formatting 134 | func (api *Slack) PostMessage(channelId string, text string, params PostMessageParameters) (channel string, timestamp string, err error) { 135 | if params.EscapeText { 136 | text = escapeMessage(text) 137 | } 138 | values := url.Values{ 139 | "token": {api.config.token}, 140 | "channel": {channelId}, 141 | "text": {text}, 142 | } 143 | if params.Username != DEFAULT_MESSAGE_USERNAME { 144 | values.Set("username", string(params.Username)) 145 | } 146 | if params.AsUser != DEFAULT_MESSAGE_ASUSER { 147 | values.Set("as_user", "true") 148 | } 149 | if params.Parse != DEFAULT_MESSAGE_PARSE { 150 | values.Set("parse", string(params.Parse)) 151 | } 152 | if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { 153 | values.Set("link_names", "1") 154 | } 155 | if params.Attachments != nil { 156 | attachments, err := json.Marshal(params.Attachments) 157 | if err != nil { 158 | return "", "", err 159 | } 160 | values.Set("attachments", string(attachments)) 161 | } 162 | if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { 163 | values.Set("unfurl_links", "true") 164 | } 165 | if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { 166 | values.Set("unfurl_media", "false") 167 | } 168 | if params.IconURL != DEFAULT_MESSAGE_ICON_URL { 169 | values.Set("icon_url", params.IconURL) 170 | } 171 | if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { 172 | values.Set("icon_emoji", params.IconEmoji) 173 | } 174 | if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { 175 | values.Set("mrkdwn", "false") 176 | } 177 | 178 | response, err := chatRequest("chat.postMessage", values, api.debug) 179 | if err != nil { 180 | return "", "", err 181 | } 182 | return response.ChannelId, response.Timestamp, nil 183 | } 184 | 185 | // UpdateMessage updates a message in a channel 186 | func (api *Slack) UpdateMessage(channelId, timestamp, text string) (string, string, string, error) { 187 | values := url.Values{ 188 | "token": {api.config.token}, 189 | "channel": {channelId}, 190 | "text": {escapeMessage(text)}, 191 | "ts": {timestamp}, 192 | } 193 | response, err := chatRequest("chat.update", values, api.debug) 194 | if err != nil { 195 | return "", "", "", err 196 | } 197 | return response.ChannelId, response.Timestamp, response.Text, nil 198 | } 199 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/files.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | // Add here the defaults in the siten 12 | DEFAULT_FILES_USERID = "" 13 | DEFAULT_FILES_TS_FROM = 0 14 | DEFAULT_FILES_TS_TO = -1 15 | DEFAULT_FILES_TYPES = "all" 16 | DEFAULT_FILES_COUNT = 100 17 | DEFAULT_FILES_PAGE = 1 18 | ) 19 | 20 | // Comment contains all the information relative to a comment 21 | type Comment struct { 22 | Id string `json:"id"` 23 | Timestamp JSONTime `json:"timestamp"` 24 | UserId string `json:"user"` 25 | Comment string `json:"comment"` 26 | Created JSONTime `json:"created,omitempty"` 27 | } 28 | 29 | // File contains all the information for a file 30 | type File struct { 31 | Id string `json:"id"` 32 | Created JSONTime `json:"created"` 33 | Timestamp JSONTime `json:"timestamp"` 34 | 35 | Name string `json:"name"` 36 | Title string `json:"title"` 37 | Mimetype string `json:"mimetype"` 38 | Filetype string `json:"filetype"` 39 | PrettyType string `json:"pretty_type"` 40 | UserId string `json:"user"` 41 | 42 | Mode string `json:"mode"` 43 | Editable bool `json:"editable"` 44 | IsExternal bool `json:"is_external"` 45 | ExternalType string `json:"external_type"` 46 | 47 | Size int `json:"size"` 48 | 49 | URL string `json:"url"` 50 | URLDownload string `json:"url_download"` 51 | URLPrivate string `json:"url_private"` 52 | URLPrivateDownload string `json:"url_private_download"` 53 | 54 | Thumb64 string `json:"thumb_64"` 55 | Thumb80 string `json:"thumb_80"` 56 | Thumb360 string `json:"thumb_360"` 57 | Thumb360Gif string `json:"thumb_360_gif"` 58 | Thumb360W int `json:"thumb_360_w"` 59 | Thumb360H int `json:"thumb_360_h"` 60 | 61 | Permalink string `json:"permalink"` 62 | EditLink string `json:"edit_link"` 63 | Preview string `json:"preview"` 64 | PreviewHighlight string `json:"preview_highlight"` 65 | Lines int `json:"lines"` 66 | LinesMore int `json:"lines_more"` 67 | 68 | IsPublic bool `json:"is_public"` 69 | PublicURLShared bool `json:"public_url_shared"` 70 | Channels []string `json:"channels"` 71 | Groups []string `json:"groups"` 72 | InitialComment Comment `json:"initial_comment"` 73 | NumStars int `json:"num_stars"` 74 | IsStarred bool `json:"is_starred"` 75 | } 76 | 77 | // FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request 78 | type FileUploadParameters struct { 79 | File string 80 | Content string 81 | Filetype string 82 | Filename string 83 | Title string 84 | InitialComment string 85 | Channels []string 86 | } 87 | 88 | // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request 89 | type GetFilesParameters struct { 90 | UserId string 91 | TimestampFrom JSONTime 92 | TimestampTo JSONTime 93 | Types string 94 | Count int 95 | Page int 96 | } 97 | 98 | type fileResponseFull struct { 99 | File `json:"file"` 100 | Paging `json:"paging"` 101 | Comments []Comment `json:"comments"` 102 | Files []File `json:"files"` 103 | 104 | SlackResponse 105 | } 106 | 107 | // NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set 108 | func NewGetFilesParameters() GetFilesParameters { 109 | return GetFilesParameters{ 110 | UserId: DEFAULT_FILES_USERID, 111 | TimestampFrom: DEFAULT_FILES_TS_FROM, 112 | TimestampTo: DEFAULT_FILES_TS_TO, 113 | Types: DEFAULT_FILES_TYPES, 114 | Count: DEFAULT_FILES_COUNT, 115 | Page: DEFAULT_FILES_PAGE, 116 | } 117 | } 118 | 119 | func fileRequest(path string, values url.Values, debug bool) (*fileResponseFull, error) { 120 | response := &fileResponseFull{} 121 | err := parseResponse(path, values, response, debug) 122 | if err != nil { 123 | return nil, err 124 | } 125 | if !response.Ok { 126 | return nil, errors.New(response.Error) 127 | } 128 | return response, nil 129 | } 130 | 131 | // GetFileInfo retrieves a file and related comments 132 | func (api *Slack) GetFileInfo(fileId string, count, page int) (*File, []Comment, *Paging, error) { 133 | values := url.Values{ 134 | "token": {api.config.token}, 135 | "file": {fileId}, 136 | "count": {strconv.Itoa(count)}, 137 | "page": {strconv.Itoa(page)}, 138 | } 139 | response, err := fileRequest("files.info", values, api.debug) 140 | if err != nil { 141 | return nil, nil, nil, err 142 | } 143 | return &response.File, response.Comments, &response.Paging, nil 144 | } 145 | 146 | // GetFiles retrieves all files according to the parameters given 147 | func (api *Slack) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { 148 | values := url.Values{ 149 | "token": {api.config.token}, 150 | } 151 | if params.UserId != DEFAULT_FILES_USERID { 152 | values.Add("user", params.UserId) 153 | } 154 | // XXX: this is broken. fix it with a proper unix timestamp 155 | if params.TimestampFrom != DEFAULT_FILES_TS_FROM { 156 | values.Add("ts_from", params.TimestampFrom.String()) 157 | } 158 | if params.TimestampTo != DEFAULT_FILES_TS_TO { 159 | values.Add("ts_to", params.TimestampTo.String()) 160 | } 161 | if params.Types != DEFAULT_FILES_TYPES { 162 | values.Add("types", params.Types) 163 | } 164 | if params.Count != DEFAULT_FILES_COUNT { 165 | values.Add("count", strconv.Itoa(params.Count)) 166 | } 167 | if params.Page != DEFAULT_FILES_PAGE { 168 | values.Add("page", strconv.Itoa(params.Page)) 169 | } 170 | response, err := fileRequest("files.list", values, api.debug) 171 | if err != nil { 172 | return nil, nil, err 173 | } 174 | return response.Files, &response.Paging, nil 175 | } 176 | 177 | // UploadFile uploads a file 178 | func (api *Slack) UploadFile(params FileUploadParameters) (file *File, err error) { 179 | // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More 180 | // investigation needed, but for now this will do. 181 | _, err = api.AuthTest() 182 | if err != nil { 183 | return nil, err 184 | } 185 | response := &fileResponseFull{} 186 | values := url.Values{ 187 | "token": {api.config.token}, 188 | } 189 | if params.Filetype != "" { 190 | values.Add("filetype", params.Filetype) 191 | } 192 | if params.Filename != "" { 193 | values.Add("filename", params.Filename) 194 | } 195 | if params.Title != "" { 196 | values.Add("title", params.Title) 197 | } 198 | if params.InitialComment != "" { 199 | values.Add("initial_comment", params.InitialComment) 200 | } 201 | if len(params.Channels) != 0 { 202 | values.Add("channels", strings.Join(params.Channels, ",")) 203 | } 204 | if params.Content != "" { 205 | values.Add("content", params.Content) 206 | err = parseResponse("files.upload", values, response, api.debug) 207 | } else if params.File != "" { 208 | err = parseResponseMultipart("files.upload", params.File, values, response, api.debug) 209 | } 210 | if err != nil { 211 | return nil, err 212 | } 213 | if !response.Ok { 214 | return nil, errors.New(response.Error) 215 | } 216 | return &response.File, nil 217 | } 218 | 219 | // DeleteFile deletes a file 220 | func (api *Slack) DeleteFile(fileId string) error { 221 | values := url.Values{ 222 | "token": {api.config.token}, 223 | "file": {fileId}, 224 | } 225 | _, err := fileRequest("files.delete", values, api.debug) 226 | if err != nil { 227 | return err 228 | } 229 | return nil 230 | 231 | } 232 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/websocket.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "net/url" 10 | "strconv" 11 | "sync" 12 | "time" 13 | 14 | "golang.org/x/net/websocket" 15 | ) 16 | 17 | type MessageEvent Message 18 | 19 | type SlackWS struct { 20 | conn *websocket.Conn 21 | messageId int 22 | mutex sync.Mutex 23 | pings map[int]time.Time 24 | Slack 25 | } 26 | 27 | // AckMessage is used for messages received in reply to other messages 28 | type AckMessage struct { 29 | ReplyTo int `json:"reply_to"` 30 | Timestamp string `json:"ts"` 31 | Text string `json:"text"` 32 | SlackWSResponse 33 | } 34 | 35 | type SlackWSResponse struct { 36 | Ok bool `json:"ok"` 37 | Error *SlackWSError `json:"error"` 38 | } 39 | 40 | type SlackWSError struct { 41 | Code int 42 | Msg string 43 | } 44 | 45 | type SlackEvent struct { 46 | Type uint64 47 | Data interface{} 48 | } 49 | 50 | type JSONTimeString string 51 | 52 | // String converts the unix timestamp into a string 53 | func (t JSONTimeString) String() string { 54 | if t == "" { 55 | return "" 56 | } 57 | floatN, err := strconv.ParseFloat(string(t), 64) 58 | if err != nil { 59 | log.Panicln(err) 60 | return "" 61 | } 62 | timeStr := int64(floatN) 63 | tm := time.Unix(int64(timeStr), 0) 64 | return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) 65 | } 66 | 67 | func (s SlackWSError) Error() string { 68 | return s.Msg 69 | } 70 | 71 | var portMapping = map[string]string{"ws": "80", "wss": "443"} 72 | 73 | func fixUrlPort(orig string) (string, error) { 74 | urlObj, err := url.ParseRequestURI(orig) 75 | if err != nil { 76 | return "", err 77 | } 78 | _, _, err = net.SplitHostPort(urlObj.Host) 79 | if err != nil { 80 | return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil 81 | } 82 | return orig, nil 83 | } 84 | 85 | func (api *Slack) StartRTM(protocol, origin string) (*SlackWS, error) { 86 | response := &infoResponseFull{} 87 | err := parseResponse("rtm.start", url.Values{"token": {api.config.token}}, response, api.debug) 88 | if err != nil { 89 | return nil, err 90 | } 91 | if !response.Ok { 92 | return nil, response.Error 93 | } 94 | api.info = response.Info 95 | // websocket.Dial does not accept url without the port (yet) 96 | // Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3 97 | // but slack returns the address with no port, so we have to fix it 98 | api.info.Url, err = fixUrlPort(api.info.Url) 99 | if err != nil { 100 | return nil, err 101 | } 102 | api.config.protocol, api.config.origin = protocol, origin 103 | wsApi := &SlackWS{Slack: *api} 104 | wsApi.conn, err = websocket.Dial(api.info.Url, api.config.protocol, api.config.origin) 105 | if err != nil { 106 | return nil, err 107 | } 108 | wsApi.pings = make(map[int]time.Time) 109 | return wsApi, nil 110 | } 111 | 112 | func (api *SlackWS) Ping() error { 113 | api.mutex.Lock() 114 | defer api.mutex.Unlock() 115 | api.messageId++ 116 | msg := &Ping{Id: api.messageId, Type: "ping"} 117 | if err := websocket.JSON.Send(api.conn, msg); err != nil { 118 | return err 119 | } 120 | // TODO: What happens if we already have this id? 121 | api.pings[api.messageId] = time.Now() 122 | return nil 123 | } 124 | 125 | func (api *SlackWS) Keepalive(interval time.Duration) { 126 | ticker := time.NewTicker(interval) 127 | defer ticker.Stop() 128 | 129 | for { 130 | select { 131 | case <-ticker.C: 132 | if err := api.Ping(); err != nil { 133 | log.Fatal(err) 134 | } 135 | } 136 | } 137 | } 138 | 139 | func (api *SlackWS) SendMessage(msg *OutgoingMessage) error { 140 | if msg == nil { 141 | return fmt.Errorf("Can't send a nil message") 142 | } 143 | 144 | if err := websocket.JSON.Send(api.conn, *msg); err != nil { 145 | return err 146 | } 147 | return nil 148 | } 149 | 150 | func (api *SlackWS) HandleIncomingEvents(ch chan SlackEvent) { 151 | for { 152 | event := json.RawMessage{} 153 | if err := websocket.JSON.Receive(api.conn, &event); err == io.EOF { 154 | //log.Println("Derpi derp, should we destroy conn and start over?") 155 | //if err = api.StartRTM(); err != nil { 156 | // log.Fatal(err) 157 | //} 158 | // should we reconnect here? 159 | if !api.conn.IsClientConn() { 160 | api.conn, err = websocket.Dial(api.info.Url, api.config.protocol, api.config.origin) 161 | if err != nil { 162 | log.Panic(err) 163 | } 164 | } 165 | // XXX: check for timeout and implement exponential backoff 166 | } else if err != nil { 167 | log.Panic(err) 168 | } 169 | if len(event) == 0 { 170 | if api.debug { 171 | log.Println("Event Empty. WTF?") 172 | } 173 | } else { 174 | if api.debug { 175 | log.Println(string(event[:])) 176 | } 177 | api.handleEvent(ch, event) 178 | } 179 | time.Sleep(time.Millisecond * 500) 180 | } 181 | } 182 | 183 | func (api *SlackWS) handleEvent(ch chan SlackEvent, event json.RawMessage) { 184 | em := Event{} 185 | err := json.Unmarshal(event, &em) 186 | if err != nil { 187 | log.Fatal(err) 188 | } 189 | switch em.Type { 190 | case "": 191 | // try ok 192 | ack := AckMessage{} 193 | if err = json.Unmarshal(event, &ack); err != nil { 194 | log.Fatal(err) 195 | } 196 | 197 | if ack.Ok { 198 | // TODO: Send the ack back (is this useful?) 199 | //ch <- SlackEvent{Type: EventAck, Data: ack} 200 | log.Printf("Received an ok for: %d", ack.ReplyTo) 201 | return 202 | } 203 | 204 | // Send the error to the user 205 | ch <- SlackEvent{Data: ack.Error} 206 | case "hello": 207 | ch <- SlackEvent{Data: HelloEvent{}} 208 | case "pong": 209 | pong := Pong{} 210 | if err = json.Unmarshal(event, &pong); err != nil { 211 | log.Fatal(err) 212 | } 213 | api.mutex.Lock() 214 | latency := time.Since(api.pings[pong.ReplyTo]) 215 | api.mutex.Unlock() 216 | ch <- SlackEvent{Data: LatencyReport{Value: latency}} 217 | default: 218 | callEvent(em.Type, ch, event) 219 | } 220 | } 221 | 222 | func callEvent(eventType string, ch chan SlackEvent, event json.RawMessage) { 223 | eventMapping := map[string]interface{}{ 224 | "message": &MessageEvent{}, 225 | "presence_change": &PresenceChangeEvent{}, 226 | "user_typing": &UserTypingEvent{}, 227 | 228 | "channel_marked": &ChannelMarkedEvent{}, 229 | "channel_created": &ChannelCreatedEvent{}, 230 | "channel_joined": &ChannelJoinedEvent{}, 231 | "channel_left": &ChannelLeftEvent{}, 232 | "channel_deleted": &ChannelDeletedEvent{}, 233 | "channel_rename": &ChannelRenameEvent{}, 234 | "channel_archive": &ChannelArchiveEvent{}, 235 | "channel_unarchive": &ChannelUnarchiveEvent{}, 236 | "channel_history_changed": &ChannelHistoryChangedEvent{}, 237 | 238 | "im_created": &IMCreatedEvent{}, 239 | "im_open": &IMOpenEvent{}, 240 | "im_close": &IMCloseEvent{}, 241 | "im_marked": &IMMarkedEvent{}, 242 | "im_history_changed": &IMHistoryChangedEvent{}, 243 | 244 | "group_marked": &GroupMarkedEvent{}, 245 | "group_open": &GroupOpenEvent{}, 246 | "group_joined": &GroupJoinedEvent{}, 247 | "group_left": &GroupLeftEvent{}, 248 | "group_close": &GroupCloseEvent{}, 249 | "group_rename": &GroupRenameEvent{}, 250 | "group_archive": &GroupArchiveEvent{}, 251 | "group_unarchive": &GroupUnarchiveEvent{}, 252 | "group_history_changed": &GroupHistoryChangedEvent{}, 253 | 254 | "file_created": &FileCreatedEvent{}, 255 | "file_shared": &FileSharedEvent{}, 256 | "file_unshared": &FileUnsharedEvent{}, 257 | "file_public": &FilePublicEvent{}, 258 | "file_private": &FilePrivateEvent{}, 259 | "file_change": &FileChangeEvent{}, 260 | "file_deleted": &FileDeletedEvent{}, 261 | "file_comment_added": &FileCommentAddedEvent{}, 262 | "file_comment_edited": &FileCommentEditedEvent{}, 263 | "file_comment_deleted": &FileCommentDeletedEvent{}, 264 | 265 | "star_added": &StarAddedEvent{}, 266 | "star_removed": &StarRemovedEvent{}, 267 | 268 | "pref_change": &PrefChangeEvent{}, 269 | 270 | "team_join": &TeamJoinEvent{}, 271 | "team_rename": &TeamRenameEvent{}, 272 | "team_pref_change": &TeamPrefChangeEvent{}, 273 | "team_domain_change": &TeamDomainChangeEvent{}, 274 | "team_migration_started": &TeamMigrationStartedEvent{}, 275 | 276 | "manual_presence_change": &ManualPresenceChangeEvent{}, 277 | 278 | "user_change": &UserChangeEvent{}, 279 | 280 | "emoji_changed": &EmojiChangedEvent{}, 281 | 282 | "commands_changed": &CommandsChangedEvent{}, 283 | 284 | "email_domain_changed": &EmailDomainChangedEvent{}, 285 | 286 | "bot_added": &BotAddedEvent{}, 287 | "bot_changed": &BotChangedEvent{}, 288 | 289 | "accounts_changed": &AccountsChangedEvent{}, 290 | } 291 | 292 | msg := eventMapping[eventType] 293 | if msg == nil { 294 | log.Printf("XXX: Not implemented yet: %s -> %v", eventType, event) 295 | } 296 | if err := json.Unmarshal(event, &msg); err != nil { 297 | log.Fatal(err) 298 | } 299 | ch <- SlackEvent{Data: msg} 300 | } 301 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/channels.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | type channelResponseFull struct { 10 | Channel Channel `json:"channel"` 11 | Channels []Channel `json:"channels"` 12 | Purpose string `json:"purpose"` 13 | Topic string `json:"topic"` 14 | NotInChannel bool `json:"not_in_channel"` 15 | History 16 | SlackResponse 17 | } 18 | 19 | // ChannelTopic contains information about the channel topic 20 | type ChannelTopic struct { 21 | Value string `json:"value"` 22 | Creator string `json:"creator"` 23 | LastSet JSONTime `json:"last_set"` 24 | } 25 | 26 | // ChannelPurpose contains information about the channel purpose 27 | type ChannelPurpose struct { 28 | Value string `json:"value"` 29 | Creator string `json:"creator"` 30 | LastSet JSONTime `json:"last_set"` 31 | } 32 | 33 | type BaseChannel struct { 34 | Id string `json:"id"` 35 | Created JSONTime `json:"created"` 36 | IsOpen bool `json:"is_open"` 37 | LastRead string `json:"last_read,omitempty"` 38 | Latest Message `json:"latest,omitempty"` 39 | UnreadCount int `json:"unread_count,omitempty"` 40 | UnreadCountDisplay int `json:"unread_count_display,omitempty"` 41 | } 42 | 43 | // Channel contains information about the channel 44 | type Channel struct { 45 | BaseChannel 46 | Name string `json:"name"` 47 | IsChannel bool `json:"is_channel"` 48 | Creator string `json:"creator"` 49 | IsArchived bool `json:"is_archived"` 50 | IsGeneral bool `json:"is_general"` 51 | Members []string `json:"members"` 52 | Topic ChannelTopic `json:"topic"` 53 | Purpose ChannelPurpose `json:"purpose"` 54 | IsMember bool `json:"is_member"` 55 | LastRead string `json:"last_read,omitempty"` 56 | Latest *Message `json:"latest,omitempty"` 57 | UnreadCount int `json:"unread_count,omitempty"` 58 | NumMembers int `json:"num_members,omitempty"` 59 | } 60 | 61 | func channelRequest(path string, values url.Values, debug bool) (*channelResponseFull, error) { 62 | response := &channelResponseFull{} 63 | err := parseResponse(path, values, response, debug) 64 | if err != nil { 65 | return nil, err 66 | } 67 | if !response.Ok { 68 | return nil, errors.New(response.Error) 69 | } 70 | return response, nil 71 | } 72 | 73 | // ArchiveChannel archives the given channel 74 | func (api *Slack) ArchiveChannel(channelId string) error { 75 | values := url.Values{ 76 | "token": {api.config.token}, 77 | "channel": {channelId}, 78 | } 79 | _, err := channelRequest("channels.archive", values, api.debug) 80 | if err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | // UnarchiveChannel unarchives the given channel 87 | func (api *Slack) UnarchiveChannel(channelId string) error { 88 | values := url.Values{ 89 | "token": {api.config.token}, 90 | "channel": {channelId}, 91 | } 92 | _, err := channelRequest("channels.unarchive", values, api.debug) 93 | if err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | 99 | // CreateChannel creates a channel with the given name and returns a *Channel 100 | func (api *Slack) CreateChannel(channel string) (*Channel, error) { 101 | values := url.Values{ 102 | "token": {api.config.token}, 103 | "name": {channel}, 104 | } 105 | response, err := channelRequest("channels.create", values, api.debug) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return &response.Channel, nil 110 | } 111 | 112 | // GetChannelHistory retrieves the channel history 113 | func (api *Slack) GetChannelHistory(channelId string, params HistoryParameters) (*History, error) { 114 | values := url.Values{ 115 | "token": {api.config.token}, 116 | "channel": {channelId}, 117 | } 118 | if params.Latest != DEFAULT_HISTORY_LATEST { 119 | values.Add("latest", params.Latest) 120 | } 121 | if params.Oldest != DEFAULT_HISTORY_OLDEST { 122 | values.Add("oldest", params.Oldest) 123 | } 124 | if params.Count != DEFAULT_HISTORY_COUNT { 125 | values.Add("count", strconv.Itoa(params.Count)) 126 | } 127 | if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { 128 | if params.Inclusive { 129 | values.Add("inclusive", "1") 130 | } else { 131 | values.Add("inclusive", "0") 132 | } 133 | } 134 | response, err := channelRequest("channels.history", values, api.debug) 135 | if err != nil { 136 | return nil, err 137 | } 138 | return &response.History, nil 139 | } 140 | 141 | // GetChannelInfo retrieves the given channel 142 | func (api *Slack) GetChannelInfo(channelId string) (*Channel, error) { 143 | values := url.Values{ 144 | "token": {api.config.token}, 145 | "channel": {channelId}, 146 | } 147 | response, err := channelRequest("channels.info", values, api.debug) 148 | if err != nil { 149 | return nil, err 150 | } 151 | return &response.Channel, nil 152 | } 153 | 154 | // InviteUserToChannel invites a user to a given channel and returns a *Channel 155 | func (api *Slack) InviteUserToChannel(channelId, userId string) (*Channel, error) { 156 | values := url.Values{ 157 | "token": {api.config.token}, 158 | "channel": {channelId}, 159 | "user": {userId}, 160 | } 161 | response, err := channelRequest("channels.invite", values, api.debug) 162 | if err != nil { 163 | return nil, err 164 | } 165 | return &response.Channel, nil 166 | } 167 | 168 | // JoinChannel joins the currently authenticated user to a channel 169 | func (api *Slack) JoinChannel(channel string) (*Channel, error) { 170 | values := url.Values{ 171 | "token": {api.config.token}, 172 | "name": {channel}, 173 | } 174 | response, err := channelRequest("channels.join", values, api.debug) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return &response.Channel, nil 179 | } 180 | 181 | // LeaveChannel makes the authenticated user leave the given channel 182 | func (api *Slack) LeaveChannel(channelId string) (bool, error) { 183 | values := url.Values{ 184 | "token": {api.config.token}, 185 | "channel": {channelId}, 186 | } 187 | response, err := channelRequest("channels.leave", values, api.debug) 188 | if err != nil { 189 | return false, err 190 | } 191 | if response.NotInChannel { 192 | return response.NotInChannel, nil 193 | } 194 | return false, nil 195 | } 196 | 197 | // KickUserFromChannel kicks a user from a given channel 198 | func (api *Slack) KickUserFromChannel(channelId, userId string) error { 199 | values := url.Values{ 200 | "token": {api.config.token}, 201 | "channel": {channelId}, 202 | "user": {userId}, 203 | } 204 | _, err := channelRequest("channels.kick", values, api.debug) 205 | if err != nil { 206 | return err 207 | } 208 | return nil 209 | } 210 | 211 | // GetChannels retrieves all the channels 212 | func (api *Slack) GetChannels(excludeArchived bool) ([]Channel, error) { 213 | values := url.Values{ 214 | "token": {api.config.token}, 215 | } 216 | if excludeArchived { 217 | values.Add("exclude_archived", "1") 218 | } 219 | response, err := channelRequest("channels.list", values, api.debug) 220 | if err != nil { 221 | return nil, err 222 | } 223 | return response.Channels, nil 224 | } 225 | 226 | // SetChannelReadMark sets the read mark of a given channel to a specific point 227 | // Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a 228 | // timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls 229 | // (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A 230 | // timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. 231 | func (api *Slack) SetChannelReadMark(channelId, ts string) error { 232 | values := url.Values{ 233 | "token": {api.config.token}, 234 | "channel": {channelId}, 235 | "ts": {ts}, 236 | } 237 | _, err := channelRequest("channels.mark", values, api.debug) 238 | if err != nil { 239 | return err 240 | } 241 | return nil 242 | } 243 | 244 | // RenameChannel renames a given channel 245 | func (api *Slack) RenameChannel(channelId, name string) (*Channel, error) { 246 | values := url.Values{ 247 | "token": {api.config.token}, 248 | "channel": {channelId}, 249 | "name": {name}, 250 | } 251 | // XXX: the created entry in this call returns a string instead of a number 252 | // so I may have to do some workaround to solve it. 253 | response, err := channelRequest("channels.rename", values, api.debug) 254 | if err != nil { 255 | return nil, err 256 | } 257 | return &response.Channel, nil 258 | 259 | } 260 | 261 | // SetChannelPurpose sets the channel purpose and returns the purpose that was 262 | // successfully set 263 | func (api *Slack) SetChannelPurpose(channelId, purpose string) (string, error) { 264 | values := url.Values{ 265 | "token": {api.config.token}, 266 | "channel": {channelId}, 267 | "purpose": {purpose}, 268 | } 269 | response, err := channelRequest("channels.setPurpose", values, api.debug) 270 | if err != nil { 271 | return "", err 272 | } 273 | return response.Purpose, nil 274 | } 275 | 276 | // SetChannelTopic sets the channel topic and returns the topic that was successfully set 277 | func (api *Slack) SetChannelTopic(channelId, topic string) (string, error) { 278 | values := url.Values{ 279 | "token": {api.config.token}, 280 | "channel": {channelId}, 281 | "topic": {topic}, 282 | } 283 | response, err := channelRequest("channels.setTopic", values, api.debug) 284 | if err != nil { 285 | return "", err 286 | } 287 | return response.Topic, nil 288 | } 289 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/nlopes/slack/groups.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | // Group contains all the information for a group 10 | type Group struct { 11 | BaseChannel 12 | Name string `json:"name"` 13 | IsGroup bool `json:"is_group"` 14 | Creator string `json:"creator"` 15 | IsArchived bool `json:"is_archived"` 16 | IsOpen bool `json:"is_open,omitempty"` 17 | Members []string `json:"members"` 18 | Topic ChannelTopic `json:"topic"` 19 | Purpose ChannelPurpose `json:"purpose"` 20 | LastRead string `json:"last_read,omitempty"` 21 | Latest *Message `json:"latest,omitempty"` 22 | UnreadCount int `json:"unread_count,omitempty"` 23 | NumMembers int `json:"num_members,omitempty"` 24 | UnreadCountDisplay int `json:"unread_count_display,omitempty"` 25 | } 26 | 27 | type groupResponseFull struct { 28 | Group Group `json:"group"` 29 | Groups []Group `json:"groups"` 30 | Purpose string `json:"purpose"` 31 | Topic string `json:"topic"` 32 | NotInGroup bool `json:"not_in_group"` 33 | NoOp bool `json:"no_op"` 34 | AlreadyClosed bool `json:"already_closed"` 35 | AlreadyOpen bool `json:"already_open"` 36 | AlreadyInGroup bool `json:"already_in_group"` 37 | Channel Channel `json:"channel"` 38 | History 39 | SlackResponse 40 | } 41 | 42 | func groupRequest(path string, values url.Values, debug bool) (*groupResponseFull, error) { 43 | response := &groupResponseFull{} 44 | err := parseResponse(path, values, response, debug) 45 | if err != nil { 46 | return nil, err 47 | } 48 | if !response.Ok { 49 | return nil, errors.New(response.Error) 50 | } 51 | return response, nil 52 | } 53 | 54 | // ArchiveGroup archives a private group 55 | func (api *Slack) ArchiveGroup(groupId string) error { 56 | values := url.Values{ 57 | "token": {api.config.token}, 58 | "channel": {groupId}, 59 | } 60 | _, err := groupRequest("groups.archive", values, api.debug) 61 | if err != nil { 62 | return err 63 | } 64 | return nil 65 | } 66 | 67 | // UnarchiveGroup unarchives a private group 68 | func (api *Slack) UnarchiveGroup(groupId string) error { 69 | values := url.Values{ 70 | "token": {api.config.token}, 71 | "channel": {groupId}, 72 | } 73 | _, err := groupRequest("groups.unarchive", values, api.debug) 74 | if err != nil { 75 | return err 76 | } 77 | return nil 78 | } 79 | 80 | // CreateGroup creates a private group 81 | func (api *Slack) CreateGroup(group string) (*Group, error) { 82 | values := url.Values{ 83 | "token": {api.config.token}, 84 | "name": {group}, 85 | } 86 | response, err := groupRequest("groups.create", values, api.debug) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return &response.Group, nil 91 | } 92 | 93 | // CreateChildGroup creates a new private group archiving the old one 94 | // This method takes an existing private group and performs the following steps: 95 | // 1. Renames the existing group (from "example" to "example-archived"). 96 | // 2. Archives the existing group. 97 | // 3. Creates a new group with the name of the existing group. 98 | // 4. Adds all members of the existing group to the new group. 99 | func (api *Slack) CreateChildGroup(groupId string) (*Group, error) { 100 | values := url.Values{ 101 | "token": {api.config.token}, 102 | "channel": {groupId}, 103 | } 104 | response, err := groupRequest("groups.createChild", values, api.debug) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return &response.Group, nil 109 | } 110 | 111 | // CloseGroup closes a private group 112 | func (api *Slack) CloseGroup(groupId string) (bool, bool, error) { 113 | values := url.Values{ 114 | "token": {api.config.token}, 115 | "channel": {groupId}, 116 | } 117 | response, err := imRequest("groups.close", values, api.debug) 118 | if err != nil { 119 | return false, false, err 120 | } 121 | return response.NoOp, response.AlreadyClosed, nil 122 | } 123 | 124 | // GetGroupHistory retrieves message history for a give group 125 | func (api *Slack) GetGroupHistory(groupId string, params HistoryParameters) (*History, error) { 126 | values := url.Values{ 127 | "token": {api.config.token}, 128 | "channel": {groupId}, 129 | } 130 | if params.Latest != DEFAULT_HISTORY_LATEST { 131 | values.Add("latest", params.Latest) 132 | } 133 | if params.Oldest != DEFAULT_HISTORY_OLDEST { 134 | values.Add("oldest", params.Oldest) 135 | } 136 | if params.Count != DEFAULT_HISTORY_COUNT { 137 | values.Add("count", strconv.Itoa(params.Count)) 138 | } 139 | if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { 140 | if params.Inclusive { 141 | values.Add("inclusive", "1") 142 | } else { 143 | values.Add("inclusive", "0") 144 | } 145 | } 146 | response, err := groupRequest("groups.history", values, api.debug) 147 | if err != nil { 148 | return nil, err 149 | } 150 | return &response.History, nil 151 | } 152 | 153 | // InviteUserToGroup invites a user to a group 154 | func (api *Slack) InviteUserToGroup(groupId, userId string) (*Group, bool, error) { 155 | values := url.Values{ 156 | "token": {api.config.token}, 157 | "channel": {groupId}, 158 | "user": {userId}, 159 | } 160 | response, err := groupRequest("groups.invite", values, api.debug) 161 | if err != nil { 162 | return nil, false, err 163 | } 164 | return &response.Group, response.AlreadyInGroup, nil 165 | } 166 | 167 | // LeaveGroup makes authenticated user leave the group 168 | func (api *Slack) LeaveGroup(groupId string) error { 169 | values := url.Values{ 170 | "token": {api.config.token}, 171 | "channel": {groupId}, 172 | } 173 | _, err := groupRequest("groups.leave", values, api.debug) 174 | if err != nil { 175 | return err 176 | } 177 | return nil 178 | } 179 | 180 | // KickUserFromGroup kicks a user from a group 181 | func (api *Slack) KickUserFromGroup(groupId, userId string) error { 182 | values := url.Values{ 183 | "token": {api.config.token}, 184 | "channel": {groupId}, 185 | "user": {userId}, 186 | } 187 | _, err := groupRequest("groups.kick", values, api.debug) 188 | if err != nil { 189 | return err 190 | } 191 | return nil 192 | } 193 | 194 | // GetGroups retrieves all groups 195 | func (api *Slack) GetGroups(excludeArchived bool) ([]Group, error) { 196 | values := url.Values{ 197 | "token": {api.config.token}, 198 | } 199 | if excludeArchived { 200 | values.Add("exclude_archived", "1") 201 | } 202 | response, err := groupRequest("groups.list", values, api.debug) 203 | if err != nil { 204 | return nil, err 205 | } 206 | return response.Groups, nil 207 | } 208 | 209 | // GetGroupInfo retrieves the given group 210 | func (api *Slack) GetGroupInfo(groupId string) (*Group, error) { 211 | values := url.Values{ 212 | "token": {api.config.token}, 213 | "channel": {groupId}, 214 | } 215 | response, err := groupRequest("groups.info", values, api.debug) 216 | if err != nil { 217 | return nil, err 218 | } 219 | return &response.Group, nil 220 | } 221 | 222 | // SetGroupReadMark sets the read mark on a private group 223 | // Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a 224 | // timer before making the call. In this way, any further updates needed during the timeout will not generate extra 225 | // calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live 226 | // channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. 227 | func (api *Slack) SetGroupReadMark(groupId, ts string) error { 228 | values := url.Values{ 229 | "token": {api.config.token}, 230 | "channel": {groupId}, 231 | "ts": {ts}, 232 | } 233 | _, err := groupRequest("groups.mark", values, api.debug) 234 | if err != nil { 235 | return err 236 | } 237 | return nil 238 | } 239 | 240 | // OpenGroup opens a private group 241 | func (api *Slack) OpenGroup(groupId string) (bool, bool, error) { 242 | values := url.Values{ 243 | "token": {api.config.token}, 244 | "user": {groupId}, 245 | } 246 | response, err := groupRequest("groups.open", values, api.debug) 247 | if err != nil { 248 | return false, false, err 249 | } 250 | return response.NoOp, response.AlreadyOpen, nil 251 | } 252 | 253 | // RenameGroup renames a group 254 | // XXX: They return a channel, not a group. What is this crap? :( 255 | // Inconsistent api it seems. 256 | func (api *Slack) RenameGroup(groupId, name string) (*Channel, error) { 257 | values := url.Values{ 258 | "token": {api.config.token}, 259 | "channel": {groupId}, 260 | "name": {name}, 261 | } 262 | // XXX: the created entry in this call returns a string instead of a number 263 | // so I may have to do some workaround to solve it. 264 | response, err := groupRequest("groups.rename", values, api.debug) 265 | if err != nil { 266 | return nil, err 267 | } 268 | return &response.Channel, nil 269 | 270 | } 271 | 272 | // SetGroupPurpose sets the group purpose 273 | func (api *Slack) SetGroupPurpose(groupId, purpose string) (string, error) { 274 | values := url.Values{ 275 | "token": {api.config.token}, 276 | "channel": {groupId}, 277 | "purpose": {purpose}, 278 | } 279 | response, err := groupRequest("groups.setPurpose", values, api.debug) 280 | if err != nil { 281 | return "", err 282 | } 283 | return response.Purpose, nil 284 | } 285 | 286 | // SetGroupTopic sets the group topic 287 | func (api *Slack) SetGroupTopic(groupId, topic string) (string, error) { 288 | values := url.Values{ 289 | "token": {api.config.token}, 290 | "channel": {groupId}, 291 | "topic": {topic}, 292 | } 293 | response, err := groupRequest("groups.setTopic", values, api.debug) 294 | if err != nil { 295 | return "", err 296 | } 297 | return response.Topic, nil 298 | } 299 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/websocket/websocket_test.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 | "bytes" 9 | "fmt" 10 | "io" 11 | "log" 12 | "net" 13 | "net/http" 14 | "net/http/httptest" 15 | "net/url" 16 | "strings" 17 | "sync" 18 | "testing" 19 | "time" 20 | ) 21 | 22 | var serverAddr string 23 | var once sync.Once 24 | 25 | func echoServer(ws *Conn) { io.Copy(ws, ws) } 26 | 27 | type Count struct { 28 | S string 29 | N int 30 | } 31 | 32 | func countServer(ws *Conn) { 33 | for { 34 | var count Count 35 | err := JSON.Receive(ws, &count) 36 | if err != nil { 37 | return 38 | } 39 | count.N++ 40 | count.S = strings.Repeat(count.S, count.N) 41 | err = JSON.Send(ws, count) 42 | if err != nil { 43 | return 44 | } 45 | } 46 | } 47 | 48 | func subProtocolHandshake(config *Config, req *http.Request) error { 49 | for _, proto := range config.Protocol { 50 | if proto == "chat" { 51 | config.Protocol = []string{proto} 52 | return nil 53 | } 54 | } 55 | return ErrBadWebSocketProtocol 56 | } 57 | 58 | func subProtoServer(ws *Conn) { 59 | for _, proto := range ws.Config().Protocol { 60 | io.WriteString(ws, proto) 61 | } 62 | } 63 | 64 | func startServer() { 65 | http.Handle("/echo", Handler(echoServer)) 66 | http.Handle("/count", Handler(countServer)) 67 | subproto := Server{ 68 | Handshake: subProtocolHandshake, 69 | Handler: Handler(subProtoServer), 70 | } 71 | http.Handle("/subproto", subproto) 72 | server := httptest.NewServer(nil) 73 | serverAddr = server.Listener.Addr().String() 74 | log.Print("Test WebSocket server listening on ", serverAddr) 75 | } 76 | 77 | func newConfig(t *testing.T, path string) *Config { 78 | config, _ := NewConfig(fmt.Sprintf("ws://%s%s", serverAddr, path), "http://localhost") 79 | return config 80 | } 81 | 82 | func TestEcho(t *testing.T) { 83 | once.Do(startServer) 84 | 85 | // websocket.Dial() 86 | client, err := net.Dial("tcp", serverAddr) 87 | if err != nil { 88 | t.Fatal("dialing", err) 89 | } 90 | conn, err := NewClient(newConfig(t, "/echo"), client) 91 | if err != nil { 92 | t.Errorf("WebSocket handshake error: %v", err) 93 | return 94 | } 95 | 96 | msg := []byte("hello, world\n") 97 | if _, err := conn.Write(msg); err != nil { 98 | t.Errorf("Write: %v", err) 99 | } 100 | var actual_msg = make([]byte, 512) 101 | n, err := conn.Read(actual_msg) 102 | if err != nil { 103 | t.Errorf("Read: %v", err) 104 | } 105 | actual_msg = actual_msg[0:n] 106 | if !bytes.Equal(msg, actual_msg) { 107 | t.Errorf("Echo: expected %q got %q", msg, actual_msg) 108 | } 109 | conn.Close() 110 | } 111 | 112 | func TestAddr(t *testing.T) { 113 | once.Do(startServer) 114 | 115 | // websocket.Dial() 116 | client, err := net.Dial("tcp", serverAddr) 117 | if err != nil { 118 | t.Fatal("dialing", err) 119 | } 120 | conn, err := NewClient(newConfig(t, "/echo"), client) 121 | if err != nil { 122 | t.Errorf("WebSocket handshake error: %v", err) 123 | return 124 | } 125 | 126 | ra := conn.RemoteAddr().String() 127 | if !strings.HasPrefix(ra, "ws://") || !strings.HasSuffix(ra, "/echo") { 128 | t.Errorf("Bad remote addr: %v", ra) 129 | } 130 | la := conn.LocalAddr().String() 131 | if !strings.HasPrefix(la, "http://") { 132 | t.Errorf("Bad local addr: %v", la) 133 | } 134 | conn.Close() 135 | } 136 | 137 | func TestCount(t *testing.T) { 138 | once.Do(startServer) 139 | 140 | // websocket.Dial() 141 | client, err := net.Dial("tcp", serverAddr) 142 | if err != nil { 143 | t.Fatal("dialing", err) 144 | } 145 | conn, err := NewClient(newConfig(t, "/count"), client) 146 | if err != nil { 147 | t.Errorf("WebSocket handshake error: %v", err) 148 | return 149 | } 150 | 151 | var count Count 152 | count.S = "hello" 153 | if err := JSON.Send(conn, count); err != nil { 154 | t.Errorf("Write: %v", err) 155 | } 156 | if err := JSON.Receive(conn, &count); err != nil { 157 | t.Errorf("Read: %v", err) 158 | } 159 | if count.N != 1 { 160 | t.Errorf("count: expected %d got %d", 1, count.N) 161 | } 162 | if count.S != "hello" { 163 | t.Errorf("count: expected %q got %q", "hello", count.S) 164 | } 165 | if err := JSON.Send(conn, count); err != nil { 166 | t.Errorf("Write: %v", err) 167 | } 168 | if err := JSON.Receive(conn, &count); err != nil { 169 | t.Errorf("Read: %v", err) 170 | } 171 | if count.N != 2 { 172 | t.Errorf("count: expected %d got %d", 2, count.N) 173 | } 174 | if count.S != "hellohello" { 175 | t.Errorf("count: expected %q got %q", "hellohello", count.S) 176 | } 177 | conn.Close() 178 | } 179 | 180 | func TestWithQuery(t *testing.T) { 181 | once.Do(startServer) 182 | 183 | client, err := net.Dial("tcp", serverAddr) 184 | if err != nil { 185 | t.Fatal("dialing", err) 186 | } 187 | 188 | config := newConfig(t, "/echo") 189 | config.Location, err = url.ParseRequestURI(fmt.Sprintf("ws://%s/echo?q=v", serverAddr)) 190 | if err != nil { 191 | t.Fatal("location url", err) 192 | } 193 | 194 | ws, err := NewClient(config, client) 195 | if err != nil { 196 | t.Errorf("WebSocket handshake: %v", err) 197 | return 198 | } 199 | ws.Close() 200 | } 201 | 202 | func testWithProtocol(t *testing.T, subproto []string) (string, error) { 203 | once.Do(startServer) 204 | 205 | client, err := net.Dial("tcp", serverAddr) 206 | if err != nil { 207 | t.Fatal("dialing", err) 208 | } 209 | 210 | config := newConfig(t, "/subproto") 211 | config.Protocol = subproto 212 | 213 | ws, err := NewClient(config, client) 214 | if err != nil { 215 | return "", err 216 | } 217 | msg := make([]byte, 16) 218 | n, err := ws.Read(msg) 219 | if err != nil { 220 | return "", err 221 | } 222 | ws.Close() 223 | return string(msg[:n]), nil 224 | } 225 | 226 | func TestWithProtocol(t *testing.T) { 227 | proto, err := testWithProtocol(t, []string{"chat"}) 228 | if err != nil { 229 | t.Errorf("SubProto: unexpected error: %v", err) 230 | } 231 | if proto != "chat" { 232 | t.Errorf("SubProto: expected %q, got %q", "chat", proto) 233 | } 234 | } 235 | 236 | func TestWithTwoProtocol(t *testing.T) { 237 | proto, err := testWithProtocol(t, []string{"test", "chat"}) 238 | if err != nil { 239 | t.Errorf("SubProto: unexpected error: %v", err) 240 | } 241 | if proto != "chat" { 242 | t.Errorf("SubProto: expected %q, got %q", "chat", proto) 243 | } 244 | } 245 | 246 | func TestWithBadProtocol(t *testing.T) { 247 | _, err := testWithProtocol(t, []string{"test"}) 248 | if err != ErrBadStatus { 249 | t.Errorf("SubProto: expected %v, got %v", ErrBadStatus, err) 250 | } 251 | } 252 | 253 | func TestHTTP(t *testing.T) { 254 | once.Do(startServer) 255 | 256 | // If the client did not send a handshake that matches the protocol 257 | // specification, the server MUST return an HTTP response with an 258 | // appropriate error code (such as 400 Bad Request) 259 | resp, err := http.Get(fmt.Sprintf("http://%s/echo", serverAddr)) 260 | if err != nil { 261 | t.Errorf("Get: error %#v", err) 262 | return 263 | } 264 | if resp == nil { 265 | t.Error("Get: resp is null") 266 | return 267 | } 268 | if resp.StatusCode != http.StatusBadRequest { 269 | t.Errorf("Get: expected %q got %q", http.StatusBadRequest, resp.StatusCode) 270 | } 271 | } 272 | 273 | func TestTrailingSpaces(t *testing.T) { 274 | // http://code.google.com/p/go/issues/detail?id=955 275 | // The last runs of this create keys with trailing spaces that should not be 276 | // generated by the client. 277 | once.Do(startServer) 278 | config := newConfig(t, "/echo") 279 | for i := 0; i < 30; i++ { 280 | // body 281 | ws, err := DialConfig(config) 282 | if err != nil { 283 | t.Errorf("Dial #%d failed: %v", i, err) 284 | break 285 | } 286 | ws.Close() 287 | } 288 | } 289 | 290 | func TestDialConfigBadVersion(t *testing.T) { 291 | once.Do(startServer) 292 | config := newConfig(t, "/echo") 293 | config.Version = 1234 294 | 295 | _, err := DialConfig(config) 296 | 297 | if dialerr, ok := err.(*DialError); ok { 298 | if dialerr.Err != ErrBadProtocolVersion { 299 | t.Errorf("dial expected err %q but got %q", ErrBadProtocolVersion, dialerr.Err) 300 | } 301 | } 302 | } 303 | 304 | func TestSmallBuffer(t *testing.T) { 305 | // http://code.google.com/p/go/issues/detail?id=1145 306 | // Read should be able to handle reading a fragment of a frame. 307 | once.Do(startServer) 308 | 309 | // websocket.Dial() 310 | client, err := net.Dial("tcp", serverAddr) 311 | if err != nil { 312 | t.Fatal("dialing", err) 313 | } 314 | conn, err := NewClient(newConfig(t, "/echo"), client) 315 | if err != nil { 316 | t.Errorf("WebSocket handshake error: %v", err) 317 | return 318 | } 319 | 320 | msg := []byte("hello, world\n") 321 | if _, err := conn.Write(msg); err != nil { 322 | t.Errorf("Write: %v", err) 323 | } 324 | var small_msg = make([]byte, 8) 325 | n, err := conn.Read(small_msg) 326 | if err != nil { 327 | t.Errorf("Read: %v", err) 328 | } 329 | if !bytes.Equal(msg[:len(small_msg)], small_msg) { 330 | t.Errorf("Echo: expected %q got %q", msg[:len(small_msg)], small_msg) 331 | } 332 | var second_msg = make([]byte, len(msg)) 333 | n, err = conn.Read(second_msg) 334 | if err != nil { 335 | t.Errorf("Read: %v", err) 336 | } 337 | second_msg = second_msg[0:n] 338 | if !bytes.Equal(msg[len(small_msg):], second_msg) { 339 | t.Errorf("Echo: expected %q got %q", msg[len(small_msg):], second_msg) 340 | } 341 | conn.Close() 342 | } 343 | 344 | var parseAuthorityTests = []struct { 345 | in *url.URL 346 | out string 347 | }{ 348 | { 349 | &url.URL{ 350 | Scheme: "ws", 351 | Host: "www.google.com", 352 | }, 353 | "www.google.com:80", 354 | }, 355 | { 356 | &url.URL{ 357 | Scheme: "wss", 358 | Host: "www.google.com", 359 | }, 360 | "www.google.com:443", 361 | }, 362 | { 363 | &url.URL{ 364 | Scheme: "ws", 365 | Host: "www.google.com:80", 366 | }, 367 | "www.google.com:80", 368 | }, 369 | { 370 | &url.URL{ 371 | Scheme: "wss", 372 | Host: "www.google.com:443", 373 | }, 374 | "www.google.com:443", 375 | }, 376 | // some invalid ones for parseAuthority. parseAuthority doesn't 377 | // concern itself with the scheme unless it actually knows about it 378 | { 379 | &url.URL{ 380 | Scheme: "http", 381 | Host: "www.google.com", 382 | }, 383 | "www.google.com", 384 | }, 385 | { 386 | &url.URL{ 387 | Scheme: "http", 388 | Host: "www.google.com:80", 389 | }, 390 | "www.google.com:80", 391 | }, 392 | { 393 | &url.URL{ 394 | Scheme: "asdf", 395 | Host: "127.0.0.1", 396 | }, 397 | "127.0.0.1", 398 | }, 399 | { 400 | &url.URL{ 401 | Scheme: "asdf", 402 | Host: "www.google.com", 403 | }, 404 | "www.google.com", 405 | }, 406 | } 407 | 408 | func TestParseAuthority(t *testing.T) { 409 | for _, tt := range parseAuthorityTests { 410 | out := parseAuthority(tt.in) 411 | if out != tt.out { 412 | t.Errorf("got %v; want %v", out, tt.out) 413 | } 414 | } 415 | } 416 | 417 | type closerConn struct { 418 | net.Conn 419 | closed int // count of the number of times Close was called 420 | } 421 | 422 | func (c *closerConn) Close() error { 423 | c.closed++ 424 | return c.Conn.Close() 425 | } 426 | 427 | func TestClose(t *testing.T) { 428 | once.Do(startServer) 429 | 430 | conn, err := net.Dial("tcp", serverAddr) 431 | if err != nil { 432 | t.Fatal("dialing", err) 433 | } 434 | 435 | cc := closerConn{Conn: conn} 436 | 437 | client, err := NewClient(newConfig(t, "/echo"), &cc) 438 | if err != nil { 439 | t.Fatalf("WebSocket handshake: %v", err) 440 | } 441 | 442 | // set the deadline to ten minutes ago, which will have expired by the time 443 | // client.Close sends the close status frame. 444 | conn.SetDeadline(time.Now().Add(-10 * time.Minute)) 445 | 446 | if err := client.Close(); err == nil { 447 | t.Errorf("ws.Close(): expected error, got %v", err) 448 | } 449 | if cc.closed < 1 { 450 | t.Fatalf("ws.Close(): expected underlying ws.rwc.Close to be called > 0 times, got: %v", cc.closed) 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | header = append(header, b) 161 | hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b) 162 | } 163 | if mask { 164 | // Masking key. 4 bytes. 165 | for i := 0; i < 4; i++ { 166 | b, err = buf.ReadByte() 167 | if err != nil { 168 | return 169 | } 170 | header = append(header, b) 171 | hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b) 172 | } 173 | } 174 | hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length) 175 | hybiFrame.header.data = bytes.NewBuffer(header) 176 | hybiFrame.length = len(header) + int(hybiFrame.header.Length) 177 | return 178 | } 179 | 180 | // A HybiFrameWriter is a writer for hybi frame. 181 | type hybiFrameWriter struct { 182 | writer *bufio.Writer 183 | 184 | header *hybiFrameHeader 185 | } 186 | 187 | func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) { 188 | var header []byte 189 | var b byte 190 | if frame.header.Fin { 191 | b |= 0x80 192 | } 193 | for i := 0; i < 3; i++ { 194 | if frame.header.Rsv[i] { 195 | j := uint(6 - i) 196 | b |= 1 << j 197 | } 198 | } 199 | b |= frame.header.OpCode 200 | header = append(header, b) 201 | if frame.header.MaskingKey != nil { 202 | b = 0x80 203 | } else { 204 | b = 0 205 | } 206 | lengthFields := 0 207 | length := len(msg) 208 | switch { 209 | case length <= 125: 210 | b |= byte(length) 211 | case length < 65536: 212 | b |= 126 213 | lengthFields = 2 214 | default: 215 | b |= 127 216 | lengthFields = 8 217 | } 218 | header = append(header, b) 219 | for i := 0; i < lengthFields; i++ { 220 | j := uint((lengthFields - i - 1) * 8) 221 | b = byte((length >> j) & 0xff) 222 | header = append(header, b) 223 | } 224 | if frame.header.MaskingKey != nil { 225 | if len(frame.header.MaskingKey) != 4 { 226 | return 0, ErrBadMaskingKey 227 | } 228 | header = append(header, frame.header.MaskingKey...) 229 | frame.writer.Write(header) 230 | data := make([]byte, length) 231 | for i := range data { 232 | data[i] = msg[i] ^ frame.header.MaskingKey[i%4] 233 | } 234 | frame.writer.Write(data) 235 | err = frame.writer.Flush() 236 | return length, err 237 | } 238 | frame.writer.Write(header) 239 | frame.writer.Write(msg) 240 | err = frame.writer.Flush() 241 | return length, err 242 | } 243 | 244 | func (frame *hybiFrameWriter) Close() error { return nil } 245 | 246 | type hybiFrameWriterFactory struct { 247 | *bufio.Writer 248 | needMaskingKey bool 249 | } 250 | 251 | func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) { 252 | frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType} 253 | if buf.needMaskingKey { 254 | frameHeader.MaskingKey, err = generateMaskingKey() 255 | if err != nil { 256 | return nil, err 257 | } 258 | } 259 | return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil 260 | } 261 | 262 | type hybiFrameHandler struct { 263 | conn *Conn 264 | payloadType byte 265 | } 266 | 267 | func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (r frameReader, err error) { 268 | if handler.conn.IsServerConn() { 269 | // The client MUST mask all frames sent to the server. 270 | if frame.(*hybiFrameReader).header.MaskingKey == nil { 271 | handler.WriteClose(closeStatusProtocolError) 272 | return nil, io.EOF 273 | } 274 | } else { 275 | // The server MUST NOT mask all frames. 276 | if frame.(*hybiFrameReader).header.MaskingKey != nil { 277 | handler.WriteClose(closeStatusProtocolError) 278 | return nil, io.EOF 279 | } 280 | } 281 | if header := frame.HeaderReader(); header != nil { 282 | io.Copy(ioutil.Discard, header) 283 | } 284 | switch frame.PayloadType() { 285 | case ContinuationFrame: 286 | frame.(*hybiFrameReader).header.OpCode = handler.payloadType 287 | case TextFrame, BinaryFrame: 288 | handler.payloadType = frame.PayloadType() 289 | case CloseFrame: 290 | return nil, io.EOF 291 | case PingFrame: 292 | pingMsg := make([]byte, maxControlFramePayloadLength) 293 | n, err := io.ReadFull(frame, pingMsg) 294 | if err != nil && err != io.ErrUnexpectedEOF { 295 | return nil, err 296 | } 297 | io.Copy(ioutil.Discard, frame) 298 | n, err = handler.WritePong(pingMsg[:n]) 299 | if err != nil { 300 | return nil, err 301 | } 302 | return nil, nil 303 | case PongFrame: 304 | return nil, ErrNotImplemented 305 | } 306 | return frame, nil 307 | } 308 | 309 | func (handler *hybiFrameHandler) WriteClose(status int) (err error) { 310 | handler.conn.wio.Lock() 311 | defer handler.conn.wio.Unlock() 312 | w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame) 313 | if err != nil { 314 | return err 315 | } 316 | msg := make([]byte, 2) 317 | binary.BigEndian.PutUint16(msg, uint16(status)) 318 | _, err = w.Write(msg) 319 | w.Close() 320 | return err 321 | } 322 | 323 | func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) { 324 | handler.conn.wio.Lock() 325 | defer handler.conn.wio.Unlock() 326 | w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame) 327 | if err != nil { 328 | return 0, err 329 | } 330 | n, err = w.Write(msg) 331 | w.Close() 332 | return n, err 333 | } 334 | 335 | // newHybiConn creates a new WebSocket connection speaking hybi draft protocol. 336 | func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 337 | if buf == nil { 338 | br := bufio.NewReader(rwc) 339 | bw := bufio.NewWriter(rwc) 340 | buf = bufio.NewReadWriter(br, bw) 341 | } 342 | ws := &Conn{config: config, request: request, buf: buf, rwc: rwc, 343 | frameReaderFactory: hybiFrameReaderFactory{buf.Reader}, 344 | frameWriterFactory: hybiFrameWriterFactory{ 345 | buf.Writer, request == nil}, 346 | PayloadType: TextFrame, 347 | defaultCloseStatus: closeStatusNormal} 348 | ws.frameHandler = &hybiFrameHandler{conn: ws} 349 | return ws 350 | } 351 | 352 | // generateMaskingKey generates a masking key for a frame. 353 | func generateMaskingKey() (maskingKey []byte, err error) { 354 | maskingKey = make([]byte, 4) 355 | if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil { 356 | return 357 | } 358 | return 359 | } 360 | 361 | // generateNonce generates a nonce consisting of a randomly selected 16-byte 362 | // value that has been base64-encoded. 363 | func generateNonce() (nonce []byte) { 364 | key := make([]byte, 16) 365 | if _, err := io.ReadFull(rand.Reader, key); err != nil { 366 | panic(err) 367 | } 368 | nonce = make([]byte, 24) 369 | base64.StdEncoding.Encode(nonce, key) 370 | return 371 | } 372 | 373 | // getNonceAccept computes the base64-encoded SHA-1 of the concatenation of 374 | // the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string. 375 | func getNonceAccept(nonce []byte) (expected []byte, err error) { 376 | h := sha1.New() 377 | if _, err = h.Write(nonce); err != nil { 378 | return 379 | } 380 | if _, err = h.Write([]byte(websocketGUID)); err != nil { 381 | return 382 | } 383 | expected = make([]byte, 28) 384 | base64.StdEncoding.Encode(expected, h.Sum(nil)) 385 | return 386 | } 387 | 388 | // Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17 389 | func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { 390 | bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") 391 | 392 | bw.WriteString("Host: " + config.Location.Host + "\r\n") 393 | bw.WriteString("Upgrade: websocket\r\n") 394 | bw.WriteString("Connection: Upgrade\r\n") 395 | nonce := generateNonce() 396 | if config.handshakeData != nil { 397 | nonce = []byte(config.handshakeData["key"]) 398 | } 399 | bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n") 400 | bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") 401 | 402 | if config.Version != ProtocolVersionHybi13 { 403 | return ErrBadProtocolVersion 404 | } 405 | 406 | bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n") 407 | if len(config.Protocol) > 0 { 408 | bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n") 409 | } 410 | // TODO(ukai): send Sec-WebSocket-Extensions. 411 | err = config.Header.WriteSubset(bw, handshakeHeader) 412 | if err != nil { 413 | return err 414 | } 415 | 416 | bw.WriteString("\r\n") 417 | if err = bw.Flush(); err != nil { 418 | return err 419 | } 420 | 421 | resp, err := http.ReadResponse(br, &http.Request{Method: "GET"}) 422 | if err != nil { 423 | return err 424 | } 425 | if resp.StatusCode != 101 { 426 | return ErrBadStatus 427 | } 428 | if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" || 429 | strings.ToLower(resp.Header.Get("Connection")) != "upgrade" { 430 | return ErrBadUpgrade 431 | } 432 | expectedAccept, err := getNonceAccept(nonce) 433 | if err != nil { 434 | return err 435 | } 436 | if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) { 437 | return ErrChallengeResponse 438 | } 439 | if resp.Header.Get("Sec-WebSocket-Extensions") != "" { 440 | return ErrUnsupportedExtensions 441 | } 442 | offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol") 443 | if offeredProtocol != "" { 444 | protocolMatched := false 445 | for i := 0; i < len(config.Protocol); i++ { 446 | if config.Protocol[i] == offeredProtocol { 447 | protocolMatched = true 448 | break 449 | } 450 | } 451 | if !protocolMatched { 452 | return ErrBadWebSocketProtocol 453 | } 454 | config.Protocol = []string{offeredProtocol} 455 | } 456 | 457 | return nil 458 | } 459 | 460 | // newHybiClientConn creates a client WebSocket connection after handshake. 461 | func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn { 462 | return newHybiConn(config, buf, rwc, nil) 463 | } 464 | 465 | // A HybiServerHandshaker performs a server handshake using hybi draft protocol. 466 | type hybiServerHandshaker struct { 467 | *Config 468 | accept []byte 469 | } 470 | 471 | func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) { 472 | c.Version = ProtocolVersionHybi13 473 | if req.Method != "GET" { 474 | return http.StatusMethodNotAllowed, ErrBadRequestMethod 475 | } 476 | // HTTP version can be safely ignored. 477 | 478 | if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" || 479 | !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") { 480 | return http.StatusBadRequest, ErrNotWebSocket 481 | } 482 | 483 | key := req.Header.Get("Sec-Websocket-Key") 484 | if key == "" { 485 | return http.StatusBadRequest, ErrChallengeResponse 486 | } 487 | version := req.Header.Get("Sec-Websocket-Version") 488 | switch version { 489 | case "13": 490 | c.Version = ProtocolVersionHybi13 491 | default: 492 | return http.StatusBadRequest, ErrBadWebSocketVersion 493 | } 494 | var scheme string 495 | if req.TLS != nil { 496 | scheme = "wss" 497 | } else { 498 | scheme = "ws" 499 | } 500 | c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI()) 501 | if err != nil { 502 | return http.StatusBadRequest, err 503 | } 504 | protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol")) 505 | if protocol != "" { 506 | protocols := strings.Split(protocol, ",") 507 | for i := 0; i < len(protocols); i++ { 508 | c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i])) 509 | } 510 | } 511 | c.accept, err = getNonceAccept([]byte(key)) 512 | if err != nil { 513 | return http.StatusInternalServerError, err 514 | } 515 | return http.StatusSwitchingProtocols, nil 516 | } 517 | 518 | // Origin parses Origin header in "req". 519 | // If origin is "null", returns (nil, nil). 520 | func Origin(config *Config, req *http.Request) (*url.URL, error) { 521 | var origin string 522 | switch config.Version { 523 | case ProtocolVersionHybi13: 524 | origin = req.Header.Get("Origin") 525 | } 526 | if origin == "null" { 527 | return nil, nil 528 | } 529 | return url.ParseRequestURI(origin) 530 | } 531 | 532 | func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) { 533 | if len(c.Protocol) > 0 { 534 | if len(c.Protocol) != 1 { 535 | // You need choose a Protocol in Handshake func in Server. 536 | return ErrBadWebSocketProtocol 537 | } 538 | } 539 | buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n") 540 | buf.WriteString("Upgrade: websocket\r\n") 541 | buf.WriteString("Connection: Upgrade\r\n") 542 | buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n") 543 | if len(c.Protocol) > 0 { 544 | buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n") 545 | } 546 | // TODO(ukai): send Sec-WebSocket-Extensions. 547 | if c.Header != nil { 548 | err := c.Header.WriteSubset(buf, handshakeHeader) 549 | if err != nil { 550 | return err 551 | } 552 | } 553 | buf.WriteString("\r\n") 554 | return buf.Flush() 555 | } 556 | 557 | func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 558 | return newHybiServerConn(c.Config, buf, rwc, request) 559 | } 560 | 561 | // newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol. 562 | func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 563 | return newHybiConn(config, buf, rwc, request) 564 | } 565 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/golang.org/x/net/websocket/hybi_test.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 | import ( 8 | "bufio" 9 | "bytes" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | "testing" 16 | ) 17 | 18 | // Test the getNonceAccept function with values in 19 | // http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 20 | func TestSecWebSocketAccept(t *testing.T) { 21 | nonce := []byte("dGhlIHNhbXBsZSBub25jZQ==") 22 | expected := []byte("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") 23 | accept, err := getNonceAccept(nonce) 24 | if err != nil { 25 | t.Errorf("getNonceAccept: returned error %v", err) 26 | return 27 | } 28 | if !bytes.Equal(expected, accept) { 29 | t.Errorf("getNonceAccept: expected %q got %q", expected, accept) 30 | } 31 | } 32 | 33 | func TestHybiClientHandshake(t *testing.T) { 34 | b := bytes.NewBuffer([]byte{}) 35 | bw := bufio.NewWriter(b) 36 | br := bufio.NewReader(strings.NewReader(`HTTP/1.1 101 Switching Protocols 37 | Upgrade: websocket 38 | Connection: Upgrade 39 | Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 40 | Sec-WebSocket-Protocol: chat 41 | 42 | `)) 43 | var err error 44 | config := new(Config) 45 | config.Location, err = url.ParseRequestURI("ws://server.example.com/chat") 46 | if err != nil { 47 | t.Fatal("location url", err) 48 | } 49 | config.Origin, err = url.ParseRequestURI("http://example.com") 50 | if err != nil { 51 | t.Fatal("origin url", err) 52 | } 53 | config.Protocol = append(config.Protocol, "chat") 54 | config.Protocol = append(config.Protocol, "superchat") 55 | config.Version = ProtocolVersionHybi13 56 | 57 | config.handshakeData = map[string]string{ 58 | "key": "dGhlIHNhbXBsZSBub25jZQ==", 59 | } 60 | err = hybiClientHandshake(config, br, bw) 61 | if err != nil { 62 | t.Errorf("handshake failed: %v", err) 63 | } 64 | req, err := http.ReadRequest(bufio.NewReader(b)) 65 | if err != nil { 66 | t.Fatalf("read request: %v", err) 67 | } 68 | if req.Method != "GET" { 69 | t.Errorf("request method expected GET, but got %q", req.Method) 70 | } 71 | if req.URL.Path != "/chat" { 72 | t.Errorf("request path expected /chat, but got %q", req.URL.Path) 73 | } 74 | if req.Proto != "HTTP/1.1" { 75 | t.Errorf("request proto expected HTTP/1.1, but got %q", req.Proto) 76 | } 77 | if req.Host != "server.example.com" { 78 | t.Errorf("request Host expected server.example.com, but got %v", req.Host) 79 | } 80 | var expectedHeader = map[string]string{ 81 | "Connection": "Upgrade", 82 | "Upgrade": "websocket", 83 | "Sec-Websocket-Key": config.handshakeData["key"], 84 | "Origin": config.Origin.String(), 85 | "Sec-Websocket-Protocol": "chat, superchat", 86 | "Sec-Websocket-Version": fmt.Sprintf("%d", ProtocolVersionHybi13), 87 | } 88 | for k, v := range expectedHeader { 89 | if req.Header.Get(k) != v { 90 | t.Errorf(fmt.Sprintf("%s expected %q but got %q", k, v, req.Header.Get(k))) 91 | } 92 | } 93 | } 94 | 95 | func TestHybiClientHandshakeWithHeader(t *testing.T) { 96 | b := bytes.NewBuffer([]byte{}) 97 | bw := bufio.NewWriter(b) 98 | br := bufio.NewReader(strings.NewReader(`HTTP/1.1 101 Switching Protocols 99 | Upgrade: websocket 100 | Connection: Upgrade 101 | Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 102 | Sec-WebSocket-Protocol: chat 103 | 104 | `)) 105 | var err error 106 | config := new(Config) 107 | config.Location, err = url.ParseRequestURI("ws://server.example.com/chat") 108 | if err != nil { 109 | t.Fatal("location url", err) 110 | } 111 | config.Origin, err = url.ParseRequestURI("http://example.com") 112 | if err != nil { 113 | t.Fatal("origin url", err) 114 | } 115 | config.Protocol = append(config.Protocol, "chat") 116 | config.Protocol = append(config.Protocol, "superchat") 117 | config.Version = ProtocolVersionHybi13 118 | config.Header = http.Header(make(map[string][]string)) 119 | config.Header.Add("User-Agent", "test") 120 | 121 | config.handshakeData = map[string]string{ 122 | "key": "dGhlIHNhbXBsZSBub25jZQ==", 123 | } 124 | err = hybiClientHandshake(config, br, bw) 125 | if err != nil { 126 | t.Errorf("handshake failed: %v", err) 127 | } 128 | req, err := http.ReadRequest(bufio.NewReader(b)) 129 | if err != nil { 130 | t.Fatalf("read request: %v", err) 131 | } 132 | if req.Method != "GET" { 133 | t.Errorf("request method expected GET, but got %q", req.Method) 134 | } 135 | if req.URL.Path != "/chat" { 136 | t.Errorf("request path expected /chat, but got %q", req.URL.Path) 137 | } 138 | if req.Proto != "HTTP/1.1" { 139 | t.Errorf("request proto expected HTTP/1.1, but got %q", req.Proto) 140 | } 141 | if req.Host != "server.example.com" { 142 | t.Errorf("request Host expected server.example.com, but got %v", req.Host) 143 | } 144 | var expectedHeader = map[string]string{ 145 | "Connection": "Upgrade", 146 | "Upgrade": "websocket", 147 | "Sec-Websocket-Key": config.handshakeData["key"], 148 | "Origin": config.Origin.String(), 149 | "Sec-Websocket-Protocol": "chat, superchat", 150 | "Sec-Websocket-Version": fmt.Sprintf("%d", ProtocolVersionHybi13), 151 | "User-Agent": "test", 152 | } 153 | for k, v := range expectedHeader { 154 | if req.Header.Get(k) != v { 155 | t.Errorf(fmt.Sprintf("%s expected %q but got %q", k, v, req.Header.Get(k))) 156 | } 157 | } 158 | } 159 | 160 | func TestHybiServerHandshake(t *testing.T) { 161 | config := new(Config) 162 | handshaker := &hybiServerHandshaker{Config: config} 163 | br := bufio.NewReader(strings.NewReader(`GET /chat HTTP/1.1 164 | Host: server.example.com 165 | Upgrade: websocket 166 | Connection: Upgrade 167 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 168 | Origin: http://example.com 169 | Sec-WebSocket-Protocol: chat, superchat 170 | Sec-WebSocket-Version: 13 171 | 172 | `)) 173 | req, err := http.ReadRequest(br) 174 | if err != nil { 175 | t.Fatal("request", err) 176 | } 177 | code, err := handshaker.ReadHandshake(br, req) 178 | if err != nil { 179 | t.Errorf("handshake failed: %v", err) 180 | } 181 | if code != http.StatusSwitchingProtocols { 182 | t.Errorf("status expected %q but got %q", http.StatusSwitchingProtocols, code) 183 | } 184 | expectedProtocols := []string{"chat", "superchat"} 185 | if fmt.Sprintf("%v", config.Protocol) != fmt.Sprintf("%v", expectedProtocols) { 186 | t.Errorf("protocol expected %q but got %q", expectedProtocols, config.Protocol) 187 | } 188 | b := bytes.NewBuffer([]byte{}) 189 | bw := bufio.NewWriter(b) 190 | 191 | config.Protocol = config.Protocol[:1] 192 | 193 | err = handshaker.AcceptHandshake(bw) 194 | if err != nil { 195 | t.Errorf("handshake response failed: %v", err) 196 | } 197 | expectedResponse := strings.Join([]string{ 198 | "HTTP/1.1 101 Switching Protocols", 199 | "Upgrade: websocket", 200 | "Connection: Upgrade", 201 | "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", 202 | "Sec-WebSocket-Protocol: chat", 203 | "", ""}, "\r\n") 204 | 205 | if b.String() != expectedResponse { 206 | t.Errorf("handshake expected %q but got %q", expectedResponse, b.String()) 207 | } 208 | } 209 | 210 | func TestHybiServerHandshakeNoSubProtocol(t *testing.T) { 211 | config := new(Config) 212 | handshaker := &hybiServerHandshaker{Config: config} 213 | br := bufio.NewReader(strings.NewReader(`GET /chat HTTP/1.1 214 | Host: server.example.com 215 | Upgrade: websocket 216 | Connection: Upgrade 217 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 218 | Origin: http://example.com 219 | Sec-WebSocket-Version: 13 220 | 221 | `)) 222 | req, err := http.ReadRequest(br) 223 | if err != nil { 224 | t.Fatal("request", err) 225 | } 226 | code, err := handshaker.ReadHandshake(br, req) 227 | if err != nil { 228 | t.Errorf("handshake failed: %v", err) 229 | } 230 | if code != http.StatusSwitchingProtocols { 231 | t.Errorf("status expected %q but got %q", http.StatusSwitchingProtocols, code) 232 | } 233 | if len(config.Protocol) != 0 { 234 | t.Errorf("len(config.Protocol) expected 0, but got %q", len(config.Protocol)) 235 | } 236 | b := bytes.NewBuffer([]byte{}) 237 | bw := bufio.NewWriter(b) 238 | 239 | err = handshaker.AcceptHandshake(bw) 240 | if err != nil { 241 | t.Errorf("handshake response failed: %v", err) 242 | } 243 | expectedResponse := strings.Join([]string{ 244 | "HTTP/1.1 101 Switching Protocols", 245 | "Upgrade: websocket", 246 | "Connection: Upgrade", 247 | "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", 248 | "", ""}, "\r\n") 249 | 250 | if b.String() != expectedResponse { 251 | t.Errorf("handshake expected %q but got %q", expectedResponse, b.String()) 252 | } 253 | } 254 | 255 | func TestHybiServerHandshakeHybiBadVersion(t *testing.T) { 256 | config := new(Config) 257 | handshaker := &hybiServerHandshaker{Config: config} 258 | br := bufio.NewReader(strings.NewReader(`GET /chat HTTP/1.1 259 | Host: server.example.com 260 | Upgrade: websocket 261 | Connection: Upgrade 262 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 263 | Sec-WebSocket-Origin: http://example.com 264 | Sec-WebSocket-Protocol: chat, superchat 265 | Sec-WebSocket-Version: 9 266 | 267 | `)) 268 | req, err := http.ReadRequest(br) 269 | if err != nil { 270 | t.Fatal("request", err) 271 | } 272 | code, err := handshaker.ReadHandshake(br, req) 273 | if err != ErrBadWebSocketVersion { 274 | t.Errorf("handshake expected err %q but got %q", ErrBadWebSocketVersion, err) 275 | } 276 | if code != http.StatusBadRequest { 277 | t.Errorf("status expected %q but got %q", http.StatusBadRequest, code) 278 | } 279 | } 280 | 281 | func testHybiFrame(t *testing.T, testHeader, testPayload, testMaskedPayload []byte, frameHeader *hybiFrameHeader) { 282 | b := bytes.NewBuffer([]byte{}) 283 | frameWriterFactory := &hybiFrameWriterFactory{bufio.NewWriter(b), false} 284 | w, _ := frameWriterFactory.NewFrameWriter(TextFrame) 285 | w.(*hybiFrameWriter).header = frameHeader 286 | _, err := w.Write(testPayload) 287 | w.Close() 288 | if err != nil { 289 | t.Errorf("Write error %q", err) 290 | } 291 | var expectedFrame []byte 292 | expectedFrame = append(expectedFrame, testHeader...) 293 | expectedFrame = append(expectedFrame, testMaskedPayload...) 294 | if !bytes.Equal(expectedFrame, b.Bytes()) { 295 | t.Errorf("frame expected %q got %q", expectedFrame, b.Bytes()) 296 | } 297 | frameReaderFactory := &hybiFrameReaderFactory{bufio.NewReader(b)} 298 | r, err := frameReaderFactory.NewFrameReader() 299 | if err != nil { 300 | t.Errorf("Read error %q", err) 301 | } 302 | if header := r.HeaderReader(); header == nil { 303 | t.Errorf("no header") 304 | } else { 305 | actualHeader := make([]byte, r.Len()) 306 | n, err := header.Read(actualHeader) 307 | if err != nil { 308 | t.Errorf("Read header error %q", err) 309 | } else { 310 | if n < len(testHeader) { 311 | t.Errorf("header too short %q got %q", testHeader, actualHeader[:n]) 312 | } 313 | if !bytes.Equal(testHeader, actualHeader[:n]) { 314 | t.Errorf("header expected %q got %q", testHeader, actualHeader[:n]) 315 | } 316 | } 317 | } 318 | if trailer := r.TrailerReader(); trailer != nil { 319 | t.Errorf("unexpected trailer %q", trailer) 320 | } 321 | frame := r.(*hybiFrameReader) 322 | if frameHeader.Fin != frame.header.Fin || 323 | frameHeader.OpCode != frame.header.OpCode || 324 | len(testPayload) != int(frame.header.Length) { 325 | t.Errorf("mismatch %v (%d) vs %v", frameHeader, len(testPayload), frame) 326 | } 327 | payload := make([]byte, len(testPayload)) 328 | _, err = r.Read(payload) 329 | if err != nil { 330 | t.Errorf("read %v", err) 331 | } 332 | if !bytes.Equal(testPayload, payload) { 333 | t.Errorf("payload %q vs %q", testPayload, payload) 334 | } 335 | } 336 | 337 | func TestHybiShortTextFrame(t *testing.T) { 338 | frameHeader := &hybiFrameHeader{Fin: true, OpCode: TextFrame} 339 | payload := []byte("hello") 340 | testHybiFrame(t, []byte{0x81, 0x05}, payload, payload, frameHeader) 341 | 342 | payload = make([]byte, 125) 343 | testHybiFrame(t, []byte{0x81, 125}, payload, payload, frameHeader) 344 | } 345 | 346 | func TestHybiShortMaskedTextFrame(t *testing.T) { 347 | frameHeader := &hybiFrameHeader{Fin: true, OpCode: TextFrame, 348 | MaskingKey: []byte{0xcc, 0x55, 0x80, 0x20}} 349 | payload := []byte("hello") 350 | maskedPayload := []byte{0xa4, 0x30, 0xec, 0x4c, 0xa3} 351 | header := []byte{0x81, 0x85} 352 | header = append(header, frameHeader.MaskingKey...) 353 | testHybiFrame(t, header, payload, maskedPayload, frameHeader) 354 | } 355 | 356 | func TestHybiShortBinaryFrame(t *testing.T) { 357 | frameHeader := &hybiFrameHeader{Fin: true, OpCode: BinaryFrame} 358 | payload := []byte("hello") 359 | testHybiFrame(t, []byte{0x82, 0x05}, payload, payload, frameHeader) 360 | 361 | payload = make([]byte, 125) 362 | testHybiFrame(t, []byte{0x82, 125}, payload, payload, frameHeader) 363 | } 364 | 365 | func TestHybiControlFrame(t *testing.T) { 366 | frameHeader := &hybiFrameHeader{Fin: true, OpCode: PingFrame} 367 | payload := []byte("hello") 368 | testHybiFrame(t, []byte{0x89, 0x05}, payload, payload, frameHeader) 369 | 370 | frameHeader = &hybiFrameHeader{Fin: true, OpCode: PongFrame} 371 | testHybiFrame(t, []byte{0x8A, 0x05}, payload, payload, frameHeader) 372 | 373 | frameHeader = &hybiFrameHeader{Fin: true, OpCode: CloseFrame} 374 | payload = []byte{0x03, 0xe8} // 1000 375 | testHybiFrame(t, []byte{0x88, 0x02}, payload, payload, frameHeader) 376 | } 377 | 378 | func TestHybiLongFrame(t *testing.T) { 379 | frameHeader := &hybiFrameHeader{Fin: true, OpCode: TextFrame} 380 | payload := make([]byte, 126) 381 | testHybiFrame(t, []byte{0x81, 126, 0x00, 126}, payload, payload, frameHeader) 382 | 383 | payload = make([]byte, 65535) 384 | testHybiFrame(t, []byte{0x81, 126, 0xff, 0xff}, payload, payload, frameHeader) 385 | 386 | payload = make([]byte, 65536) 387 | testHybiFrame(t, []byte{0x81, 127, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00}, payload, payload, frameHeader) 388 | } 389 | 390 | func TestHybiClientRead(t *testing.T) { 391 | wireData := []byte{0x81, 0x05, 'h', 'e', 'l', 'l', 'o', 392 | 0x89, 0x05, 'h', 'e', 'l', 'l', 'o', // ping 393 | 0x81, 0x05, 'w', 'o', 'r', 'l', 'd'} 394 | br := bufio.NewReader(bytes.NewBuffer(wireData)) 395 | bw := bufio.NewWriter(bytes.NewBuffer([]byte{})) 396 | conn := newHybiConn(newConfig(t, "/"), bufio.NewReadWriter(br, bw), nil, nil) 397 | 398 | msg := make([]byte, 512) 399 | n, err := conn.Read(msg) 400 | if err != nil { 401 | t.Errorf("read 1st frame, error %q", err) 402 | } 403 | if n != 5 { 404 | t.Errorf("read 1st frame, expect 5, got %d", n) 405 | } 406 | if !bytes.Equal(wireData[2:7], msg[:n]) { 407 | t.Errorf("read 1st frame %v, got %v", wireData[2:7], msg[:n]) 408 | } 409 | n, err = conn.Read(msg) 410 | if err != nil { 411 | t.Errorf("read 2nd frame, error %q", err) 412 | } 413 | if n != 5 { 414 | t.Errorf("read 2nd frame, expect 5, got %d", n) 415 | } 416 | if !bytes.Equal(wireData[16:21], msg[:n]) { 417 | t.Errorf("read 2nd frame %v, got %v", wireData[16:21], msg[:n]) 418 | } 419 | n, err = conn.Read(msg) 420 | if err == nil { 421 | t.Errorf("read not EOF") 422 | } 423 | if n != 0 { 424 | t.Errorf("expect read 0, got %d", n) 425 | } 426 | } 427 | 428 | func TestHybiShortRead(t *testing.T) { 429 | wireData := []byte{0x81, 0x05, 'h', 'e', 'l', 'l', 'o', 430 | 0x89, 0x05, 'h', 'e', 'l', 'l', 'o', // ping 431 | 0x81, 0x05, 'w', 'o', 'r', 'l', 'd'} 432 | br := bufio.NewReader(bytes.NewBuffer(wireData)) 433 | bw := bufio.NewWriter(bytes.NewBuffer([]byte{})) 434 | conn := newHybiConn(newConfig(t, "/"), bufio.NewReadWriter(br, bw), nil, nil) 435 | 436 | step := 0 437 | pos := 0 438 | expectedPos := []int{2, 5, 16, 19} 439 | expectedLen := []int{3, 2, 3, 2} 440 | for { 441 | msg := make([]byte, 3) 442 | n, err := conn.Read(msg) 443 | if step >= len(expectedPos) { 444 | if err == nil { 445 | t.Errorf("read not EOF") 446 | } 447 | if n != 0 { 448 | t.Errorf("expect read 0, got %d", n) 449 | } 450 | return 451 | } 452 | pos = expectedPos[step] 453 | endPos := pos + expectedLen[step] 454 | if err != nil { 455 | t.Errorf("read from %d, got error %q", pos, err) 456 | return 457 | } 458 | if n != endPos-pos { 459 | t.Errorf("read from %d, expect %d, got %d", pos, endPos-pos, n) 460 | } 461 | if !bytes.Equal(wireData[pos:endPos], msg[:n]) { 462 | t.Errorf("read from %d, frame %v, got %v", pos, wireData[pos:endPos], msg[:n]) 463 | } 464 | step++ 465 | } 466 | } 467 | 468 | func TestHybiServerRead(t *testing.T) { 469 | wireData := []byte{0x81, 0x85, 0xcc, 0x55, 0x80, 0x20, 470 | 0xa4, 0x30, 0xec, 0x4c, 0xa3, // hello 471 | 0x89, 0x85, 0xcc, 0x55, 0x80, 0x20, 472 | 0xa4, 0x30, 0xec, 0x4c, 0xa3, // ping: hello 473 | 0x81, 0x85, 0xed, 0x83, 0xb4, 0x24, 474 | 0x9a, 0xec, 0xc6, 0x48, 0x89, // world 475 | } 476 | br := bufio.NewReader(bytes.NewBuffer(wireData)) 477 | bw := bufio.NewWriter(bytes.NewBuffer([]byte{})) 478 | conn := newHybiConn(newConfig(t, "/"), bufio.NewReadWriter(br, bw), nil, new(http.Request)) 479 | 480 | expected := [][]byte{[]byte("hello"), []byte("world")} 481 | 482 | msg := make([]byte, 512) 483 | n, err := conn.Read(msg) 484 | if err != nil { 485 | t.Errorf("read 1st frame, error %q", err) 486 | } 487 | if n != 5 { 488 | t.Errorf("read 1st frame, expect 5, got %d", n) 489 | } 490 | if !bytes.Equal(expected[0], msg[:n]) { 491 | t.Errorf("read 1st frame %q, got %q", expected[0], msg[:n]) 492 | } 493 | 494 | n, err = conn.Read(msg) 495 | if err != nil { 496 | t.Errorf("read 2nd frame, error %q", err) 497 | } 498 | if n != 5 { 499 | t.Errorf("read 2nd frame, expect 5, got %d", n) 500 | } 501 | if !bytes.Equal(expected[1], msg[:n]) { 502 | t.Errorf("read 2nd frame %q, got %q", expected[1], msg[:n]) 503 | } 504 | 505 | n, err = conn.Read(msg) 506 | if err == nil { 507 | t.Errorf("read not EOF") 508 | } 509 | if n != 0 { 510 | t.Errorf("expect read 0, got %d", n) 511 | } 512 | } 513 | 514 | func TestHybiServerReadWithoutMasking(t *testing.T) { 515 | wireData := []byte{0x81, 0x05, 'h', 'e', 'l', 'l', 'o'} 516 | br := bufio.NewReader(bytes.NewBuffer(wireData)) 517 | bw := bufio.NewWriter(bytes.NewBuffer([]byte{})) 518 | conn := newHybiConn(newConfig(t, "/"), bufio.NewReadWriter(br, bw), nil, new(http.Request)) 519 | // server MUST close the connection upon receiving a non-masked frame. 520 | msg := make([]byte, 512) 521 | _, err := conn.Read(msg) 522 | if err != io.EOF { 523 | t.Errorf("read 1st frame, expect %q, but got %q", io.EOF, err) 524 | } 525 | } 526 | 527 | func TestHybiClientReadWithMasking(t *testing.T) { 528 | wireData := []byte{0x81, 0x85, 0xcc, 0x55, 0x80, 0x20, 529 | 0xa4, 0x30, 0xec, 0x4c, 0xa3, // hello 530 | } 531 | br := bufio.NewReader(bytes.NewBuffer(wireData)) 532 | bw := bufio.NewWriter(bytes.NewBuffer([]byte{})) 533 | conn := newHybiConn(newConfig(t, "/"), bufio.NewReadWriter(br, bw), nil, nil) 534 | 535 | // client MUST close the connection upon receiving a masked frame. 536 | msg := make([]byte, 512) 537 | _, err := conn.Read(msg) 538 | if err != io.EOF { 539 | t.Errorf("read 1st frame, expect %q, but got %q", io.EOF, err) 540 | } 541 | } 542 | 543 | // Test the hybiServerHandshaker supports firefox implementation and 544 | // checks Connection request header include (but it's not necessary 545 | // equal to) "upgrade" 546 | func TestHybiServerFirefoxHandshake(t *testing.T) { 547 | config := new(Config) 548 | handshaker := &hybiServerHandshaker{Config: config} 549 | br := bufio.NewReader(strings.NewReader(`GET /chat HTTP/1.1 550 | Host: server.example.com 551 | Upgrade: websocket 552 | Connection: keep-alive, upgrade 553 | Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 554 | Origin: http://example.com 555 | Sec-WebSocket-Protocol: chat, superchat 556 | Sec-WebSocket-Version: 13 557 | 558 | `)) 559 | req, err := http.ReadRequest(br) 560 | if err != nil { 561 | t.Fatal("request", err) 562 | } 563 | code, err := handshaker.ReadHandshake(br, req) 564 | if err != nil { 565 | t.Errorf("handshake failed: %v", err) 566 | } 567 | if code != http.StatusSwitchingProtocols { 568 | t.Errorf("status expected %q but got %q", http.StatusSwitchingProtocols, code) 569 | } 570 | b := bytes.NewBuffer([]byte{}) 571 | bw := bufio.NewWriter(b) 572 | 573 | config.Protocol = []string{"chat"} 574 | 575 | err = handshaker.AcceptHandshake(bw) 576 | if err != nil { 577 | t.Errorf("handshake response failed: %v", err) 578 | } 579 | expectedResponse := strings.Join([]string{ 580 | "HTTP/1.1 101 Switching Protocols", 581 | "Upgrade: websocket", 582 | "Connection: Upgrade", 583 | "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", 584 | "Sec-WebSocket-Protocol: chat", 585 | "", ""}, "\r\n") 586 | 587 | if b.String() != expectedResponse { 588 | t.Errorf("handshake expected %q but got %q", expectedResponse, b.String()) 589 | } 590 | } 591 | --------------------------------------------------------------------------------