├── .gitignore ├── LICENSE ├── README.md ├── config.json ├── config_test.json ├── main.go ├── main_test.go ├── vendor ├── github.com │ └── nlopes │ │ └── slack │ │ ├── LICENSE │ │ ├── README.md │ │ ├── TODO.txt │ │ ├── admin.go │ │ ├── attachments.go │ │ ├── backoff.go │ │ ├── channels.go │ │ ├── chat.go │ │ ├── comment.go │ │ ├── conversation.go │ │ ├── dnd.go │ │ ├── emoji.go │ │ ├── files.go │ │ ├── groups.go │ │ ├── history.go │ │ ├── im.go │ │ ├── info.go │ │ ├── item.go │ │ ├── messageID.go │ │ ├── messages.go │ │ ├── misc.go │ │ ├── oauth.go │ │ ├── pagination.go │ │ ├── pins.go │ │ ├── reactions.go │ │ ├── rtm.go │ │ ├── search.go │ │ ├── slack.go │ │ ├── stars.go │ │ ├── team.go │ │ ├── users.go │ │ ├── websocket.go │ │ ├── websocket_channels.go │ │ ├── websocket_dm.go │ │ ├── websocket_dnd.go │ │ ├── websocket_files.go │ │ ├── websocket_groups.go │ │ ├── websocket_internals.go │ │ ├── websocket_managed_conn.go │ │ ├── websocket_misc.go │ │ ├── websocket_pins.go │ │ ├── websocket_proxy.go │ │ ├── websocket_reactions.go │ │ ├── websocket_stars.go │ │ ├── websocket_teams.go │ │ └── websocket_utils.go ├── golang.org │ └── x │ │ └── net │ │ ├── LICENSE │ │ ├── PATENTS │ │ └── websocket │ │ ├── client.go │ │ ├── hybi.go │ │ ├── server.go │ │ └── websocket.go └── vendor.json └── yoda.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /wisemonk 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wisemonk 2 | 3 | Wisemonk is a slackbot to keep people from wasting too much time on Slack. It isn't just another bot, its a super intelligent one. You can tell it what channels to monitor and then it can 4 | 5 | * alert you when you are talking too much. 6 | * automatically create a topic on discourse with the contents of your chat and share the url with you. 7 | * create a topic on demand on discourse. 8 | 9 | A lot of teams have found that the synchronous nature of communication on slack destroys the productivity of their team members.For more on that read this article [here](https://medium.com/better-people/slack-i-m-breaking-up-with-you-54600ace03ea#.ox3a8tukc). Thus we at Dgraph try to have most of our structured and meaningful conversations on [Discourse](https://www.discourse.org/). Wisemonk constantly monitors our communication and helps us move our discussion to discourse when we are talking a lot. 10 | 11 | ## Install 12 | 13 | `go get github.com/dgraph-io/wisemonk` 14 | 15 | `go install github.com/dgraph-io/wisemonk` 16 | 17 | We use govendor to manage our deps and all our deps our checked in to the repo so you don't need to install them separately. 18 | 19 | ## Usage 20 | 21 | Add config to `config.json`. 22 | 23 | ``` 24 | { 25 | // slackbot token. 26 | "token": "", 27 | "discourseprefix": "https://discuss.dgraph.io", 28 | // discourse api key. 29 | "discoursekey": "", 30 | "channels": { 31 | // slack channel id 32 | "G1D59039B": { 33 | // interval should be a value that can be parsed by https://golang.org/pkg/time/#ParseDuration. 34 | "interval": "10m", 35 | "maxmsg":20, 36 | // slug of discourse categories that wisemonk would search in. 37 | "search_over":["minions","dev","user","decisions","reading","blog","faqs","docs","use-cases","recruit"], 38 | // slug of discourse category that a new topic would be created in. 39 | "create_topic_in": "slack" 40 | }, 41 | } 42 | } 43 | ``` 44 | After adding config, since the wisemonk binary is now installed and if you have `$GOPATH/bin` in your path you can call wisemonk like this `wisemonk` 45 | 46 | Now if in any 10 minute interval more than 20 messages are exchanged, wisemonk would alert you. 47 | 48 | Token for slack can be obtained after creating a bot user at https://api.slack.com/bot-users. Also note that you would have to add wisemonk as a user to all the channels that you want it to be active on. 49 | 50 | If you use [discourse](https://www.discourse.org/), then wisemonk has some other advanced functionalities that you could make use of. Wisemonk stores the messages exchanged and automatically creates a discourse topic for you, a link of which it shares while sending the alert. 51 | 52 | 53 | You could customize the alert message displayed. For now we display Yoda, followed by a [Go Proverb](https://go-proverbs.github.io/) and then the link for the discourse topic if a discourse key and discourse prefix are given as config. 54 | 55 | 56 | ## Interaction 57 | 58 | - You can search over your topics in discourse like 59 | 60 | `wisemonk query [query_string] [max_count]` 61 | 62 | So `wisemonk query release v0.3 5` would return url of top 5 topics which have `release v0.3` as part of them. 63 | 64 | - Sometimes you are having an important discussion on slack and don't want wisemonk to interrupt you. In these scenarios you could ask the wisemonk to meditate for some time like this in your slack channel. 65 | 66 | `wisemonk meditate for 20m` 67 | 68 | If successful, wisemonk replies with `Okay, I am going to meditate for 20m`. The duration can be anything understood by [ParseDuration](https://golang.org/pkg/time/#ParseDuration). 69 | 70 | - If you are using discourse and you observe that you are having an important discussion, you could create a discourse topic from slack using wisemonk. This topic would have your last n messages and would provide relevant context for further discussion on discourse. The command for creating a topic is 71 | 72 | `wisemonk create topic [title of discourse topic]` 73 | 74 | Wisemonk will reply back with the url of the new topic that was created. 75 | 76 | ## Technologies involved 77 | 78 | Wisemonk is written in Go and makes use of 79 | 80 | * [Slack RTM API](https://api.slack.com/rtm) 81 | * [Discourse API](https://meta.discourse.org/t/discourse-api-documentation/22706) 82 | 83 | ## About the project 84 | Wisemonk was born out of our experience at [Dgraph](https://github.com/dgraph-io/dgraph). Read more about why we built it in our [blogpost](https://medium.dgraph.io/wisemonk-a-slackbot-to-move-discussions-from-slack-to-discourse-22a53ddce78f#.rcn1wlv3p). 85 | 86 | We got featured on [Hacker News](https://news.ycombinator.com/item?id=11908704) too. 87 | 88 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "", 3 | "discourseprefix": "https://discuss.dgraph.io", 4 | "discoursekey": "", 5 | "channels": { 6 | "G1D59039B": { 7 | "interval": "10m", 8 | "maxmsg":20, 9 | "search_over":["minions","dev","user","decisions","reading", 10 | "blog","faqs","docs","use-cases","recruit"], 11 | "create_topic_in": "slack" 12 | }, 13 | "C13LH03RR": { 14 | "interval": "10m", 15 | "maxmsg":1000, 16 | "search_over":["dev","user","docs","faqs"], 17 | "create_topic_in": "user" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "xoxb", 3 | "discourseprefix": "https://discuss.dgraph.io", 4 | "discoursekey": "000a9b9b87357092931e068", 5 | "channels": { 6 | "G1D59039B": { 7 | "interval": "10m", 8 | "maxmsg":20, 9 | "search_over":["minions","dev","user","decisions","reading", 10 | "blog","faqs","docs","use-cases","recruit"], 11 | "create_topic_in": "slack" 12 | }, 13 | "C13LH03RR": { 14 | "interval": "10m", 15 | "maxmsg":1000, 16 | "search_over":["dev","user","docs","faqs"], 17 | "create_topic_in": "user" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 DGraph Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | "net/http/httptest" 23 | "strconv" 24 | "strings" 25 | "testing" 26 | "time" 27 | 28 | "github.com/nlopes/slack" 29 | ) 30 | 31 | func TestSanitizeTitle(t *testing.T) { 32 | title := "Short title" 33 | expected := "Topic created by wisemonk with title: Short title" 34 | if st := sanitizeTitle(title); st != expected { 35 | t.Errorf("Expected: %s, Got: %s", expected, st) 36 | } 37 | 38 | title = "Long title with word breaks" 39 | expected = "Long title with word" 40 | if st := sanitizeTitle(title); st != expected { 41 | t.Errorf("Expected: %s, Got: %s", expected, st) 42 | } 43 | 44 | title = "This title is 20char" 45 | expected = title 46 | if st := sanitizeTitle(title); st != expected { 47 | t.Errorf("Expected: %s, Got: %s", expected, st) 48 | } 49 | 50 | title = `This title has more than 100chars. It should be trimmed 51 | down. We should avoid having long titles obviously` 52 | expected = `This title has more than 100chars. It should be trimmed 53 | down. We should avoid having long titles` 54 | if st := sanitizeTitle(title); st != expected { 55 | t.Errorf("Expected: %s, Got: %s", expected, st) 56 | } 57 | 58 | title = " Short title" 59 | expected = "Topic created by wisemonk with title: Short title" 60 | if st := sanitizeTitle(title); st != expected { 61 | t.Errorf("Expected: %s, Got: %s", expected, st) 62 | } 63 | } 64 | 65 | func TestAskToMeditate(t *testing.T) { 66 | c := &Counter{} 67 | 68 | message := "wisemonk meditat for 1hr" 69 | m := askToMeditate(c, message) 70 | em := "" 71 | if m != em { 72 | t.Errorf("Expected: %s, Got: %s", em, m) 73 | } 74 | 75 | message = "wisemonk meditate for 1hr" 76 | m = askToMeditate(c, message) 77 | em = "Sorry, I don't understand you." 78 | if m != em { 79 | t.Errorf("Expected: %s, Got: %s", em, m) 80 | } 81 | 82 | message = "wisemonk meditate for 200h" 83 | m = askToMeditate(c, message) 84 | em = "It's hard to meditate for more than an hour at one go you know." 85 | if m != em { 86 | t.Errorf("Expected: %s, Got: %s", em, m) 87 | } 88 | 89 | message = "wisemonk meditate for -5m" 90 | m = askToMeditate(c, message) 91 | em = "Sorry, going back in time is not what I can do." 92 | if m != em { 93 | t.Errorf("Expected: %s, Got: %s", em, m) 94 | } 95 | 96 | message = "wisemonk meditate for 5m" 97 | m = askToMeditate(c, message) 98 | em = "Okay, I am going to meditate for 5m0s" 99 | if m != em { 100 | t.Errorf("Expected: %s, Got: %s", em, m) 101 | } 102 | 103 | message = "wisemonk meditate for 5m" 104 | m = askToMeditate(c, message) 105 | em = "I am meditating. My meditation will finish in 5 mins" 106 | if m != em { 107 | t.Errorf("Expected: %s, Got: %s", em, m) 108 | } 109 | } 110 | 111 | func TestIncrement(t *testing.T) { 112 | c := &Counter{ChannelId: "general"} 113 | msgs := []slack.Msg{ 114 | {Channel: "general", Timestamp: "1465010249.000606", 115 | Text: " First message"}, 116 | {Channel: "general", Timestamp: "1465010259.000606", 117 | Text: " Second message"}, 118 | {Channel: "general", Timestamp: "1465010249.000806", 119 | Text: " Third message at same timestamp as first"}, 120 | } 121 | 122 | for _, m := range msgs { 123 | c.Increment(&m, map[string]string{}) 124 | } 125 | if len(c.buckets) != 2 { 126 | t.Errorf("Expected: %d,Got: %d buckets", 1, len(c.buckets)) 127 | } 128 | if c.buckets[0].count != 2 { 129 | t.Errorf("Expected bucket to have %d messages, Got: %d", 2, 130 | c.buckets[0].count) 131 | } 132 | if c.buckets[1].count != 1 { 133 | t.Errorf("Expected bucket to have %d messages, Got: %d", 1, 134 | c.buckets[1].count) 135 | } 136 | } 137 | 138 | func addBuckets(c *Counter, text string, t int64) { 139 | for i := 0; i < 10; i++ { 140 | c.Increment(&slack.Msg{Channel: "general", 141 | Timestamp: strconv.FormatInt(t-int64(i), 10), 142 | Text: text}, map[string]string{}) 143 | } 144 | } 145 | 146 | func TestCount(t *testing.T) { 147 | c := &Counter{ChannelId: "general", Interval: "10m"} 148 | timeNow := time.Now().Unix() 149 | addBuckets(c, "New buckets", timeNow) 150 | 151 | timeBfrInterval := time.Now().Add(-10 * time.Minute).Unix() 152 | addBuckets(c, "Old buckets", timeBfrInterval) 153 | if count := c.Count(); count != 10 { 154 | t.Errorf("Expected count to be %d, Got: %d", 10, count) 155 | } 156 | if len(c.buckets) != 10 { 157 | t.Errorf("Expected %d buckets, Got: %d", 10, len(c.buckets)) 158 | } 159 | } 160 | 161 | func createServer(t *testing.T, status int, i interface{}) *httptest.Server { 162 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, 163 | r *http.Request) { 164 | w.WriteHeader(status) 165 | b, err := json.Marshal(i) 166 | if err != nil { 167 | t.Error(err) 168 | } 169 | w.Write(b) 170 | })) 171 | return ts 172 | } 173 | 174 | func TestCreateTopic(t *testing.T) { 175 | c := &Counter{ChannelId: "general"} 176 | timeNow := time.Now().Unix() 177 | addBuckets(c, "New buckets", timeNow) 178 | 179 | ts := createServer(t, http.StatusNotFound, TopicBody{}) 180 | conf.DiscPrefix = ts.URL 181 | defer ts.Close() 182 | 183 | if url := createTopic(c, "Test title"); url != "" { 184 | t.Errorf("Expected url to be blank, Got: ", url) 185 | } 186 | 187 | ts = createServer(t, http.StatusOK, 188 | TopicBody{Id: 1, Slug: "test-title-created"}) 189 | conf.DiscPrefix = ts.URL 190 | if url := createTopic(c, "Test title"); !strings.Contains(url, 191 | "test-title-created") { 192 | t.Errorf("Expected url to contain test-title-created, Got: %s", 193 | url) 194 | } 195 | } 196 | 197 | type r struct { 198 | } 199 | 200 | var invoked = false 201 | 202 | func (rtm *r) SendMessage(msg *slack.OutgoingMessage) { 203 | invoked = true 204 | } 205 | 206 | func (rtm *r) NewOutgoingMessage(text string, channel string) *slack.OutgoingMessage { 207 | return new(slack.OutgoingMessage) 208 | } 209 | 210 | func TestSearchDiscourse(t *testing.T) { 211 | c := &Counter{ChannelId: "general", SearchOver: []string{"Slack"}} 212 | discourseCategory = make(map[int]string) 213 | discourseCategory[1] = "Slack" 214 | discourseCategory[2] = "Reading" 215 | rtm := &r{} 216 | invoked = false 217 | conf.DiscKey = "testkey" 218 | ts := createServer(t, http.StatusOK, 219 | SearchResponse{Topics: []SearchTopic{ 220 | {Id: 1, Slug: "test-1", Category: 1}, 221 | {Id: 2, Slug: "test-2", Category: 2}, 222 | }}) 223 | conf.DiscPrefix = ts.URL 224 | if searchDiscourse(c, "wisemonk search something", rtm); invoked { 225 | t.Errorf("rtm.SendMessage() should have been called") 226 | } 227 | 228 | if searchDiscourse(c, "wisemonk query test 5", rtm); !invoked { 229 | t.Errorf("rtm.SendMessage() should have been called") 230 | } 231 | } 232 | 233 | func TestFilterTopics(t *testing.T) { 234 | c := &Counter{ChannelId: "general", SearchOver: []string{"Slack"}} 235 | discourseCategory = make(map[int]string) 236 | discourseCategory[1] = "Slack" 237 | discourseCategory[2] = "Reading" 238 | topics := []SearchTopic{ 239 | {Id: 1, Slug: "test-1", Category: 1}, 240 | {Id: 2, Slug: "test-2", Category: 2}, 241 | } 242 | ft := filterTopics(c, topics) 243 | if len(ft) != 1 { 244 | t.Errorf("Expected filtered topics to have length %d. Got: %d", 245 | 1, len(ft)) 246 | } 247 | } 248 | 249 | func TestCallYoda(t *testing.T) { 250 | c := &Counter{ChannelId: "general"} 251 | timeNow := time.Now().Unix() 252 | addBuckets(c, "New buckets", timeNow) 253 | rtm := &r{} 254 | 255 | if callYoda(c, rtm, "Message to append"); !invoked { 256 | t.Errorf("Expected invoked to be %t, Got: %t", true, false) 257 | } 258 | } 259 | 260 | func TestSendMessage(t *testing.T) { 261 | c := &Counter{ChannelId: "general"} 262 | timeNow := time.Now().Unix() 263 | addBuckets(c, "New buckets", timeNow) 264 | rtm := &r{} 265 | 266 | invoked = false 267 | if sendMessage(c, rtm); !invoked { 268 | t.Errorf("Expected invoked to be %t, Got: %t", true, false) 269 | } 270 | 271 | conf.DiscKey = "testkey" 272 | addBuckets(c, "New buckets", timeNow) 273 | invoked = false 274 | ts := createServer(t, http.StatusOK, TopicBody{Id: 1, 275 | Slug: "test-title-created"}) 276 | conf.DiscPrefix = ts.URL 277 | defer ts.Close() 278 | 279 | if sendMessage(c, rtm); !invoked { 280 | t.Errorf("Expected invoked to be %t, Got: %t", true, false) 281 | } 282 | } 283 | 284 | func TestCreateNewTopic(t *testing.T) { 285 | c := &Counter{ChannelId: "general"} 286 | timeNow := time.Now().Unix() 287 | addBuckets(c, "New buckets", timeNow) 288 | m := "wisemonk create topic testing wisemonk" 289 | rtm := &r{} 290 | ts := createServer(t, http.StatusOK, TopicBody{Id: 1, 291 | Slug: "test-title-created"}) 292 | conf.DiscPrefix = ts.URL 293 | defer ts.Close() 294 | 295 | conf.DiscKey = "testkey" 296 | invoked = false 297 | if createNewTopic(c, m, rtm); !invoked { 298 | t.Errorf("Expected invoked to be %t, Got: %t", true, false) 299 | } 300 | } 301 | 302 | func TestSubstituteUsernames(t *testing.T) { 303 | memmap := make(map[string]string) 304 | memmap["U13LHF42F"] = "mrjn" 305 | memmap["U13LHF42G"] = "pawan" 306 | text := "<@U13LHF42F> <@U13LHF42F> <@U13LHF42G>" 307 | 308 | text = substituteUsernames(text, memmap) 309 | expected := "@mrjn @mrjn @pawan" 310 | if text != expected { 311 | t.Errorf("Expected %s, Got: %s", expected, text) 312 | } 313 | } 314 | 315 | func TestRunQueryAndParseResponse(t *testing.T) { 316 | mems := Members{} 317 | mems.Users = append(mems.Users, Member{Id: "U13GH76YT", Name: "mrjn"}, 318 | Member{Id: "U13GH13YT", Name: "pawan"}) 319 | ts := createServer(t, http.StatusOK, mems) 320 | defer ts.Close() 321 | 322 | var m Members 323 | runQueryAndParseResponse(ts.URL, &m) 324 | } 325 | 326 | func TestCacheUsernames(t *testing.T) { 327 | mems := Members{} 328 | mems.Users = append(mems.Users, Member{Id: "U13GH76YT", Name: "mrjn"}, 329 | Member{Id: "U13GH13YT", Name: "pawan"}) 330 | ts := createServer(t, http.StatusOK, mems) 331 | defer ts.Close() 332 | 333 | memmap := cacheUsernames(ts.URL) 334 | if _, ok := memmap["U13GH76YT"]; !ok { 335 | t.Errorf("Expected ok to be true. Got false") 336 | } 337 | uname := memmap["U13GH76YT"] 338 | if uname != "mrjn" { 339 | t.Errorf("Expected username to be mrjn, Got: ", uname) 340 | } 341 | if _, ok := memmap["U13GH13YT"]; !ok { 342 | t.Errorf("Expected ok to be true. Got false") 343 | } 344 | } 345 | 346 | func TestCheckDiscourseCategory(t *testing.T) { 347 | readConfig("config_test.json") 348 | discourseCategory = make(map[int]string) 349 | discourseCategory[1] = "slack" 350 | discourseCategory[2] = "user" 351 | cr := CategoryRes{CategoryList: Categories{}} 352 | cr.CategoryList.Cats = append(cr.CategoryList.Cats, 353 | Category{Slug: "slack"}, 354 | Category{Slug: "user"}) 355 | ts := createServer(t, http.StatusOK, cr) 356 | defer ts.Close() 357 | 358 | checkDiscourseCategory(conf.Channels, ts.URL) 359 | } 360 | 361 | func TestReadConfig(t *testing.T) { 362 | readConfig("config_test.json") 363 | if conf.Token == "" { 364 | t.Errorf("Expected token to not be nil.") 365 | } 366 | if conf.DiscPrefix == "" { 367 | t.Errorf("Expected discuss prefix to not be nil.") 368 | } 369 | if len(conf.Channels) != 2 { 370 | t.Errorf("Expected len of Channels to be %d. Got: %d", 2, 371 | len(conf.Channels)) 372 | } 373 | } 374 | 375 | func TestParseSearchQuery(t *testing.T) { 376 | m := "wisemonk query performance blogpost abc" 377 | q, c := parseSearchQuery(m) 378 | expected := "performance blogpost abc" 379 | if q != expected { 380 | t.Errorf("Expected query to be: %s. Got: %s", expected, q) 381 | } 382 | if c != 3 { 383 | t.Errorf("Expected count to be %d. Got: %d", 3, c) 384 | } 385 | 386 | m = "wisemonk query performance blogpost 4" 387 | q, c = parseSearchQuery(m) 388 | expected = "performance blogpost" 389 | if q != expected { 390 | t.Errorf("Expected query to be: %s. Got: %s", expected, q) 391 | } 392 | if c != 4 { 393 | t.Errorf("Expected count to be %d. Got: %d", 4, c) 394 | } 395 | 396 | m = "wisemonk query performance" 397 | q, c = parseSearchQuery(m) 398 | expected = "performance" 399 | if q != expected { 400 | t.Errorf("Expected query to be: %s. Got: %s", expected, q) 401 | } 402 | if c != 3 { 403 | t.Errorf("Expected count to be %d. Got: %d", 3, c) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /vendor/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 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/README.md: -------------------------------------------------------------------------------- 1 | Slack API in Go [![GoDoc](https://godoc.org/github.com/nlopes/slack?status.svg)](https://godoc.org/github.com/nlopes/slack) [![Build Status](https://travis-ci.org/nlopes/slack.svg)](https://travis-ci.org/nlopes/slack) 2 | =============== 3 | 4 | This library supports most if not all of the `api.slack.com` REST 5 | calls, as well as the Real-Time Messaging protocol over websocket, in 6 | a fully managed way. 7 | 8 | 9 | Note: If you just updated from master and it broke your implementation, please check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1) 10 | 11 | ## Installing 12 | 13 | ### *go get* 14 | 15 | $ go get github.com/nlopes/slack 16 | 17 | ## Example 18 | 19 | ### Getting all groups 20 | 21 | import ( 22 | "fmt" 23 | 24 | "github.com/nlopes/slack" 25 | ) 26 | 27 | func main() { 28 | api := slack.New("YOUR_TOKEN_HERE") 29 | // If you set debugging, it will log all requests to the console 30 | // Useful when encountering issues 31 | // api.SetDebug(true) 32 | groups, err := api.GetGroups(false) 33 | if err != nil { 34 | fmt.Printf("%s\n", err) 35 | return 36 | } 37 | for _, group := range groups { 38 | fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) 39 | } 40 | } 41 | 42 | ### Getting User Information 43 | 44 | import ( 45 | "fmt" 46 | 47 | "github.com/nlopes/slack" 48 | ) 49 | 50 | func main() { 51 | api := slack.New("YOUR_TOKEN_HERE") 52 | user, err := api.GetUserInfo("U023BECGF") 53 | if err != nil { 54 | fmt.Printf("%s\n", err) 55 | return 56 | } 57 | fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) 58 | } 59 | 60 | ## Minimal RTM usage: 61 | 62 | See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go 63 | 64 | 65 | ## Contributing 66 | 67 | You are more than welcome to contribute to this project. Fork and 68 | make a Pull Request, or create an Issue if you see any problem. 69 | 70 | ## License 71 | 72 | BSD 2 Clause license 73 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/TODO.txt: -------------------------------------------------------------------------------- 1 | - Add more tests!!! 2 | - Add support to have markdown hints 3 | - See section Message Formatting at https://api.slack.com/docs/formatting 4 | -------------------------------------------------------------------------------- /vendor/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 | // DisableUser disabled a user account, given a user ID 29 | func (api *Client) DisableUser(teamName string, uid string) error { 30 | values := url.Values{ 31 | "user": {uid}, 32 | "token": {api.config.token}, 33 | "set_active": {"true"}, 34 | "_attempts": {"1"}, 35 | } 36 | 37 | _, err := adminRequest("setInactive", teamName, values, api.debug) 38 | if err != nil { 39 | return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // InviteGuest invites a user to Slack as a single-channel guest 46 | func (api *Client) InviteGuest( 47 | teamName string, 48 | channel string, 49 | firstName string, 50 | lastName string, 51 | emailAddress string, 52 | ) error { 53 | values := url.Values{ 54 | "email": {emailAddress}, 55 | "channels": {channel}, 56 | "first_name": {firstName}, 57 | "last_name": {lastName}, 58 | "ultra_restricted": {"1"}, 59 | "token": {api.config.token}, 60 | "set_active": {"true"}, 61 | "_attempts": {"1"}, 62 | } 63 | 64 | _, err := adminRequest("invite", teamName, values, api.debug) 65 | if err != nil { 66 | return fmt.Errorf("Failed to invite single-channel guest: %s", err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // InviteRestricted invites a user to Slack as a restricted account 73 | func (api *Client) InviteRestricted( 74 | teamName string, 75 | channel string, 76 | firstName string, 77 | lastName string, 78 | emailAddress string, 79 | ) error { 80 | values := url.Values{ 81 | "email": {emailAddress}, 82 | "channels": {channel}, 83 | "first_name": {firstName}, 84 | "last_name": {lastName}, 85 | "restricted": {"1"}, 86 | "token": {api.config.token}, 87 | "set_active": {"true"}, 88 | "_attempts": {"1"}, 89 | } 90 | 91 | _, err := adminRequest("invite", teamName, values, api.debug) 92 | if err != nil { 93 | return fmt.Errorf("Failed to restricted account: %s", err) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // InviteToTeam invites a user to a Slack team 100 | func (api *Client) InviteToTeam( 101 | teamName string, 102 | firstName string, 103 | lastName string, 104 | emailAddress string, 105 | ) error { 106 | values := url.Values{ 107 | "email": {emailAddress}, 108 | "first_name": {firstName}, 109 | "last_name": {lastName}, 110 | "token": {api.config.token}, 111 | "set_active": {"true"}, 112 | "_attempts": {"1"}, 113 | } 114 | 115 | _, err := adminRequest("invite", teamName, values, api.debug) 116 | if err != nil { 117 | return fmt.Errorf("Failed to invite to team: %s", err) 118 | } 119 | 120 | return nil 121 | } 122 | 123 | // SetRegular enables the specified user 124 | func (api *Client) SetRegular(teamName string, user string) error { 125 | values := url.Values{ 126 | "user": {user}, 127 | "token": {api.config.token}, 128 | "set_active": {"true"}, 129 | "_attempts": {"1"}, 130 | } 131 | 132 | _, err := adminRequest("setRegular", teamName, values, api.debug) 133 | if err != nil { 134 | return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) 135 | } 136 | 137 | return nil 138 | } 139 | 140 | // SendSSOBindingEmail sends an SSO binding email to the specified user 141 | func (api *Client) SendSSOBindingEmail(teamName string, user string) error { 142 | values := url.Values{ 143 | "user": {user}, 144 | "token": {api.config.token}, 145 | "set_active": {"true"}, 146 | "_attempts": {"1"}, 147 | } 148 | 149 | _, err := adminRequest("sendSSOBind", teamName, values, api.debug) 150 | if err != nil { 151 | return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) 152 | } 153 | 154 | return nil 155 | } 156 | 157 | // SetUltraRestricted converts a user into a single-channel guest 158 | func (api *Client) SetUltraRestricted(teamName, uid, channel string) error { 159 | values := url.Values{ 160 | "user": {uid}, 161 | "channel": {channel}, 162 | "token": {api.config.token}, 163 | "set_active": {"true"}, 164 | "_attempts": {"1"}, 165 | } 166 | 167 | _, err := adminRequest("setUltraRestricted", teamName, values, api.debug) 168 | if err != nil { 169 | return fmt.Errorf("Failed to ultra-restrict account: %s", err) 170 | } 171 | 172 | return nil 173 | } 174 | 175 | // SetRestricted converts a user into a restricted account 176 | func (api *Client) SetRestricted(teamName, uid string) error { 177 | values := url.Values{ 178 | "user": {uid}, 179 | "token": {api.config.token}, 180 | "set_active": {"true"}, 181 | "_attempts": {"1"}, 182 | } 183 | 184 | _, err := adminRequest("setRestricted", teamName, values, api.debug) 185 | if err != nil { 186 | return fmt.Errorf("Failed to restrict account: %s", err) 187 | } 188 | 189 | return nil 190 | } 191 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/attachments.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // AttachmentField contains information for an attachment field 4 | // An Attachment can contain multiple of these 5 | type AttachmentField struct { 6 | Title string `json:"title"` 7 | Value string `json:"value"` 8 | Short bool `json:"short"` 9 | } 10 | 11 | // Attachment contains all the information for an attachment 12 | type Attachment struct { 13 | Color string `json:"color,omitempty"` 14 | Fallback string `json:"fallback"` 15 | 16 | AuthorName string `json:"author_name,omitempty"` 17 | AuthorSubname string `json:"author_subname,omitempty"` 18 | AuthorLink string `json:"author_link,omitempty"` 19 | AuthorIcon string `json:"author_icon,omitempty"` 20 | 21 | Title string `json:"title,omitempty"` 22 | TitleLink string `json:"title_link,omitempty"` 23 | Pretext string `json:"pretext,omitempty"` 24 | Text string `json:"text"` 25 | 26 | ImageURL string `json:"image_url,omitempty"` 27 | ThumbURL string `json:"thumb_url,omitempty"` 28 | 29 | Fields []AttachmentField `json:"fields,omitempty"` 30 | MarkdownIn []string `json:"mrkdwn_in,omitempty"` 31 | } 32 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/backoff.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // This one was ripped from https://github.com/jpillora/backoff/blob/master/backoff.go 10 | 11 | // Backoff is a time.Duration counter. It starts at Min. After every 12 | // call to Duration() it is multiplied by Factor. It is capped at 13 | // Max. It returns to Min on every call to Reset(). Used in 14 | // conjunction with the time package. 15 | type backoff struct { 16 | attempts int 17 | //Factor is the multiplying factor for each increment step 18 | Factor float64 19 | //Jitter eases contention by randomizing backoff steps 20 | Jitter bool 21 | //Min and Max are the minimum and maximum values of the counter 22 | Min, Max time.Duration 23 | } 24 | 25 | // Returns the current value of the counter and then multiplies it 26 | // Factor 27 | func (b *backoff) Duration() time.Duration { 28 | //Zero-values are nonsensical, so we use 29 | //them to apply defaults 30 | if b.Min == 0 { 31 | b.Min = 100 * time.Millisecond 32 | } 33 | if b.Max == 0 { 34 | b.Max = 10 * time.Second 35 | } 36 | if b.Factor == 0 { 37 | b.Factor = 2 38 | } 39 | //calculate this duration 40 | dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts)) 41 | if b.Jitter == true { 42 | dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) 43 | } 44 | //cap! 45 | if dur > float64(b.Max) { 46 | return b.Max 47 | } 48 | //bump attempts count 49 | b.attempts++ 50 | //return as a time.Duration 51 | return time.Duration(dur) 52 | } 53 | 54 | //Resets the current value of the counter back to Min 55 | func (b *backoff) Reset() { 56 | b.attempts = 0 57 | } 58 | -------------------------------------------------------------------------------- /vendor/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 | // Channel contains information about the channel 20 | type Channel struct { 21 | groupConversation 22 | IsChannel bool `json:"is_channel"` 23 | IsGeneral bool `json:"is_general"` 24 | IsMember bool `json:"is_member"` 25 | } 26 | 27 | func channelRequest(path string, values url.Values, debug bool) (*channelResponseFull, error) { 28 | response := &channelResponseFull{} 29 | err := post(path, values, response, debug) 30 | if err != nil { 31 | return nil, err 32 | } 33 | if !response.Ok { 34 | return nil, errors.New(response.Error) 35 | } 36 | return response, nil 37 | } 38 | 39 | // ArchiveChannel archives the given channel 40 | func (api *Client) ArchiveChannel(channel string) error { 41 | values := url.Values{ 42 | "token": {api.config.token}, 43 | "channel": {channel}, 44 | } 45 | _, err := channelRequest("channels.archive", values, api.debug) 46 | if err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | // UnarchiveChannel unarchives the given channel 53 | func (api *Client) UnarchiveChannel(channel string) error { 54 | values := url.Values{ 55 | "token": {api.config.token}, 56 | "channel": {channel}, 57 | } 58 | _, err := channelRequest("channels.unarchive", values, api.debug) 59 | if err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | 65 | // CreateChannel creates a channel with the given name and returns a *Channel 66 | func (api *Client) CreateChannel(channel string) (*Channel, error) { 67 | values := url.Values{ 68 | "token": {api.config.token}, 69 | "name": {channel}, 70 | } 71 | response, err := channelRequest("channels.create", values, api.debug) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return &response.Channel, nil 76 | } 77 | 78 | // GetChannelHistory retrieves the channel history 79 | func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) { 80 | values := url.Values{ 81 | "token": {api.config.token}, 82 | "channel": {channel}, 83 | } 84 | if params.Latest != DEFAULT_HISTORY_LATEST { 85 | values.Add("latest", params.Latest) 86 | } 87 | if params.Oldest != DEFAULT_HISTORY_OLDEST { 88 | values.Add("oldest", params.Oldest) 89 | } 90 | if params.Count != DEFAULT_HISTORY_COUNT { 91 | values.Add("count", strconv.Itoa(params.Count)) 92 | } 93 | if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { 94 | if params.Inclusive { 95 | values.Add("inclusive", "1") 96 | } else { 97 | values.Add("inclusive", "0") 98 | } 99 | } 100 | if params.Unreads != DEFAULT_HISTORY_UNREADS { 101 | if params.Unreads { 102 | values.Add("unreads", "1") 103 | } else { 104 | values.Add("unreads", "0") 105 | } 106 | } 107 | response, err := channelRequest("channels.history", values, api.debug) 108 | if err != nil { 109 | return nil, err 110 | } 111 | return &response.History, nil 112 | } 113 | 114 | // GetChannelInfo retrieves the given channel 115 | func (api *Client) GetChannelInfo(channel string) (*Channel, error) { 116 | values := url.Values{ 117 | "token": {api.config.token}, 118 | "channel": {channel}, 119 | } 120 | response, err := channelRequest("channels.info", values, api.debug) 121 | if err != nil { 122 | return nil, err 123 | } 124 | return &response.Channel, nil 125 | } 126 | 127 | // InviteUserToChannel invites a user to a given channel and returns a *Channel 128 | func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) { 129 | values := url.Values{ 130 | "token": {api.config.token}, 131 | "channel": {channel}, 132 | "user": {user}, 133 | } 134 | response, err := channelRequest("channels.invite", values, api.debug) 135 | if err != nil { 136 | return nil, err 137 | } 138 | return &response.Channel, nil 139 | } 140 | 141 | // JoinChannel joins the currently authenticated user to a channel 142 | func (api *Client) JoinChannel(channel string) (*Channel, error) { 143 | values := url.Values{ 144 | "token": {api.config.token}, 145 | "name": {channel}, 146 | } 147 | response, err := channelRequest("channels.join", values, api.debug) 148 | if err != nil { 149 | return nil, err 150 | } 151 | return &response.Channel, nil 152 | } 153 | 154 | // LeaveChannel makes the authenticated user leave the given channel 155 | func (api *Client) LeaveChannel(channel string) (bool, error) { 156 | values := url.Values{ 157 | "token": {api.config.token}, 158 | "channel": {channel}, 159 | } 160 | response, err := channelRequest("channels.leave", values, api.debug) 161 | if err != nil { 162 | return false, err 163 | } 164 | if response.NotInChannel { 165 | return response.NotInChannel, nil 166 | } 167 | return false, nil 168 | } 169 | 170 | // KickUserFromChannel kicks a user from a given channel 171 | func (api *Client) KickUserFromChannel(channel, user string) error { 172 | values := url.Values{ 173 | "token": {api.config.token}, 174 | "channel": {channel}, 175 | "user": {user}, 176 | } 177 | _, err := channelRequest("channels.kick", values, api.debug) 178 | if err != nil { 179 | return err 180 | } 181 | return nil 182 | } 183 | 184 | // GetChannels retrieves all the channels 185 | func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) { 186 | values := url.Values{ 187 | "token": {api.config.token}, 188 | } 189 | if excludeArchived { 190 | values.Add("exclude_archived", "1") 191 | } 192 | response, err := channelRequest("channels.list", values, api.debug) 193 | if err != nil { 194 | return nil, err 195 | } 196 | return response.Channels, nil 197 | } 198 | 199 | // SetChannelReadMark sets the read mark of a given channel to a specific point 200 | // Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a 201 | // timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls 202 | // (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A 203 | // timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. 204 | func (api *Client) SetChannelReadMark(channel, ts string) error { 205 | values := url.Values{ 206 | "token": {api.config.token}, 207 | "channel": {channel}, 208 | "ts": {ts}, 209 | } 210 | _, err := channelRequest("channels.mark", values, api.debug) 211 | if err != nil { 212 | return err 213 | } 214 | return nil 215 | } 216 | 217 | // RenameChannel renames a given channel 218 | func (api *Client) RenameChannel(channel, name string) (*Channel, error) { 219 | values := url.Values{ 220 | "token": {api.config.token}, 221 | "channel": {channel}, 222 | "name": {name}, 223 | } 224 | // XXX: the created entry in this call returns a string instead of a number 225 | // so I may have to do some workaround to solve it. 226 | response, err := channelRequest("channels.rename", values, api.debug) 227 | if err != nil { 228 | return nil, err 229 | } 230 | return &response.Channel, nil 231 | 232 | } 233 | 234 | // SetChannelPurpose sets the channel purpose and returns the purpose that was 235 | // successfully set 236 | func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) { 237 | values := url.Values{ 238 | "token": {api.config.token}, 239 | "channel": {channel}, 240 | "purpose": {purpose}, 241 | } 242 | response, err := channelRequest("channels.setPurpose", values, api.debug) 243 | if err != nil { 244 | return "", err 245 | } 246 | return response.Purpose, nil 247 | } 248 | 249 | // SetChannelTopic sets the channel topic and returns the topic that was successfully set 250 | func (api *Client) SetChannelTopic(channel, topic string) (string, error) { 251 | values := url.Values{ 252 | "token": {api.config.token}, 253 | "channel": {channel}, 254 | "topic": {topic}, 255 | } 256 | response, err := channelRequest("channels.setTopic", values, api.debug) 257 | if err != nil { 258 | return "", err 259 | } 260 | return response.Topic, nil 261 | } 262 | -------------------------------------------------------------------------------- /vendor/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 | Channel string `json:"channel"` 25 | Timestamp string `json:"ts"` 26 | Text string `json:"text"` 27 | SlackResponse 28 | } 29 | 30 | // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request 31 | type PostMessageParameters struct { 32 | Text string 33 | Username string 34 | AsUser bool 35 | Parse string 36 | LinkNames int 37 | Attachments []Attachment 38 | UnfurlLinks bool 39 | UnfurlMedia bool 40 | IconURL string 41 | IconEmoji string 42 | Markdown bool `json:"mrkdwn,omitempty"` 43 | EscapeText bool 44 | } 45 | 46 | // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set 47 | func NewPostMessageParameters() PostMessageParameters { 48 | return PostMessageParameters{ 49 | Username: DEFAULT_MESSAGE_USERNAME, 50 | AsUser: DEFAULT_MESSAGE_ASUSER, 51 | Parse: DEFAULT_MESSAGE_PARSE, 52 | LinkNames: DEFAULT_MESSAGE_LINK_NAMES, 53 | Attachments: nil, 54 | UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, 55 | UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, 56 | IconURL: DEFAULT_MESSAGE_ICON_URL, 57 | IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, 58 | Markdown: DEFAULT_MESSAGE_MARKDOWN, 59 | EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, 60 | } 61 | } 62 | 63 | func chatRequest(path string, values url.Values, debug bool) (*chatResponseFull, error) { 64 | response := &chatResponseFull{} 65 | err := post(path, values, response, debug) 66 | if err != nil { 67 | return nil, err 68 | } 69 | if !response.Ok { 70 | return nil, errors.New(response.Error) 71 | } 72 | return response, nil 73 | } 74 | 75 | // DeleteMessage deletes a message in a channel 76 | func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { 77 | values := url.Values{ 78 | "token": {api.config.token}, 79 | "channel": {channel}, 80 | "ts": {messageTimestamp}, 81 | } 82 | response, err := chatRequest("chat.delete", values, api.debug) 83 | if err != nil { 84 | return "", "", err 85 | } 86 | return response.Channel, response.Timestamp, nil 87 | } 88 | 89 | func escapeMessage(message string) string { 90 | replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") 91 | return replacer.Replace(message) 92 | } 93 | 94 | // PostMessage sends a message to a channel. 95 | // Message is escaped by default according to https://api.slack.com/docs/formatting 96 | // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. 97 | func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) { 98 | if params.EscapeText { 99 | text = escapeMessage(text) 100 | } 101 | values := url.Values{ 102 | "token": {api.config.token}, 103 | "channel": {channel}, 104 | "text": {text}, 105 | } 106 | if params.Username != DEFAULT_MESSAGE_USERNAME { 107 | values.Set("username", string(params.Username)) 108 | } 109 | if params.AsUser != DEFAULT_MESSAGE_ASUSER { 110 | values.Set("as_user", "true") 111 | } 112 | if params.Parse != DEFAULT_MESSAGE_PARSE { 113 | values.Set("parse", string(params.Parse)) 114 | } 115 | if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { 116 | values.Set("link_names", "1") 117 | } 118 | if params.Attachments != nil { 119 | attachments, err := json.Marshal(params.Attachments) 120 | if err != nil { 121 | return "", "", err 122 | } 123 | values.Set("attachments", string(attachments)) 124 | } 125 | if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { 126 | values.Set("unfurl_links", "true") 127 | } 128 | // I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request. 129 | // Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side. 130 | if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { 131 | values.Set("unfurl_links", "false") 132 | } 133 | if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { 134 | values.Set("unfurl_media", "false") 135 | } 136 | if params.IconURL != DEFAULT_MESSAGE_ICON_URL { 137 | values.Set("icon_url", params.IconURL) 138 | } 139 | if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { 140 | values.Set("icon_emoji", params.IconEmoji) 141 | } 142 | if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { 143 | values.Set("mrkdwn", "false") 144 | } 145 | 146 | response, err := chatRequest("chat.postMessage", values, api.debug) 147 | if err != nil { 148 | return "", "", err 149 | } 150 | return response.Channel, response.Timestamp, nil 151 | } 152 | 153 | // UpdateMessage updates a message in a channel 154 | func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { 155 | values := url.Values{ 156 | "token": {api.config.token}, 157 | "channel": {channel}, 158 | "text": {escapeMessage(text)}, 159 | "ts": {timestamp}, 160 | } 161 | response, err := chatRequest("chat.update", values, api.debug) 162 | if err != nil { 163 | return "", "", "", err 164 | } 165 | return response.Channel, response.Timestamp, response.Text, nil 166 | } 167 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/comment.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // Comment contains all the information relative to a comment 4 | type Comment struct { 5 | ID string `json:"id,omitempty"` 6 | Created JSONTime `json:"created,omitempty"` 7 | Timestamp JSONTime `json:"timestamp,omitempty"` 8 | User string `json:"user,omitempty"` 9 | Comment string `json:"comment,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/conversation.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // Conversation is the foundation for IM and BaseGroupConversation 4 | type conversation struct { 5 | ID string `json:"id"` 6 | Created JSONTime `json:"created"` 7 | IsOpen bool `json:"is_open"` 8 | LastRead string `json:"last_read,omitempty"` 9 | Latest *Message `json:"latest,omitempty"` 10 | UnreadCount int `json:"unread_count,omitempty"` 11 | UnreadCountDisplay int `json:"unread_count_display,omitempty"` 12 | } 13 | 14 | // GroupConversation is the foundation for Group and Channel 15 | type groupConversation struct { 16 | conversation 17 | Name string `json:"name"` 18 | Creator string `json:"creator"` 19 | IsArchived bool `json:"is_archived"` 20 | Members []string `json:"members"` 21 | NumMembers int `json:"num_members,omitempty"` 22 | Topic Topic `json:"topic"` 23 | Purpose Purpose `json:"purpose"` 24 | } 25 | 26 | // Topic contains information about the topic 27 | type Topic struct { 28 | Value string `json:"value"` 29 | Creator string `json:"creator"` 30 | LastSet JSONTime `json:"last_set"` 31 | } 32 | 33 | // Purpose contains information about the purpose 34 | type Purpose struct { 35 | Value string `json:"value"` 36 | Creator string `json:"creator"` 37 | LastSet JSONTime `json:"last_set"` 38 | } 39 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/dnd.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type SnoozeDebug struct { 11 | SnoozeEndDate string `json:"snooze_end_date"` 12 | } 13 | 14 | type SnoozeInfo struct { 15 | SnoozeEnabled bool `json:"snooze_enabled,omitempty"` 16 | SnoozeEndTime int `json:"snooze_endtime,omitempty"` 17 | SnoozeRemaining int `json:"snooze_remaining,omitempty"` 18 | SnoozeDebug SnoozeDebug `json:"snooze_debug,omitempty"` 19 | } 20 | 21 | type DNDStatus struct { 22 | Enabled bool `json:"dnd_enabled"` 23 | NextStartTimestamp int `json:"next_dnd_start_ts"` 24 | NextEndTimestamp int `json:"next_dnd_end_ts"` 25 | SnoozeInfo 26 | } 27 | 28 | type dndResponseFull struct { 29 | DNDStatus 30 | SlackResponse 31 | } 32 | 33 | type dndTeamInfoResponse struct { 34 | Users map[string]DNDStatus `json:"users"` 35 | SlackResponse 36 | } 37 | 38 | func dndRequest(path string, values url.Values, debug bool) (*dndResponseFull, error) { 39 | response := &dndResponseFull{} 40 | err := post(path, values, response, debug) 41 | if err != nil { 42 | return nil, err 43 | } 44 | if !response.Ok { 45 | return nil, errors.New(response.Error) 46 | } 47 | return response, nil 48 | } 49 | 50 | // EndDND ends the user's scheduled Do Not Disturb session 51 | func (api *Client) EndDND() error { 52 | values := url.Values{ 53 | "token": {api.config.token}, 54 | } 55 | 56 | response := &SlackResponse{} 57 | if err := post("dnd.endDnd", values, response, api.debug); err != nil { 58 | return err 59 | } 60 | if !response.Ok { 61 | return errors.New(response.Error) 62 | } 63 | return nil 64 | } 65 | 66 | // EndSnooze ends the current user's snooze mode 67 | func (api *Client) EndSnooze() (*DNDStatus, error) { 68 | values := url.Values{ 69 | "token": {api.config.token}, 70 | } 71 | 72 | response, err := dndRequest("dnd.endSnooze", values, api.debug) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return &response.DNDStatus, nil 77 | } 78 | 79 | // GetDNDInfo provides information about a user's current Do Not Disturb settings. 80 | func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { 81 | values := url.Values{ 82 | "token": {api.config.token}, 83 | } 84 | if user != nil { 85 | values.Set("user", *user) 86 | } 87 | response, err := dndRequest("dnd.info", values, api.debug) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return &response.DNDStatus, nil 92 | } 93 | 94 | // GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. 95 | func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { 96 | values := url.Values{ 97 | "token": {api.config.token}, 98 | "users": {strings.Join(users, ",")}, 99 | } 100 | response := &dndTeamInfoResponse{} 101 | if err := post("dnd.teamInfo", values, response, api.debug); err != nil { 102 | return nil, err 103 | } 104 | if !response.Ok { 105 | return nil, errors.New(response.Error) 106 | } 107 | return response.Users, nil 108 | } 109 | 110 | // SetSnooze adjusts the snooze duration for a user's Do Not Disturb 111 | // settings. If a snooze session is not already active for the user, invoking 112 | // this method will begin one for the specified duration. 113 | func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { 114 | values := url.Values{ 115 | "token": {api.config.token}, 116 | "num_minutes": {strconv.Itoa(minutes)}, 117 | } 118 | response, err := dndRequest("dnd.setSnooze", values, api.debug) 119 | if err != nil { 120 | return nil, err 121 | } 122 | return &response.DNDStatus, nil 123 | } 124 | -------------------------------------------------------------------------------- /vendor/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 *Client) GetEmoji() (map[string]string, error) { 15 | values := url.Values{ 16 | "token": {api.config.token}, 17 | } 18 | response := &emojiResponseFull{} 19 | err := post("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 | -------------------------------------------------------------------------------- /vendor/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_USER = "" 13 | DEFAULT_FILES_CHANNEL = "" 14 | DEFAULT_FILES_TS_FROM = 0 15 | DEFAULT_FILES_TS_TO = -1 16 | DEFAULT_FILES_TYPES = "all" 17 | DEFAULT_FILES_COUNT = 100 18 | DEFAULT_FILES_PAGE = 1 19 | ) 20 | 21 | // File contains all the information for a file 22 | type File struct { 23 | ID string `json:"id"` 24 | Created JSONTime `json:"created"` 25 | Timestamp JSONTime `json:"timestamp"` 26 | 27 | Name string `json:"name"` 28 | Title string `json:"title"` 29 | Mimetype string `json:"mimetype"` 30 | ImageExifRotation int `json:"image_exif_rotation"` 31 | Filetype string `json:"filetype"` 32 | PrettyType string `json:"pretty_type"` 33 | User string `json:"user"` 34 | 35 | Mode string `json:"mode"` 36 | Editable bool `json:"editable"` 37 | IsExternal bool `json:"is_external"` 38 | ExternalType string `json:"external_type"` 39 | 40 | Size int `json:"size"` 41 | 42 | URL string `json:"url"` // Deprecated - never set 43 | URLDownload string `json:"url_download"` // Deprecated - never set 44 | URLPrivate string `json:"url_private"` 45 | URLPrivateDownload string `json:"url_private_download"` 46 | 47 | OriginalH int `json:"original_h"` 48 | OriginalW int `json:"original_w"` 49 | Thumb64 string `json:"thumb_64"` 50 | Thumb80 string `json:"thumb_80"` 51 | Thumb160 string `json:"thumb_160"` 52 | Thumb360 string `json:"thumb_360"` 53 | Thumb360Gif string `json:"thumb_360_gif"` 54 | Thumb360W int `json:"thumb_360_w"` 55 | Thumb360H int `json:"thumb_360_h"` 56 | Thumb480 string `json:"thumb_480"` 57 | Thumb480W int `json:"thumb_480_w"` 58 | Thumb480H int `json:"thumb_480_h"` 59 | Thumb720 string `json:"thumb_720"` 60 | Thumb720W int `json:"thumb_720_w"` 61 | Thumb720H int `json:"thumb_720_h"` 62 | Thumb960 string `json:"thumb_960"` 63 | Thumb960W int `json:"thumb_960_w"` 64 | Thumb960H int `json:"thumb_960_h"` 65 | Thumb1024 string `json:"thumb_1024"` 66 | Thumb1024W int `json:"thumb_1024_w"` 67 | Thumb1024H int `json:"thumb_1024_h"` 68 | 69 | Permalink string `json:"permalink"` 70 | PermalinkPublic string `json:"permalink_public"` 71 | 72 | EditLink string `json:"edit_link"` 73 | Preview string `json:"preview"` 74 | PreviewHighlight string `json:"preview_highlight"` 75 | Lines int `json:"lines"` 76 | LinesMore int `json:"lines_more"` 77 | 78 | IsPublic bool `json:"is_public"` 79 | PublicURLShared bool `json:"public_url_shared"` 80 | Channels []string `json:"channels"` 81 | Groups []string `json:"groups"` 82 | IMs []string `json:"ims"` 83 | InitialComment Comment `json:"initial_comment"` 84 | CommentsCount int `json:"comments_count"` 85 | NumStars int `json:"num_stars"` 86 | IsStarred bool `json:"is_starred"` 87 | } 88 | 89 | // FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request 90 | type FileUploadParameters struct { 91 | File string 92 | Content string 93 | Filetype string 94 | Filename string 95 | Title string 96 | InitialComment string 97 | Channels []string 98 | } 99 | 100 | // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request 101 | type GetFilesParameters struct { 102 | User string 103 | Channel string 104 | TimestampFrom JSONTime 105 | TimestampTo JSONTime 106 | Types string 107 | Count int 108 | Page int 109 | } 110 | 111 | type fileResponseFull struct { 112 | File `json:"file"` 113 | Paging `json:"paging"` 114 | Comments []Comment `json:"comments"` 115 | Files []File `json:"files"` 116 | 117 | SlackResponse 118 | } 119 | 120 | // NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set 121 | func NewGetFilesParameters() GetFilesParameters { 122 | return GetFilesParameters{ 123 | User: DEFAULT_FILES_USER, 124 | Channel: DEFAULT_FILES_CHANNEL, 125 | TimestampFrom: DEFAULT_FILES_TS_FROM, 126 | TimestampTo: DEFAULT_FILES_TS_TO, 127 | Types: DEFAULT_FILES_TYPES, 128 | Count: DEFAULT_FILES_COUNT, 129 | Page: DEFAULT_FILES_PAGE, 130 | } 131 | } 132 | 133 | func fileRequest(path string, values url.Values, debug bool) (*fileResponseFull, error) { 134 | response := &fileResponseFull{} 135 | err := post(path, values, response, debug) 136 | if err != nil { 137 | return nil, err 138 | } 139 | if !response.Ok { 140 | return nil, errors.New(response.Error) 141 | } 142 | return response, nil 143 | } 144 | 145 | // GetFileInfo retrieves a file and related comments 146 | func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { 147 | values := url.Values{ 148 | "token": {api.config.token}, 149 | "file": {fileID}, 150 | "count": {strconv.Itoa(count)}, 151 | "page": {strconv.Itoa(page)}, 152 | } 153 | response, err := fileRequest("files.info", values, api.debug) 154 | if err != nil { 155 | return nil, nil, nil, err 156 | } 157 | return &response.File, response.Comments, &response.Paging, nil 158 | } 159 | 160 | // GetFiles retrieves all files according to the parameters given 161 | func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { 162 | values := url.Values{ 163 | "token": {api.config.token}, 164 | } 165 | if params.User != DEFAULT_FILES_USER { 166 | values.Add("user", params.User) 167 | } 168 | if params.Channel != DEFAULT_FILES_CHANNEL { 169 | values.Add("channel", params.Channel) 170 | } 171 | // XXX: this is broken. fix it with a proper unix timestamp 172 | if params.TimestampFrom != DEFAULT_FILES_TS_FROM { 173 | values.Add("ts_from", params.TimestampFrom.String()) 174 | } 175 | if params.TimestampTo != DEFAULT_FILES_TS_TO { 176 | values.Add("ts_to", params.TimestampTo.String()) 177 | } 178 | if params.Types != DEFAULT_FILES_TYPES { 179 | values.Add("types", params.Types) 180 | } 181 | if params.Count != DEFAULT_FILES_COUNT { 182 | values.Add("count", strconv.Itoa(params.Count)) 183 | } 184 | if params.Page != DEFAULT_FILES_PAGE { 185 | values.Add("page", strconv.Itoa(params.Page)) 186 | } 187 | response, err := fileRequest("files.list", values, api.debug) 188 | if err != nil { 189 | return nil, nil, err 190 | } 191 | return response.Files, &response.Paging, nil 192 | } 193 | 194 | // UploadFile uploads a file 195 | func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { 196 | // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More 197 | // investigation needed, but for now this will do. 198 | _, err = api.AuthTest() 199 | if err != nil { 200 | return nil, err 201 | } 202 | response := &fileResponseFull{} 203 | values := url.Values{ 204 | "token": {api.config.token}, 205 | } 206 | if params.Filetype != "" { 207 | values.Add("filetype", params.Filetype) 208 | } 209 | if params.Filename != "" { 210 | values.Add("filename", params.Filename) 211 | } 212 | if params.Title != "" { 213 | values.Add("title", params.Title) 214 | } 215 | if params.InitialComment != "" { 216 | values.Add("initial_comment", params.InitialComment) 217 | } 218 | if len(params.Channels) != 0 { 219 | values.Add("channels", strings.Join(params.Channels, ",")) 220 | } 221 | if params.Content != "" { 222 | values.Add("content", params.Content) 223 | err = post("files.upload", values, response, api.debug) 224 | } else if params.File != "" { 225 | err = postWithMultipartResponse("files.upload", params.File, values, response, api.debug) 226 | } 227 | if err != nil { 228 | return nil, err 229 | } 230 | if !response.Ok { 231 | return nil, errors.New(response.Error) 232 | } 233 | return &response.File, nil 234 | } 235 | 236 | // DeleteFile deletes a file 237 | func (api *Client) DeleteFile(fileID string) error { 238 | values := url.Values{ 239 | "token": {api.config.token}, 240 | "file": {fileID}, 241 | } 242 | _, err := fileRequest("files.delete", values, api.debug) 243 | if err != nil { 244 | return err 245 | } 246 | return nil 247 | 248 | } 249 | 250 | // RevokeFilePublicURL disables public/external sharing for a file 251 | func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { 252 | values := url.Values{ 253 | "token": {api.config.token}, 254 | "file": {fileID}, 255 | } 256 | response, err := fileRequest("files.revokePublicURL", values, api.debug) 257 | if err != nil { 258 | return nil, err 259 | } 260 | return &response.File, nil 261 | } 262 | 263 | // ShareFilePublicURL enabled public/external sharing for a file 264 | func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) { 265 | values := url.Values{ 266 | "token": {api.config.token}, 267 | "file": {fileID}, 268 | } 269 | response, err := fileRequest("files.sharedPublicURL", values, api.debug) 270 | if err != nil { 271 | return nil, nil, nil, err 272 | } 273 | return &response.File, response.Comments, &response.Paging, nil 274 | } 275 | -------------------------------------------------------------------------------- /vendor/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 | groupConversation 12 | IsGroup bool `json:"is_group"` 13 | } 14 | 15 | type groupResponseFull struct { 16 | Group Group `json:"group"` 17 | Groups []Group `json:"groups"` 18 | Purpose string `json:"purpose"` 19 | Topic string `json:"topic"` 20 | NotInGroup bool `json:"not_in_group"` 21 | NoOp bool `json:"no_op"` 22 | AlreadyClosed bool `json:"already_closed"` 23 | AlreadyOpen bool `json:"already_open"` 24 | AlreadyInGroup bool `json:"already_in_group"` 25 | Channel Channel `json:"channel"` 26 | History 27 | SlackResponse 28 | } 29 | 30 | func groupRequest(path string, values url.Values, debug bool) (*groupResponseFull, error) { 31 | response := &groupResponseFull{} 32 | err := post(path, values, response, debug) 33 | if err != nil { 34 | return nil, err 35 | } 36 | if !response.Ok { 37 | return nil, errors.New(response.Error) 38 | } 39 | return response, nil 40 | } 41 | 42 | // ArchiveGroup archives a private group 43 | func (api *Client) ArchiveGroup(group string) error { 44 | values := url.Values{ 45 | "token": {api.config.token}, 46 | "channel": {group}, 47 | } 48 | _, err := groupRequest("groups.archive", values, api.debug) 49 | if err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | 55 | // UnarchiveGroup unarchives a private group 56 | func (api *Client) UnarchiveGroup(group string) error { 57 | values := url.Values{ 58 | "token": {api.config.token}, 59 | "channel": {group}, 60 | } 61 | _, err := groupRequest("groups.unarchive", values, api.debug) 62 | if err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | // CreateGroup creates a private group 69 | func (api *Client) CreateGroup(group string) (*Group, error) { 70 | values := url.Values{ 71 | "token": {api.config.token}, 72 | "name": {group}, 73 | } 74 | response, err := groupRequest("groups.create", values, api.debug) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return &response.Group, nil 79 | } 80 | 81 | // CreateChildGroup creates a new private group archiving the old one 82 | // This method takes an existing private group and performs the following steps: 83 | // 1. Renames the existing group (from "example" to "example-archived"). 84 | // 2. Archives the existing group. 85 | // 3. Creates a new group with the name of the existing group. 86 | // 4. Adds all members of the existing group to the new group. 87 | func (api *Client) CreateChildGroup(group string) (*Group, error) { 88 | values := url.Values{ 89 | "token": {api.config.token}, 90 | "channel": {group}, 91 | } 92 | response, err := groupRequest("groups.createChild", values, api.debug) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return &response.Group, nil 97 | } 98 | 99 | // CloseGroup closes a private group 100 | func (api *Client) CloseGroup(group string) (bool, bool, error) { 101 | values := url.Values{ 102 | "token": {api.config.token}, 103 | "channel": {group}, 104 | } 105 | response, err := imRequest("groups.close", values, api.debug) 106 | if err != nil { 107 | return false, false, err 108 | } 109 | return response.NoOp, response.AlreadyClosed, nil 110 | } 111 | 112 | // GetGroupHistory fetches all the history for a private group 113 | func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) { 114 | values := url.Values{ 115 | "token": {api.config.token}, 116 | "channel": {group}, 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 | if params.Unreads != DEFAULT_HISTORY_UNREADS { 135 | if params.Unreads { 136 | values.Add("unreads", "1") 137 | } else { 138 | values.Add("unreads", "0") 139 | } 140 | } 141 | response, err := groupRequest("groups.history", values, api.debug) 142 | if err != nil { 143 | return nil, err 144 | } 145 | return &response.History, nil 146 | } 147 | 148 | // InviteUserToGroup invites a specific user to a private group 149 | func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) { 150 | values := url.Values{ 151 | "token": {api.config.token}, 152 | "channel": {group}, 153 | "user": {user}, 154 | } 155 | response, err := groupRequest("groups.invite", values, api.debug) 156 | if err != nil { 157 | return nil, false, err 158 | } 159 | return &response.Group, response.AlreadyInGroup, nil 160 | } 161 | 162 | // LeaveGroup makes authenticated user leave the group 163 | func (api *Client) LeaveGroup(group string) error { 164 | values := url.Values{ 165 | "token": {api.config.token}, 166 | "channel": {group}, 167 | } 168 | _, err := groupRequest("groups.leave", values, api.debug) 169 | if err != nil { 170 | return err 171 | } 172 | return nil 173 | } 174 | 175 | // KickUserFromGroup kicks a user from a group 176 | func (api *Client) KickUserFromGroup(group, user string) error { 177 | values := url.Values{ 178 | "token": {api.config.token}, 179 | "channel": {group}, 180 | "user": {user}, 181 | } 182 | _, err := groupRequest("groups.kick", values, api.debug) 183 | if err != nil { 184 | return err 185 | } 186 | return nil 187 | } 188 | 189 | // GetGroups retrieves all groups 190 | func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { 191 | values := url.Values{ 192 | "token": {api.config.token}, 193 | } 194 | if excludeArchived { 195 | values.Add("exclude_archived", "1") 196 | } 197 | response, err := groupRequest("groups.list", values, api.debug) 198 | if err != nil { 199 | return nil, err 200 | } 201 | return response.Groups, nil 202 | } 203 | 204 | // GetGroupInfo retrieves the given group 205 | func (api *Client) GetGroupInfo(group string) (*Group, error) { 206 | values := url.Values{ 207 | "token": {api.config.token}, 208 | "channel": {group}, 209 | } 210 | response, err := groupRequest("groups.info", values, api.debug) 211 | if err != nil { 212 | return nil, err 213 | } 214 | return &response.Group, nil 215 | } 216 | 217 | // SetGroupReadMark sets the read mark on a private group 218 | // Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a 219 | // timer before making the call. In this way, any further updates needed during the timeout will not generate extra 220 | // calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live 221 | // channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. 222 | func (api *Client) SetGroupReadMark(group, ts string) error { 223 | values := url.Values{ 224 | "token": {api.config.token}, 225 | "channel": {group}, 226 | "ts": {ts}, 227 | } 228 | _, err := groupRequest("groups.mark", values, api.debug) 229 | if err != nil { 230 | return err 231 | } 232 | return nil 233 | } 234 | 235 | // OpenGroup opens a private group 236 | func (api *Client) OpenGroup(group string) (bool, bool, error) { 237 | values := url.Values{ 238 | "token": {api.config.token}, 239 | "channel": {group}, 240 | } 241 | response, err := groupRequest("groups.open", values, api.debug) 242 | if err != nil { 243 | return false, false, err 244 | } 245 | return response.NoOp, response.AlreadyOpen, nil 246 | } 247 | 248 | // RenameGroup renames a group 249 | // XXX: They return a channel, not a group. What is this crap? :( 250 | // Inconsistent api it seems. 251 | func (api *Client) RenameGroup(group, name string) (*Channel, error) { 252 | values := url.Values{ 253 | "token": {api.config.token}, 254 | "channel": {group}, 255 | "name": {name}, 256 | } 257 | // XXX: the created entry in this call returns a string instead of a number 258 | // so I may have to do some workaround to solve it. 259 | response, err := groupRequest("groups.rename", values, api.debug) 260 | if err != nil { 261 | return nil, err 262 | } 263 | return &response.Channel, nil 264 | 265 | } 266 | 267 | // SetGroupPurpose sets the group purpose 268 | func (api *Client) SetGroupPurpose(group, purpose string) (string, error) { 269 | values := url.Values{ 270 | "token": {api.config.token}, 271 | "channel": {group}, 272 | "purpose": {purpose}, 273 | } 274 | response, err := groupRequest("groups.setPurpose", values, api.debug) 275 | if err != nil { 276 | return "", err 277 | } 278 | return response.Purpose, nil 279 | } 280 | 281 | // SetGroupTopic sets the group topic 282 | func (api *Client) SetGroupTopic(group, topic string) (string, error) { 283 | values := url.Values{ 284 | "token": {api.config.token}, 285 | "channel": {group}, 286 | "topic": {topic}, 287 | } 288 | response, err := groupRequest("groups.setTopic", values, api.debug) 289 | if err != nil { 290 | return "", err 291 | } 292 | return response.Topic, nil 293 | } 294 | -------------------------------------------------------------------------------- /vendor/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 | DEFAULT_HISTORY_UNREADS = false 9 | ) 10 | 11 | // HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs 12 | type HistoryParameters struct { 13 | Latest string 14 | Oldest string 15 | Count int 16 | Inclusive bool 17 | Unreads bool 18 | } 19 | 20 | // History contains message history information needed to navigate a Channel / Group / DM history 21 | type History struct { 22 | Latest string `json:"latest"` 23 | Messages []Message `json:"messages"` 24 | HasMore bool `json:"has_more"` 25 | } 26 | 27 | // NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set 28 | func NewHistoryParameters() HistoryParameters { 29 | return HistoryParameters{ 30 | Latest: DEFAULT_HISTORY_LATEST, 31 | Oldest: DEFAULT_HISTORY_OLDEST, 32 | Count: DEFAULT_HISTORY_COUNT, 33 | Inclusive: DEFAULT_HISTORY_INCLUSIVE, 34 | Unreads: DEFAULT_HISTORY_UNREADS, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/im.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 | conversation 26 | IsIM bool `json:"is_im"` 27 | User 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 := post(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 *Client) CloseIMChannel(channel string) (bool, bool, error) { 45 | values := url.Values{ 46 | "token": {api.config.token}, 47 | "channel": {channel}, 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 channel ID 58 | func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) { 59 | values := url.Values{ 60 | "token": {api.config.token}, 61 | "user": {user}, 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 *Client) MarkIMChannel(channel, ts string) (err error) { 72 | values := url.Values{ 73 | "token": {api.config.token}, 74 | "channel": {channel}, 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 *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) { 86 | values := url.Values{ 87 | "token": {api.config.token}, 88 | "channel": {channel}, 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 | if params.Unreads != DEFAULT_HISTORY_UNREADS { 107 | if params.Unreads { 108 | values.Add("unreads", "1") 109 | } else { 110 | values.Add("unreads", "0") 111 | } 112 | } 113 | response, err := imRequest("im.history", values, api.debug) 114 | if err != nil { 115 | return nil, err 116 | } 117 | return &response.History, nil 118 | } 119 | 120 | // GetIMChannels returns the list of direct message channels 121 | func (api *Client) GetIMChannels() ([]IM, error) { 122 | values := url.Values{ 123 | "token": {api.config.token}, 124 | } 125 | response, err := imRequest("im.list", values, api.debug) 126 | if err != nil { 127 | return nil, err 128 | } 129 | return response.IMs, nil 130 | } 131 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/info.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // UserPrefs needs to be implemented 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 := t.Time() 122 | return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) 123 | } 124 | 125 | // Time returns a `time.Time` representation of this value. 126 | func (t JSONTime) Time() time.Time { 127 | return time.Unix(int64(t), 0) 128 | } 129 | 130 | // Team contains details about a team 131 | type Team struct { 132 | ID string `json:"id"` 133 | Name string `json:"name"` 134 | Domain string `json:"domain"` 135 | } 136 | 137 | // Icons XXX: needs further investigation 138 | type Icons struct { 139 | Image48 string `json:"image_48"` 140 | } 141 | 142 | // Bot contains information about a bot 143 | type Bot struct { 144 | ID string `json:"id"` 145 | Name string `json:"name"` 146 | Deleted bool `json:"deleted"` 147 | Icons Icons `json:"icons"` 148 | } 149 | 150 | // Info contains various details about Users, Channels, Bots and the authenticated user. 151 | // It is returned by StartRTM or included in the "ConnectedEvent" RTM event. 152 | type Info struct { 153 | URL string `json:"url,omitempty"` 154 | User *UserDetails `json:"self,omitempty"` 155 | Team *Team `json:"team,omitempty"` 156 | Users []User `json:"users,omitempty"` 157 | Channels []Channel `json:"channels,omitempty"` 158 | Groups []Group `json:"groups,omitempty"` 159 | Bots []Bot `json:"bots,omitempty"` 160 | IMs []IM `json:"ims,omitempty"` 161 | } 162 | 163 | type infoResponseFull struct { 164 | Info 165 | WebResponse 166 | } 167 | 168 | // GetBotByID returns a bot given a bot id 169 | func (info Info) GetBotByID(botID string) *Bot { 170 | for _, bot := range info.Bots { 171 | if bot.ID == botID { 172 | return &bot 173 | } 174 | } 175 | return nil 176 | } 177 | 178 | // GetUserByID returns a user given a user id 179 | func (info Info) GetUserByID(userID string) *User { 180 | for _, user := range info.Users { 181 | if user.ID == userID { 182 | return &user 183 | } 184 | } 185 | return nil 186 | } 187 | 188 | // GetChannelByID returns a channel given a channel id 189 | func (info Info) GetChannelByID(channelID string) *Channel { 190 | for _, channel := range info.Channels { 191 | if channel.ID == channelID { 192 | return &channel 193 | } 194 | } 195 | return nil 196 | } 197 | 198 | // GetGroupByID returns a group given a group id 199 | func (info Info) GetGroupByID(groupID string) *Group { 200 | for _, group := range info.Groups { 201 | if group.ID == groupID { 202 | return &group 203 | } 204 | } 205 | return nil 206 | } 207 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/item.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | const ( 4 | TYPE_MESSAGE = "message" 5 | TYPE_FILE = "file" 6 | TYPE_FILE_COMMENT = "file_comment" 7 | TYPE_CHANNEL = "channel" 8 | TYPE_IM = "im" 9 | TYPE_GROUP = "group" 10 | ) 11 | 12 | // Item is any type of slack message - message, file, or file comment. 13 | type Item struct { 14 | Type string `json:"type"` 15 | Channel string `json:"channel,omitempty"` 16 | Message *Message `json:"message,omitempty"` 17 | File *File `json:"file,omitempty"` 18 | Comment *Comment `json:"comment,omitempty"` 19 | Timestamp string `json:"ts,omitempty"` 20 | } 21 | 22 | // NewMessageItem turns a message on a channel into a typed message struct. 23 | func NewMessageItem(ch string, m *Message) Item { 24 | return Item{Type: TYPE_MESSAGE, Channel: ch, Message: m} 25 | } 26 | 27 | // NewFileItem turns a file into a typed file struct. 28 | func NewFileItem(f *File) Item { 29 | return Item{Type: TYPE_FILE, File: f} 30 | } 31 | 32 | // NewFileCommentItem turns a file and comment into a typed file_comment struct. 33 | func NewFileCommentItem(f *File, c *Comment) Item { 34 | return Item{Type: TYPE_FILE_COMMENT, File: f, Comment: c} 35 | } 36 | 37 | // NewChannelItem turns a channel id into a typed channel struct. 38 | func NewChannelItem(ch string) Item { 39 | return Item{Type: TYPE_CHANNEL, Channel: ch} 40 | } 41 | 42 | // NewIMItem turns a channel id into a typed im struct. 43 | func NewIMItem(ch string) Item { 44 | return Item{Type: TYPE_IM, Channel: ch} 45 | } 46 | 47 | // NewGroupItem turns a channel id into a typed group struct. 48 | func NewGroupItem(ch string) Item { 49 | return Item{Type: TYPE_GROUP, Channel: ch} 50 | } 51 | 52 | // ItemRef is a reference to a message of any type. One of FileID, 53 | // CommentId, or the combination of ChannelId and Timestamp must be 54 | // specified. 55 | type ItemRef struct { 56 | Channel string `json:"channel"` 57 | Timestamp string `json:"timestamp"` 58 | File string `json:"file"` 59 | Comment string `json:"file_comment"` 60 | } 61 | 62 | // NewRefToMessage initializes a reference to to a message. 63 | func NewRefToMessage(channel, timestamp string) ItemRef { 64 | return ItemRef{Channel: channel, Timestamp: timestamp} 65 | } 66 | 67 | // NewRefToFile initializes a reference to a file. 68 | func NewRefToFile(file string) ItemRef { 69 | return ItemRef{File: file} 70 | } 71 | 72 | // NewRefToComment initializes a reference to a file comment. 73 | func NewRefToComment(comment string) ItemRef { 74 | return ItemRef{Comment: comment} 75 | } 76 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/messageID.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import "sync" 4 | 5 | // IDGenerator provides an interface for generating integer ID values. 6 | type IDGenerator interface { 7 | Next() int 8 | } 9 | 10 | // NewSafeID returns a new instance of an IDGenerator which is safe for 11 | // concurrent use by multiple goroutines. 12 | func NewSafeID(startID int) IDGenerator { 13 | return &safeID{ 14 | nextID: startID, 15 | mutex: &sync.Mutex{}, 16 | } 17 | } 18 | 19 | type safeID struct { 20 | nextID int 21 | mutex *sync.Mutex 22 | } 23 | 24 | func (s *safeID) Next() int { 25 | s.mutex.Lock() 26 | defer s.mutex.Unlock() 27 | id := s.nextID 28 | s.nextID++ 29 | return id 30 | } 31 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/messages.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // OutgoingMessage is used for the realtime API, and seems incomplete. 4 | type OutgoingMessage struct { 5 | ID int `json:"id"` 6 | Channel string `json:"channel,omitempty"` 7 | Text string `json:"text,omitempty"` 8 | Type string `json:"type,omitempty"` 9 | } 10 | 11 | // Message is an auxiliary type to allow us to have a message containing sub messages 12 | type Message struct { 13 | Msg 14 | SubMessage *Msg `json:"message,omitempty"` 15 | } 16 | 17 | // Msg contains information about a slack message 18 | type Msg struct { 19 | // Basic Message 20 | Type string `json:"type,omitempty"` 21 | Channel string `json:"channel,omitempty"` 22 | User string `json:"user,omitempty"` 23 | Text string `json:"text,omitempty"` 24 | Timestamp string `json:"ts,omitempty"` 25 | IsStarred bool `json:"is_starred,omitempty"` 26 | PinnedTo []string `json:"pinned_to, omitempty"` 27 | Attachments []Attachment `json:"attachments,omitempty"` 28 | Edited *Edited `json:"edited,omitempty"` 29 | 30 | // Message Subtypes 31 | SubType string `json:"subtype,omitempty"` 32 | 33 | // Hidden Subtypes 34 | Hidden bool `json:"hidden,omitempty"` // message_changed, message_deleted, unpinned_item 35 | DeletedTimestamp string `json:"deleted_ts,omitempty"` // message_deleted 36 | EventTimestamp string `json:"event_ts,omitempty"` 37 | 38 | // bot_message (https://api.slack.com/events/message/bot_message) 39 | BotID string `json:"bot_id,omitempty"` 40 | Username string `json:"username,omitempty"` 41 | Icons *Icon `json:"icons,omitempty"` 42 | 43 | // channel_join, group_join 44 | Inviter string `json:"inviter,omitempty"` 45 | 46 | // channel_topic, group_topic 47 | Topic string `json:"topic,omitempty"` 48 | 49 | // channel_purpose, group_purpose 50 | Purpose string `json:"purpose,omitempty"` 51 | 52 | // channel_name, group_name 53 | Name string `json:"name,omitempty"` 54 | OldName string `json:"old_name,omitempty"` 55 | 56 | // channel_archive, group_archive 57 | Members []string `json:"members,omitempty"` 58 | 59 | // file_share, file_comment, file_mention 60 | File *File `json:"file,omitempty"` 61 | 62 | // file_share 63 | Upload bool `json:"upload,omitempty"` 64 | 65 | // file_comment 66 | Comment *Comment `json:"comment,omitempty"` 67 | 68 | // pinned_item 69 | ItemType string `json:"item_type,omitempty"` 70 | 71 | // https://api.slack.com/rtm 72 | ReplyTo int `json:"reply_to,omitempty"` 73 | Team string `json:"team,omitempty"` 74 | 75 | // reactions 76 | Reactions []ItemReaction `json:"reactions,omitempty"` 77 | } 78 | 79 | // Icon is used for bot messages 80 | type Icon struct { 81 | IconURL string `json:"icon_url,omitempty"` 82 | IconEmoji string `json:"icon_emoji,omitempty"` 83 | } 84 | 85 | // Edited indicates that a message has been edited. 86 | type Edited struct { 87 | User string `json:"user,omitempty"` 88 | Timestamp string `json:"ts,omitempty"` 89 | } 90 | 91 | // Event contains the event type 92 | type Event struct { 93 | Type string `json:"type,omitempty"` 94 | } 95 | 96 | // Ping contains information about a Ping Event 97 | type Ping struct { 98 | ID int `json:"id"` 99 | Type string `json:"type"` 100 | } 101 | 102 | // Pong contains information about a Pong Event 103 | type Pong struct { 104 | Type string `json:"type"` 105 | ReplyTo int `json:"reply_to"` 106 | } 107 | 108 | // NewOutgoingMessage prepares an OutgoingMessage that the user can 109 | // use to send a message. Use this function to properly set the 110 | // messageID. 111 | func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage { 112 | id := rtm.idGen.Next() 113 | return &OutgoingMessage{ 114 | ID: id, 115 | Type: "message", 116 | Channel: channel, 117 | Text: text, 118 | } 119 | } 120 | 121 | // NewTypingMessage prepares an OutgoingMessage that the user can 122 | // use to send as a typing indicator. Use this function to properly set the 123 | // messageID. 124 | func (rtm *RTM) NewTypingMessage(channel string) *OutgoingMessage { 125 | id := rtm.idGen.Next() 126 | return &OutgoingMessage{ 127 | ID: id, 128 | Type: "typing", 129 | Channel: channel, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /vendor/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 | type WebResponse struct { 20 | Ok bool `json:"ok"` 21 | Error *WebError `json:"error"` 22 | } 23 | 24 | type WebError string 25 | 26 | func (s WebError) Error() string { 27 | return string(s) 28 | } 29 | 30 | func fileUploadReq(path, fpath string, values url.Values) (*http.Request, error) { 31 | fullpath, err := filepath.Abs(fpath) 32 | if err != nil { 33 | return nil, err 34 | } 35 | file, err := os.Open(fullpath) 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer file.Close() 40 | 41 | body := &bytes.Buffer{} 42 | wr := multipart.NewWriter(body) 43 | 44 | ioWriter, err := wr.CreateFormFile("file", filepath.Base(fullpath)) 45 | if err != nil { 46 | wr.Close() 47 | return nil, err 48 | } 49 | bytes, err := io.Copy(ioWriter, file) 50 | if err != nil { 51 | wr.Close() 52 | return nil, err 53 | } 54 | // Close the multipart writer or the footer won't be written 55 | wr.Close() 56 | stat, err := file.Stat() 57 | if err != nil { 58 | return nil, err 59 | } 60 | if bytes != stat.Size() { 61 | return nil, errors.New("could not read the whole file") 62 | } 63 | req, err := http.NewRequest("POST", path, body) 64 | if err != nil { 65 | return nil, err 66 | } 67 | req.Header.Add("Content-Type", wr.FormDataContentType()) 68 | req.URL.RawQuery = (values).Encode() 69 | return req, nil 70 | } 71 | 72 | func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error { 73 | response, err := ioutil.ReadAll(body) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | // FIXME: will be api.Debugf 79 | if debug { 80 | log.Printf("parseResponseBody: %s\n", string(response)) 81 | } 82 | 83 | err = json.Unmarshal(response, &intf) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func postWithMultipartResponse(path string, filepath string, values url.Values, intf interface{}, debug bool) error { 92 | req, err := fileUploadReq(SLACK_API+path, filepath, values) 93 | client := &http.Client{} 94 | resp, err := client.Do(req) 95 | if err != nil { 96 | return err 97 | } 98 | defer resp.Body.Close() 99 | return parseResponseBody(resp.Body, &intf, debug) 100 | } 101 | 102 | func postForm(endpoint string, values url.Values, intf interface{}, debug bool) error { 103 | resp, err := http.PostForm(endpoint, values) 104 | if err != nil { 105 | return err 106 | } 107 | defer resp.Body.Close() 108 | 109 | return parseResponseBody(resp.Body, &intf, debug) 110 | } 111 | 112 | func post(path string, values url.Values, intf interface{}, debug bool) error { 113 | return postForm(SLACK_API+path, values, intf, debug) 114 | } 115 | 116 | func parseAdminResponse(method string, teamName string, values url.Values, intf interface{}, debug bool) error { 117 | endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix()) 118 | return postForm(endpoint, values, intf, debug) 119 | } 120 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/oauth.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | ) 7 | 8 | type OAuthResponseIncomingWebhook struct { 9 | URL string `json:"url"` 10 | Channel string `json:"channel"` 11 | ConfigurationURL string `json:"configuration_url"` 12 | } 13 | 14 | type OAuthResponseBot struct { 15 | BotUserID string `json:"bot_user_id"` 16 | BotAccessToken string `json:"bot_access_token"` 17 | } 18 | 19 | type OAuthResponse struct { 20 | AccessToken string `json:"access_token"` 21 | Scope string `json:"scope"` 22 | TeamName string `json:"team_name"` 23 | TeamID string `json:"team_id"` 24 | IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"` 25 | Bot OAuthResponseBot `json:"bot"` 26 | SlackResponse 27 | } 28 | 29 | // GetOAuthToken retrieves an AccessToken 30 | func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { 31 | response, err := GetOAuthResponse(clientID, clientSecret, code, redirectURI, debug) 32 | if err != nil { 33 | return "", "", err 34 | } 35 | return response.AccessToken, response.Scope, nil 36 | } 37 | 38 | func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) { 39 | values := url.Values{ 40 | "client_id": {clientID}, 41 | "client_secret": {clientSecret}, 42 | "code": {code}, 43 | "redirect_uri": {redirectURI}, 44 | } 45 | response := &OAuthResponse{} 46 | err = post("oauth.access", values, response, debug) 47 | if err != nil { 48 | return nil, err 49 | } 50 | if !response.Ok { 51 | return nil, errors.New(response.Error) 52 | } 53 | return response, nil 54 | } 55 | -------------------------------------------------------------------------------- /vendor/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 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/pins.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | ) 7 | 8 | type listPinsResponseFull struct { 9 | Items []Item 10 | Paging `json:"paging"` 11 | SlackResponse 12 | } 13 | 14 | // AddPin pins an item in a channel 15 | func (api *Client) AddPin(channel string, item ItemRef) error { 16 | values := url.Values{ 17 | "channel": {channel}, 18 | "token": {api.config.token}, 19 | } 20 | if item.Timestamp != "" { 21 | values.Set("timestamp", string(item.Timestamp)) 22 | } 23 | if item.File != "" { 24 | values.Set("file", string(item.File)) 25 | } 26 | if item.Comment != "" { 27 | values.Set("file_comment", string(item.Comment)) 28 | } 29 | response := &SlackResponse{} 30 | if err := post("pins.add", values, response, api.debug); err != nil { 31 | return err 32 | } 33 | if !response.Ok { 34 | return errors.New(response.Error) 35 | } 36 | return nil 37 | } 38 | 39 | // RemovePin un-pins an item from a channel 40 | func (api *Client) RemovePin(channel string, item ItemRef) error { 41 | values := url.Values{ 42 | "channel": {channel}, 43 | "token": {api.config.token}, 44 | } 45 | if item.Timestamp != "" { 46 | values.Set("timestamp", string(item.Timestamp)) 47 | } 48 | if item.File != "" { 49 | values.Set("file", string(item.File)) 50 | } 51 | if item.Comment != "" { 52 | values.Set("file_comment", string(item.Comment)) 53 | } 54 | response := &SlackResponse{} 55 | if err := post("pins.remove", values, response, api.debug); err != nil { 56 | return err 57 | } 58 | if !response.Ok { 59 | return errors.New(response.Error) 60 | } 61 | return nil 62 | } 63 | 64 | // ListPins returns information about the items a user reacted to. 65 | func (api *Client) ListPins(channel string) ([]Item, *Paging, error) { 66 | values := url.Values{ 67 | "channel": {channel}, 68 | "token": {api.config.token}, 69 | } 70 | response := &listPinsResponseFull{} 71 | err := post("pins.list", values, response, api.debug) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | if !response.Ok { 76 | return nil, nil, errors.New(response.Error) 77 | } 78 | return response.Items, &response.Paging, nil 79 | } 80 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/reactions.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | // ItemReaction is the reactions that have happened on an item. 10 | type ItemReaction struct { 11 | Name string `json:"name"` 12 | Count int `json:"count"` 13 | Users []string `json:"users"` 14 | } 15 | 16 | // ReactedItem is an item that was reacted to, and the details of the 17 | // reactions. 18 | type ReactedItem struct { 19 | Item 20 | Reactions []ItemReaction 21 | } 22 | 23 | // GetReactionsParameters is the inputs to get reactions to an item. 24 | type GetReactionsParameters struct { 25 | Full bool 26 | } 27 | 28 | // NewGetReactionsParameters initializes the inputs to get reactions to an item. 29 | func NewGetReactionsParameters() GetReactionsParameters { 30 | return GetReactionsParameters{ 31 | Full: false, 32 | } 33 | } 34 | 35 | type getReactionsResponseFull struct { 36 | Type string 37 | M struct { 38 | Reactions []ItemReaction 39 | } `json:"message"` 40 | F struct { 41 | Reactions []ItemReaction 42 | } `json:"file"` 43 | FC struct { 44 | Reactions []ItemReaction 45 | } `json:"comment"` 46 | SlackResponse 47 | } 48 | 49 | func (res getReactionsResponseFull) extractReactions() []ItemReaction { 50 | switch res.Type { 51 | case "message": 52 | return res.M.Reactions 53 | case "file": 54 | return res.F.Reactions 55 | case "file_comment": 56 | return res.FC.Reactions 57 | } 58 | return []ItemReaction{} 59 | } 60 | 61 | const ( 62 | DEFAULT_REACTIONS_USER = "" 63 | DEFAULT_REACTIONS_COUNT = 100 64 | DEFAULT_REACTIONS_PAGE = 1 65 | DEFAULT_REACTIONS_FULL = false 66 | ) 67 | 68 | // ListReactionsParameters is the inputs to find all reactions by a user. 69 | type ListReactionsParameters struct { 70 | User string 71 | Count int 72 | Page int 73 | Full bool 74 | } 75 | 76 | // NewListReactionsParameters initializes the inputs to find all reactions 77 | // performed by a user. 78 | func NewListReactionsParameters() ListReactionsParameters { 79 | return ListReactionsParameters{ 80 | User: DEFAULT_REACTIONS_USER, 81 | Count: DEFAULT_REACTIONS_COUNT, 82 | Page: DEFAULT_REACTIONS_PAGE, 83 | Full: DEFAULT_REACTIONS_FULL, 84 | } 85 | } 86 | 87 | type listReactionsResponseFull struct { 88 | Items []struct { 89 | Type string 90 | Channel string 91 | M struct { 92 | *Message 93 | } `json:"message"` 94 | F struct { 95 | *File 96 | Reactions []ItemReaction 97 | } `json:"file"` 98 | FC struct { 99 | *Comment 100 | Reactions []ItemReaction 101 | } `json:"comment"` 102 | } 103 | Paging `json:"paging"` 104 | SlackResponse 105 | } 106 | 107 | func (res listReactionsResponseFull) extractReactedItems() []ReactedItem { 108 | items := make([]ReactedItem, len(res.Items)) 109 | for i, input := range res.Items { 110 | item := ReactedItem{} 111 | item.Type = input.Type 112 | switch input.Type { 113 | case "message": 114 | item.Channel = input.Channel 115 | item.Message = input.M.Message 116 | item.Reactions = input.M.Reactions 117 | case "file": 118 | item.File = input.F.File 119 | item.Reactions = input.F.Reactions 120 | case "file_comment": 121 | item.File = input.F.File 122 | item.Comment = input.FC.Comment 123 | item.Reactions = input.FC.Reactions 124 | } 125 | items[i] = item 126 | } 127 | return items 128 | } 129 | 130 | // AddReaction adds a reaction emoji to a message, file or file comment. 131 | func (api *Client) AddReaction(name string, item ItemRef) error { 132 | values := url.Values{ 133 | "token": {api.config.token}, 134 | } 135 | if name != "" { 136 | values.Set("name", name) 137 | } 138 | if item.Channel != "" { 139 | values.Set("channel", string(item.Channel)) 140 | } 141 | if item.Timestamp != "" { 142 | values.Set("timestamp", string(item.Timestamp)) 143 | } 144 | if item.File != "" { 145 | values.Set("file", string(item.File)) 146 | } 147 | if item.Comment != "" { 148 | values.Set("file_comment", string(item.Comment)) 149 | } 150 | response := &SlackResponse{} 151 | if err := post("reactions.add", values, response, api.debug); err != nil { 152 | return err 153 | } 154 | if !response.Ok { 155 | return errors.New(response.Error) 156 | } 157 | return nil 158 | } 159 | 160 | // RemoveReaction removes a reaction emoji from a message, file or file comment. 161 | func (api *Client) RemoveReaction(name string, item ItemRef) error { 162 | values := url.Values{ 163 | "token": {api.config.token}, 164 | } 165 | if name != "" { 166 | values.Set("name", name) 167 | } 168 | if item.Channel != "" { 169 | values.Set("channel", string(item.Channel)) 170 | } 171 | if item.Timestamp != "" { 172 | values.Set("timestamp", string(item.Timestamp)) 173 | } 174 | if item.File != "" { 175 | values.Set("file", string(item.File)) 176 | } 177 | if item.Comment != "" { 178 | values.Set("file_comment", string(item.Comment)) 179 | } 180 | response := &SlackResponse{} 181 | if err := post("reactions.remove", values, response, api.debug); err != nil { 182 | return err 183 | } 184 | if !response.Ok { 185 | return errors.New(response.Error) 186 | } 187 | return nil 188 | } 189 | 190 | // GetReactions returns details about the reactions on an item. 191 | func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { 192 | values := url.Values{ 193 | "token": {api.config.token}, 194 | } 195 | if item.Channel != "" { 196 | values.Set("channel", string(item.Channel)) 197 | } 198 | if item.Timestamp != "" { 199 | values.Set("timestamp", string(item.Timestamp)) 200 | } 201 | if item.File != "" { 202 | values.Set("file", string(item.File)) 203 | } 204 | if item.Comment != "" { 205 | values.Set("file_comment", string(item.Comment)) 206 | } 207 | if params.Full != DEFAULT_REACTIONS_FULL { 208 | values.Set("full", strconv.FormatBool(params.Full)) 209 | } 210 | response := &getReactionsResponseFull{} 211 | if err := post("reactions.get", values, response, api.debug); err != nil { 212 | return nil, err 213 | } 214 | if !response.Ok { 215 | return nil, errors.New(response.Error) 216 | } 217 | return response.extractReactions(), nil 218 | } 219 | 220 | // ListReactions returns information about the items a user reacted to. 221 | func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) { 222 | values := url.Values{ 223 | "token": {api.config.token}, 224 | } 225 | if params.User != DEFAULT_REACTIONS_USER { 226 | values.Add("user", params.User) 227 | } 228 | if params.Count != DEFAULT_REACTIONS_COUNT { 229 | values.Add("count", strconv.Itoa(params.Count)) 230 | } 231 | if params.Page != DEFAULT_REACTIONS_PAGE { 232 | values.Add("page", strconv.Itoa(params.Page)) 233 | } 234 | if params.Full != DEFAULT_REACTIONS_FULL { 235 | values.Add("full", strconv.FormatBool(params.Full)) 236 | } 237 | response := &listReactionsResponseFull{} 238 | err := post("reactions.list", values, response, api.debug) 239 | if err != nil { 240 | return nil, nil, err 241 | } 242 | if !response.Ok { 243 | return nil, nil, errors.New(response.Error) 244 | } 245 | return response.extractReactedItems(), &response.Paging, nil 246 | } 247 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/rtm.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info 9 | // block. 10 | // 11 | // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` 12 | // on it. 13 | func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { 14 | response := &infoResponseFull{} 15 | err = post("rtm.start", url.Values{"token": {api.config.token}}, response, api.debug) 16 | if err != nil { 17 | return nil, "", fmt.Errorf("post: %s", err) 18 | } 19 | if !response.Ok { 20 | return nil, "", response.Error 21 | } 22 | 23 | // websocket.Dial does not accept url without the port (yet) 24 | // Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3 25 | // but slack returns the address with no port, so we have to fix it 26 | api.Debugln("Using URL:", response.Info.URL) 27 | websocketURL, err = websocketizeURLPort(response.Info.URL) 28 | if err != nil { 29 | return nil, "", fmt.Errorf("parsing response URL: %s", err) 30 | } 31 | 32 | return &response.Info, websocketURL, nil 33 | } 34 | 35 | // NewRTM returns a RTM, which provides a fully managed connection to 36 | // Slack's websocket-based Real-Time Messaging protocol./ 37 | func (api *Client) NewRTM() *RTM { 38 | return newRTM(api) 39 | } 40 | -------------------------------------------------------------------------------- /vendor/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 | User 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 | User 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 (api *Client) _search(path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) { 84 | values := url.Values{ 85 | "token": {api.config.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 := post(path, values, response, api.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 *Client) Search(query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) { 116 | response, err := api._search("search.all", query, params, true, true) 117 | if err != nil { 118 | return nil, nil, err 119 | } 120 | return &response.SearchMessages, &response.SearchFiles, nil 121 | } 122 | 123 | func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFiles, error) { 124 | response, err := api._search("search.files", query, params, true, false) 125 | if err != nil { 126 | return nil, err 127 | } 128 | return &response.SearchFiles, nil 129 | } 130 | 131 | func (api *Client) SearchMessages(query string, params SearchParameters) (*SearchMessages, error) { 132 | response, err := api._search("search.messages", query, params, false, true) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return &response.SearchMessages, nil 137 | } 138 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/url" 7 | ) 8 | 9 | /* 10 | Added as a var so that we can change this for testing purposes 11 | */ 12 | var SLACK_API string = "https://slack.com/api/" 13 | var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s" 14 | 15 | type SlackResponse struct { 16 | Ok bool `json:"ok"` 17 | Error string `json:"error"` 18 | } 19 | 20 | type AuthTestResponse struct { 21 | URL string `json:"url"` 22 | Team string `json:"team"` 23 | User string `json:"user"` 24 | TeamID string `json:"team_id"` 25 | UserID string `json:"user_id"` 26 | } 27 | 28 | type authTestResponseFull struct { 29 | SlackResponse 30 | AuthTestResponse 31 | } 32 | 33 | type Client struct { 34 | config struct { 35 | token string 36 | } 37 | info Info 38 | debug bool 39 | } 40 | 41 | func New(token string) *Client { 42 | s := &Client{} 43 | s.config.token = token 44 | return s 45 | } 46 | 47 | // AuthTest tests if the user is able to do authenticated requests or not 48 | func (api *Client) AuthTest() (response *AuthTestResponse, error error) { 49 | responseFull := &authTestResponseFull{} 50 | err := post("auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug) 51 | if err != nil { 52 | return nil, err 53 | } 54 | if !responseFull.Ok { 55 | return nil, errors.New(responseFull.Error) 56 | } 57 | return &responseFull.AuthTestResponse, nil 58 | } 59 | 60 | // SetDebug switches the api into debug mode 61 | // When in debug mode, it logs various info about what its doing 62 | // If you ever use this in production, don't call SetDebug(true) 63 | func (api *Client) SetDebug(debug bool) { 64 | api.debug = debug 65 | } 66 | 67 | func (api *Client) Debugf(format string, v ...interface{}) { 68 | if api.debug { 69 | log.Printf(format, v...) 70 | } 71 | } 72 | 73 | func (api *Client) Debugln(v ...interface{}) { 74 | if api.debug { 75 | log.Println(v...) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /vendor/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_USER = "" 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 | type StarredItem Item 22 | 23 | type listResponseFull struct { 24 | Items []Item `json:"items"` 25 | Paging `json:"paging"` 26 | SlackResponse 27 | } 28 | 29 | // NewStarsParameters initialises StarsParameters with default values 30 | func NewStarsParameters() StarsParameters { 31 | return StarsParameters{ 32 | User: DEFAULT_STARS_USER, 33 | Count: DEFAULT_STARS_COUNT, 34 | Page: DEFAULT_STARS_PAGE, 35 | } 36 | } 37 | 38 | // AddStar stars an item in a channel 39 | func (api *Client) AddStar(channel string, item ItemRef) error { 40 | values := url.Values{ 41 | "channel": {channel}, 42 | "token": {api.config.token}, 43 | } 44 | if item.Timestamp != "" { 45 | values.Set("timestamp", string(item.Timestamp)) 46 | } 47 | if item.File != "" { 48 | values.Set("file", string(item.File)) 49 | } 50 | if item.Comment != "" { 51 | values.Set("file_comment", string(item.Comment)) 52 | } 53 | response := &SlackResponse{} 54 | if err := post("stars.add", values, response, api.debug); err != nil { 55 | return err 56 | } 57 | if !response.Ok { 58 | return errors.New(response.Error) 59 | } 60 | return nil 61 | } 62 | 63 | // RemoveStar removes a starred item from a channel 64 | func (api *Client) RemoveStar(channel string, item ItemRef) error { 65 | values := url.Values{ 66 | "channel": {channel}, 67 | "token": {api.config.token}, 68 | } 69 | if item.Timestamp != "" { 70 | values.Set("timestamp", string(item.Timestamp)) 71 | } 72 | if item.File != "" { 73 | values.Set("file", string(item.File)) 74 | } 75 | if item.Comment != "" { 76 | values.Set("file_comment", string(item.Comment)) 77 | } 78 | response := &SlackResponse{} 79 | if err := post("stars.remove", values, response, api.debug); err != nil { 80 | return err 81 | } 82 | if !response.Ok { 83 | return errors.New(response.Error) 84 | } 85 | return nil 86 | } 87 | 88 | // ListStars returns information about the stars a user added 89 | func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) { 90 | values := url.Values{ 91 | "token": {api.config.token}, 92 | } 93 | if params.User != DEFAULT_STARS_USER { 94 | values.Add("user", params.User) 95 | } 96 | if params.Count != DEFAULT_STARS_COUNT { 97 | values.Add("count", strconv.Itoa(params.Count)) 98 | } 99 | if params.Page != DEFAULT_STARS_PAGE { 100 | values.Add("page", strconv.Itoa(params.Page)) 101 | } 102 | response := &listResponseFull{} 103 | err := post("stars.list", values, response, api.debug) 104 | if err != nil { 105 | return nil, nil, err 106 | } 107 | if !response.Ok { 108 | return nil, nil, errors.New(response.Error) 109 | } 110 | return response.Items, &response.Paging, nil 111 | } 112 | 113 | // GetStarred returns a list of StarredItem items. The user then has to iterate over them and figure out what they should 114 | // be looking at according to what is in the Type. 115 | // for _, item := range items { 116 | // switch c.Type { 117 | // case "file_comment": 118 | // log.Println(c.Comment) 119 | // case "file": 120 | // ... 121 | // 122 | // } 123 | // This function still exists to maintain backwards compatibility. 124 | // I exposed it as returning []StarredItem, so it shall stay as StarredItem 125 | func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) { 126 | items, paging, err := api.ListStars(params) 127 | if err != nil { 128 | return nil, nil, err 129 | } 130 | starredItems := make([]StarredItem, len(items)) 131 | for i, item := range items { 132 | starredItems[i] = StarredItem(item) 133 | } 134 | return starredItems, paging, nil 135 | } 136 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/team.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | ) 7 | 8 | type TeamResponse struct { 9 | Team TeamInfo `json:"team"` 10 | SlackResponse 11 | } 12 | 13 | type TeamInfo struct { 14 | ID string `json:"id"` 15 | Name string `json:"name"` 16 | Domain string `json:"domain"` 17 | EmailDomain string `json:"email_domain"` 18 | Icon map[string]interface{} `json:"icon"` 19 | } 20 | 21 | func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, error) { 22 | response := &TeamResponse{} 23 | err := post(path, values, response, debug) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | if !response.Ok { 29 | return nil, errors.New(response.Error) 30 | } 31 | 32 | return response, nil 33 | } 34 | 35 | // GetTeamInfo gets the Team Information of the user 36 | func (api *Client) GetTeamInfo() (*TeamInfo, error) { 37 | values := url.Values{ 38 | "token": {api.config.token}, 39 | } 40 | 41 | response, err := teamRequest("team.info", values, api.debug) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return &response.Team, nil 46 | } 47 | -------------------------------------------------------------------------------- /vendor/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 := post(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 *Client) GetUserPresence(user string) (*UserPresence, error) { 79 | values := url.Values{ 80 | "token": {api.config.token}, 81 | "user": {user}, 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 *Client) GetUserInfo(user string) (*User, error) { 92 | values := url.Values{ 93 | "token": {api.config.token}, 94 | "user": {user}, 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 *Client) GetUsers() ([]User, error) { 105 | values := url.Values{ 106 | "token": {api.config.token}, 107 | "presence": {"1"}, 108 | } 109 | response, err := userRequest("users.list", values, api.debug) 110 | if err != nil { 111 | return nil, err 112 | } 113 | return response.Members, nil 114 | } 115 | 116 | // SetUserAsActive marks the currently authenticated user as active 117 | func (api *Client) SetUserAsActive() error { 118 | values := url.Values{ 119 | "token": {api.config.token}, 120 | } 121 | _, err := userRequest("users.setActive", values, api.debug) 122 | if err != nil { 123 | return err 124 | } 125 | return nil 126 | } 127 | 128 | // SetUserPresence changes the currently authenticated user presence 129 | func (api *Client) SetUserPresence(presence string) error { 130 | values := url.Values{ 131 | "token": {api.config.token}, 132 | "presence": {presence}, 133 | } 134 | _, err := userRequest("users.setPresence", values, api.debug) 135 | if err != nil { 136 | return err 137 | } 138 | return nil 139 | 140 | } 141 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "time" 8 | 9 | "golang.org/x/net/websocket" 10 | ) 11 | 12 | const ( 13 | // MaxMessageTextLength is the current maximum message length in number of characters as defined here 14 | // https://api.slack.com/rtm#limits 15 | MaxMessageTextLength = 4000 16 | ) 17 | 18 | // RTM represents a managed websocket connection. It also supports 19 | // all the methods of the `Client` type. 20 | // 21 | // Create this element with Client's NewRTM(). 22 | type RTM struct { 23 | idGen IDGenerator 24 | pings map[int]time.Time 25 | 26 | // Connection life-cycle 27 | conn *websocket.Conn 28 | IncomingEvents chan RTMEvent 29 | outgoingMessages chan OutgoingMessage 30 | killChannel chan bool 31 | forcePing chan bool 32 | rawEvents chan json.RawMessage 33 | wasIntentional bool 34 | isConnected bool 35 | 36 | // Client is the main API, embedded 37 | Client 38 | websocketURL string 39 | 40 | // UserDetails upon connection 41 | info *Info 42 | } 43 | 44 | // NewRTM returns a RTM, which provides a fully managed connection to 45 | // Slack's websocket-based Real-Time Messaging protocol. 46 | func newRTM(api *Client) *RTM { 47 | return &RTM{ 48 | Client: *api, 49 | IncomingEvents: make(chan RTMEvent, 50), 50 | outgoingMessages: make(chan OutgoingMessage, 20), 51 | pings: make(map[int]time.Time), 52 | isConnected: false, 53 | wasIntentional: true, 54 | killChannel: make(chan bool), 55 | forcePing: make(chan bool), 56 | rawEvents: make(chan json.RawMessage), 57 | idGen: NewSafeID(1), 58 | } 59 | } 60 | 61 | // Disconnect and wait, blocking until a successful disconnection. 62 | func (rtm *RTM) Disconnect() error { 63 | if !rtm.isConnected { 64 | return errors.New("Invalid call to Disconnect - Slack API is already disconnected") 65 | } 66 | rtm.killChannel <- true 67 | return nil 68 | } 69 | 70 | // Reconnect only makes sense if you've successfully disconnectd with Disconnect(). 71 | func (rtm *RTM) Reconnect() error { 72 | log.Println("RTM::Reconnect not implemented!") 73 | return nil 74 | } 75 | 76 | // GetInfo returns the info structure received when calling 77 | // "startrtm", holding all channels, groups and other metadata needed 78 | // to implement a full chat client. It will be non-nil after a call to 79 | // StartRTM(). 80 | func (rtm *RTM) GetInfo() *Info { 81 | return rtm.info 82 | } 83 | 84 | // SendMessage submits a simple message through the websocket. For 85 | // more complicated messages, use `rtm.PostMessage` with a complete 86 | // struct describing your attachments and all. 87 | func (rtm *RTM) SendMessage(msg *OutgoingMessage) { 88 | if msg == nil { 89 | rtm.Debugln("Error: Attempted to SendMessage(nil)") 90 | return 91 | } 92 | 93 | rtm.outgoingMessages <- *msg 94 | } 95 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_channels.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // ChannelCreatedEvent represents the Channel created event 4 | type ChannelCreatedEvent struct { 5 | Type string `json:"type"` 6 | Channel ChannelCreatedInfo `json:"channel"` 7 | EventTimestamp string `json:"event_ts"` 8 | } 9 | 10 | // ChannelCreatedInfo represents the information associated with the Channel created event 11 | type ChannelCreatedInfo struct { 12 | ID string `json:"id"` 13 | IsChannel bool `json:"is_channel"` 14 | Name string `json:"name"` 15 | Created int `json:"created"` 16 | Creator string `json:"creator"` 17 | } 18 | 19 | // ChannelJoinedEvent represents the Channel joined event 20 | type ChannelJoinedEvent struct { 21 | Type string `json:"type"` 22 | Channel Channel `json:"channel"` 23 | } 24 | 25 | // ChannelInfoEvent represents the Channel info event 26 | type ChannelInfoEvent struct { 27 | // channel_left 28 | // channel_deleted 29 | // channel_archive 30 | // channel_unarchive 31 | Type string `json:"type"` 32 | Channel string `json:"channel"` 33 | User string `json:"user,omitempty"` 34 | Timestamp string `json:"ts,omitempty"` 35 | } 36 | 37 | // ChannelRenameEvent represents the Channel rename event 38 | type ChannelRenameEvent struct { 39 | Type string `json:"type"` 40 | Channel ChannelRenameInfo `json:"channel"` 41 | Timestamp string `json:"event_ts"` 42 | } 43 | 44 | // ChannelRenameInfo represents the information associated with a Channel rename event 45 | type ChannelRenameInfo struct { 46 | ID string `json:"id"` 47 | Name string `json:"name"` 48 | Created string `json:"created"` 49 | } 50 | 51 | // ChannelHistoryChangedEvent represents the Channel history changed event 52 | type ChannelHistoryChangedEvent struct { 53 | Type string `json:"type"` 54 | Latest string `json:"latest"` 55 | Timestamp string `json:"ts"` 56 | EventTimestamp string `json:"event_ts"` 57 | } 58 | 59 | // ChannelMarkedEvent represents the Channel marked event 60 | type ChannelMarkedEvent ChannelInfoEvent 61 | 62 | // ChannelLeftEvent represents the Channel left event 63 | type ChannelLeftEvent ChannelInfoEvent 64 | 65 | // ChannelDeletedEvent represents the Channel deleted event 66 | type ChannelDeletedEvent ChannelInfoEvent 67 | 68 | // ChannelArchiveEvent represents the Channel archive event 69 | type ChannelArchiveEvent ChannelInfoEvent 70 | 71 | // ChannelUnarchiveEvent represents the Channel unarchive event 72 | type ChannelUnarchiveEvent ChannelInfoEvent 73 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_dm.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // IMCreatedEvent represents the IM created event 4 | type IMCreatedEvent struct { 5 | Type string `json:"type"` 6 | User string `json:"user"` 7 | Channel ChannelCreatedInfo `json:"channel"` 8 | } 9 | 10 | // IMHistoryChangedEvent represents the IM history changed event 11 | type IMHistoryChangedEvent ChannelHistoryChangedEvent 12 | 13 | // IMOpenEvent represents the IM open event 14 | type IMOpenEvent ChannelInfoEvent 15 | 16 | // IMCloseEvent represents the IM close event 17 | type IMCloseEvent ChannelInfoEvent 18 | 19 | // IMMarkedEvent represents the IM marked event 20 | type IMMarkedEvent ChannelInfoEvent 21 | 22 | // IMMarkedHistoryChanged represents the IM marked history changed event 23 | type IMMarkedHistoryChanged ChannelInfoEvent 24 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_dnd.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // DNDUpdatedEvent represents the update event for Do Not Disturb 4 | type DNDUpdatedEvent struct { 5 | Type string `json:"type"` 6 | User string `json:"user"` 7 | Status DNDStatus `json:"dnd_status"` 8 | } 9 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_files.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // FileActionEvent represents the File action event 4 | type fileActionEvent struct { 5 | Type string `json:"type"` 6 | EventTimestamp string `json:"event_ts"` 7 | File File `json:"file"` 8 | // FileID is used for FileDeletedEvent 9 | FileID string `json:"file_id,omitempty"` 10 | } 11 | 12 | // FileCreatedEvent represents the File created event 13 | type FileCreatedEvent fileActionEvent 14 | 15 | // FileSharedEvent represents the File shared event 16 | type FileSharedEvent fileActionEvent 17 | 18 | // FilePublicEvent represents the File public event 19 | type FilePublicEvent fileActionEvent 20 | 21 | // FileUnsharedEvent represents the File unshared event 22 | type FileUnsharedEvent fileActionEvent 23 | 24 | // FileChangeEvent represents the File change event 25 | type FileChangeEvent fileActionEvent 26 | 27 | // FileDeletedEvent represents the File deleted event 28 | type FileDeletedEvent fileActionEvent 29 | 30 | // FilePrivateEvent represents the File private event 31 | type FilePrivateEvent fileActionEvent 32 | 33 | // FileCommentAddedEvent represents the File comment added event 34 | type FileCommentAddedEvent struct { 35 | fileActionEvent 36 | Comment Comment `json:"comment"` 37 | } 38 | 39 | // FileCommentEditedEvent represents the File comment edited event 40 | type FileCommentEditedEvent struct { 41 | fileActionEvent 42 | Comment Comment `json:"comment"` 43 | } 44 | 45 | // FileCommentDeletedEvent represents the File comment deleted event 46 | type FileCommentDeletedEvent struct { 47 | fileActionEvent 48 | Comment string `json:"comment"` 49 | } 50 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_groups.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // GroupCreatedEvent represents the Group created event 4 | type GroupCreatedEvent struct { 5 | Type string `json:"type"` 6 | User string `json:"user"` 7 | Channel ChannelCreatedInfo `json:"channel"` 8 | } 9 | 10 | // XXX: Should we really do this? event.Group is probably nicer than event.Channel 11 | // even though the api returns "channel" 12 | 13 | // GroupMarkedEvent represents the Group marked event 14 | type GroupMarkedEvent ChannelInfoEvent 15 | 16 | // GroupOpenEvent represents the Group open event 17 | type GroupOpenEvent ChannelInfoEvent 18 | 19 | // GroupCloseEvent represents the Group close event 20 | type GroupCloseEvent ChannelInfoEvent 21 | 22 | // GroupArchiveEvent represents the Group archive event 23 | type GroupArchiveEvent ChannelInfoEvent 24 | 25 | // GroupUnarchiveEvent represents the Group unarchive event 26 | type GroupUnarchiveEvent ChannelInfoEvent 27 | 28 | // GroupLeftEvent represents the Group left event 29 | type GroupLeftEvent ChannelInfoEvent 30 | 31 | // GroupJoinedEvent represents the Group joined event 32 | type GroupJoinedEvent ChannelJoinedEvent 33 | 34 | // GroupRenameEvent represents the Group rename event 35 | type GroupRenameEvent struct { 36 | Type string `json:"type"` 37 | Group GroupRenameInfo `json:"channel"` 38 | Timestamp string `json:"ts"` 39 | } 40 | 41 | // GroupRenameInfo represents the group info related to the renamed group 42 | type GroupRenameInfo struct { 43 | ID string `json:"id"` 44 | Name string `json:"name"` 45 | Created string `json:"created"` 46 | } 47 | 48 | // GroupHistoryChangedEvent represents the Group history changed event 49 | type GroupHistoryChangedEvent ChannelHistoryChangedEvent 50 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_internals.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | /** 9 | * Internal events, created by this lib and not mapped to Slack APIs. 10 | */ 11 | 12 | // ConnectedEvent is used for when we connect to Slack 13 | type ConnectedEvent struct { 14 | ConnectionCount int // 1 = first time, 2 = second time 15 | Info *Info 16 | } 17 | 18 | // ConnectionErrorEvent contains information about a connection error 19 | type ConnectionErrorEvent struct { 20 | Attempt int 21 | ErrorObj error 22 | } 23 | 24 | func (c *ConnectionErrorEvent) Error() string { 25 | return c.ErrorObj.Error() 26 | } 27 | 28 | // ConnectingEvent contains information about our connection attempt 29 | type ConnectingEvent struct { 30 | Attempt int // 1 = first attempt, 2 = second attempt 31 | ConnectionCount int 32 | } 33 | 34 | // DisconnectedEvent contains information about how we disconnected 35 | type DisconnectedEvent struct { 36 | Intentional bool 37 | } 38 | 39 | // LatencyReport contains information about connection latency 40 | type LatencyReport struct { 41 | Value time.Duration 42 | } 43 | 44 | // InvalidAuthEvent is used in case we can't even authenticate with the API 45 | type InvalidAuthEvent struct{} 46 | 47 | // UnmarshallingErrorEvent is used when there are issues deconstructing a response 48 | type UnmarshallingErrorEvent struct { 49 | ErrorObj error 50 | } 51 | 52 | func (u UnmarshallingErrorEvent) Error() string { 53 | return u.ErrorObj.Error() 54 | } 55 | 56 | // MessageTooLongEvent is used when sending a message that is too long 57 | type MessageTooLongEvent struct { 58 | Message OutgoingMessage 59 | MaxLength int 60 | } 61 | 62 | func (m *MessageTooLongEvent) Error() string { 63 | return fmt.Sprintf("Message too long (max %d characters)", m.MaxLength) 64 | } 65 | 66 | // OutgoingErrorEvent contains information in case there were errors sending messages 67 | type OutgoingErrorEvent struct { 68 | Message OutgoingMessage 69 | ErrorObj error 70 | } 71 | 72 | func (o OutgoingErrorEvent) Error() string { 73 | return o.ErrorObj.Error() 74 | } 75 | 76 | // IncomingEventError contains information about an unexpected error receiving a websocket event 77 | type IncomingEventError struct { 78 | ErrorObj error 79 | } 80 | 81 | func (i *IncomingEventError) Error() string { 82 | return i.ErrorObj.Error() 83 | } 84 | 85 | // AckErrorEvent i 86 | type AckErrorEvent struct { 87 | ErrorObj error 88 | } 89 | 90 | func (a *AckErrorEvent) Error() string { 91 | return a.ErrorObj.Error() 92 | } 93 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_managed_conn.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "time" 9 | 10 | "golang.org/x/net/websocket" 11 | ) 12 | 13 | // ManageConnection can be called on a Slack RTM instance returned by the 14 | // NewRTM method. It will connect to the slack RTM API and handle all incoming 15 | // and outgoing events. If a connection fails then it will attempt to reconnect 16 | // and will notify any listeners through an error event on the IncomingEvents 17 | // channel. 18 | // 19 | // If the connection ends and the disconnect was unintentional then this will 20 | // attempt to reconnect. 21 | // 22 | // This should only be called once per slack API! Otherwise expect undefined 23 | // behavior. 24 | // 25 | // The defined error events are located in websocket_internals.go. 26 | func (rtm *RTM) ManageConnection() { 27 | var connectionCount int 28 | for { 29 | connectionCount++ 30 | // start trying to connect 31 | // the returned err is already passed onto the IncomingEvents channel 32 | info, conn, err := rtm.connect(connectionCount) 33 | // if err != nil then the connection is sucessful - otherwise it is 34 | // fatal 35 | if err != nil { 36 | return 37 | } 38 | rtm.info = info 39 | rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{ 40 | ConnectionCount: connectionCount, 41 | Info: info, 42 | }} 43 | 44 | rtm.conn = conn 45 | rtm.isConnected = true 46 | 47 | keepRunning := make(chan bool) 48 | // we're now connected (or have failed fatally) so we can set up 49 | // listeners 50 | go rtm.handleIncomingEvents(keepRunning) 51 | 52 | // this should be a blocking call until the connection has ended 53 | rtm.handleEvents(keepRunning, 30*time.Second) 54 | 55 | // after being disconnected we need to check if it was intentional 56 | // if not then we should try to reconnect 57 | if rtm.wasIntentional { 58 | return 59 | } 60 | // else continue and run the loop again to connect 61 | } 62 | } 63 | 64 | // connect attempts to connect to the slack websocket API. It handles any 65 | // errors that occur while connecting and will return once a connection 66 | // has been successfully opened. 67 | func (rtm *RTM) connect(connectionCount int) (*Info, *websocket.Conn, error) { 68 | // used to provide exponential backoff wait time with jitter before trying 69 | // to connect to slack again 70 | boff := &backoff{ 71 | Min: 100 * time.Millisecond, 72 | Max: 5 * time.Minute, 73 | Factor: 2, 74 | Jitter: true, 75 | } 76 | 77 | for { 78 | // send connecting event 79 | rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{ 80 | Attempt: boff.attempts + 1, 81 | ConnectionCount: connectionCount, 82 | }} 83 | // attempt to start the connection 84 | info, conn, err := rtm.startRTMAndDial() 85 | if err == nil { 86 | return info, conn, nil 87 | } 88 | // check for fatal errors - currently only invalid_auth 89 | if sErr, ok := err.(*WebError); ok && (sErr.Error() == "invalid_auth" || sErr.Error() == "account_inactive") { 90 | rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} 91 | return nil, nil, sErr 92 | } 93 | // any other errors are treated as recoverable and we try again after 94 | // sending the event along the IncomingEvents channel 95 | rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{ 96 | Attempt: boff.attempts, 97 | ErrorObj: err, 98 | }} 99 | // get time we should wait before attempting to connect again 100 | dur := boff.Duration() 101 | rtm.Debugf("reconnection %d failed: %s", boff.attempts+1, err) 102 | rtm.Debugln(" -> reconnecting in", dur) 103 | time.Sleep(dur) 104 | } 105 | } 106 | 107 | // startRTMAndDial attemps to connect to the slack websocket. It returns the 108 | // full information returned by the "rtm.start" method on the slack API. 109 | func (rtm *RTM) startRTMAndDial() (*Info, *websocket.Conn, error) { 110 | info, url, err := rtm.StartRTM() 111 | if err != nil { 112 | return nil, nil, err 113 | } 114 | 115 | conn, err := websocketProxyDial(url, "http://api.slack.com") 116 | if err != nil { 117 | return nil, nil, err 118 | } 119 | return info, conn, err 120 | } 121 | 122 | // killConnection stops the websocket connection and signals to all goroutines 123 | // that they should cease listening to the connection for events. 124 | // 125 | // This should not be called directly! Instead a boolean value (true for 126 | // intentional, false otherwise) should be sent to the killChannel on the RTM. 127 | func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error { 128 | rtm.Debugln("killing connection") 129 | if rtm.isConnected { 130 | close(keepRunning) 131 | } 132 | rtm.isConnected = false 133 | rtm.wasIntentional = intentional 134 | err := rtm.conn.Close() 135 | rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}} 136 | return err 137 | } 138 | 139 | // handleEvents is a blocking function that handles all events. This sends 140 | // pings when asked to (on rtm.forcePing) and upon every given elapsed 141 | // interval. This also sends outgoing messages that are received from the RTM's 142 | // outgoingMessages channel. This also handles incoming raw events from the RTM 143 | // rawEvents channel. 144 | func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { 145 | ticker := time.NewTicker(interval) 146 | defer ticker.Stop() 147 | for { 148 | select { 149 | // catch "stop" signal on channel close 150 | case intentional := <-rtm.killChannel: 151 | _ = rtm.killConnection(keepRunning, intentional) 152 | return 153 | // send pings on ticker interval 154 | case <-ticker.C: 155 | err := rtm.ping() 156 | if err != nil { 157 | _ = rtm.killConnection(keepRunning, false) 158 | return 159 | } 160 | case <-rtm.forcePing: 161 | err := rtm.ping() 162 | if err != nil { 163 | _ = rtm.killConnection(keepRunning, false) 164 | return 165 | } 166 | // listen for messages that need to be sent 167 | case msg := <-rtm.outgoingMessages: 168 | rtm.sendOutgoingMessage(msg) 169 | // listen for incoming messages that need to be parsed 170 | case rawEvent := <-rtm.rawEvents: 171 | rtm.handleRawEvent(rawEvent) 172 | } 173 | } 174 | } 175 | 176 | // handleIncomingEvents monitors the RTM's opened websocket for any incoming 177 | // events. It pushes the raw events onto the RTM channel rawEvents. 178 | // 179 | // This will stop executing once the RTM's keepRunning channel has been closed 180 | // or has anything sent to it. 181 | func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) { 182 | for { 183 | // non-blocking listen to see if channel is closed 184 | select { 185 | // catch "stop" signal on channel close 186 | case <-keepRunning: 187 | return 188 | default: 189 | rtm.receiveIncomingEvent() 190 | } 191 | } 192 | } 193 | 194 | // sendOutgoingMessage sends the given OutgoingMessage to the slack websocket. 195 | // 196 | // It does not currently detect if a outgoing message fails due to a disconnect 197 | // and instead lets a future failed 'PING' detect the failed connection. 198 | func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { 199 | rtm.Debugln("Sending message:", msg) 200 | if len(msg.Text) > MaxMessageTextLength { 201 | rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{ 202 | Message: msg, 203 | MaxLength: MaxMessageTextLength, 204 | }} 205 | return 206 | } 207 | fmt.Println("outgoing msg", msg) 208 | err := websocket.JSON.Send(rtm.conn, msg) 209 | if err != nil { 210 | rtm.IncomingEvents <- RTMEvent{"outgoing_error", &OutgoingErrorEvent{ 211 | Message: msg, 212 | ErrorObj: err, 213 | }} 214 | fmt.Println("error here", err) 215 | // TODO force ping? 216 | } 217 | } 218 | 219 | // ping sends a 'PING' message to the RTM's websocket. If the 'PING' message 220 | // fails to send then this returns an error signifying that the connection 221 | // should be considered disconnected. 222 | // 223 | // This does not handle incoming 'PONG' responses but does store the time of 224 | // each successful 'PING' send so latency can be detected upon a 'PONG' 225 | // response. 226 | func (rtm *RTM) ping() error { 227 | id := rtm.idGen.Next() 228 | rtm.Debugln("Sending PING ", id) 229 | rtm.pings[id] = time.Now() 230 | 231 | msg := &Ping{ID: id, Type: "ping"} 232 | err := websocket.JSON.Send(rtm.conn, msg) 233 | if err != nil { 234 | rtm.Debugf("RTM Error sending 'PING %d': %s", id, err.Error()) 235 | return err 236 | } 237 | return nil 238 | } 239 | 240 | // receiveIncomingEvent attempts to receive an event from the RTM's websocket. 241 | // This will block until a frame is available from the websocket. 242 | func (rtm *RTM) receiveIncomingEvent() { 243 | event := json.RawMessage{} 244 | err := websocket.JSON.Receive(rtm.conn, &event) 245 | if err == io.EOF { 246 | // EOF's don't seem to signify a failed connection so instead we ignore 247 | // them here and detect a failed connection upon attempting to send a 248 | // 'PING' message 249 | 250 | // trigger a 'PING' to detect pontential websocket disconnect 251 | rtm.forcePing <- true 252 | return 253 | } else if err != nil { 254 | rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{ 255 | ErrorObj: err, 256 | }} 257 | // force a ping here too? 258 | return 259 | } else if len(event) == 0 { 260 | rtm.Debugln("Received empty event") 261 | return 262 | } 263 | rtm.Debugln("Incoming Event:", string(event[:])) 264 | rtm.rawEvents <- event 265 | } 266 | 267 | // handleRawEvent takes a raw JSON message received from the slack websocket 268 | // and handles the encoded event. 269 | func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) { 270 | event := &Event{} 271 | err := json.Unmarshal(rawEvent, event) 272 | if err != nil { 273 | rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} 274 | return 275 | } 276 | switch event.Type { 277 | case "": 278 | rtm.handleAck(rawEvent) 279 | case "hello": 280 | rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}} 281 | case "pong": 282 | rtm.handlePong(rawEvent) 283 | default: 284 | rtm.handleEvent(event.Type, rawEvent) 285 | } 286 | } 287 | 288 | // handleAck handles an incoming 'ACK' message. 289 | func (rtm *RTM) handleAck(event json.RawMessage) { 290 | ack := &AckMessage{} 291 | if err := json.Unmarshal(event, ack); err != nil { 292 | rtm.Debugln("RTM Error unmarshalling 'ack' event:", err) 293 | rtm.Debugln(" -> Erroneous 'ack' event:", string(event)) 294 | return 295 | } 296 | if ack.Ok { 297 | rtm.IncomingEvents <- RTMEvent{"ack", ack} 298 | } else { 299 | rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}} 300 | } 301 | } 302 | 303 | // handlePong handles an incoming 'PONG' message which should be in response to 304 | // a previously sent 'PING' message. This is then used to compute the 305 | // connection's latency. 306 | func (rtm *RTM) handlePong(event json.RawMessage) { 307 | pong := &Pong{} 308 | if err := json.Unmarshal(event, pong); err != nil { 309 | rtm.Debugln("RTM Error unmarshalling 'pong' event:", err) 310 | rtm.Debugln(" -> Erroneous 'ping' event:", string(event)) 311 | return 312 | } 313 | if pingTime, exists := rtm.pings[pong.ReplyTo]; exists { 314 | latency := time.Since(pingTime) 315 | rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}} 316 | delete(rtm.pings, pong.ReplyTo) 317 | } else { 318 | rtm.Debugln("RTM Error - unmatched 'pong' event:", string(event)) 319 | } 320 | } 321 | 322 | // handleEvent is the "default" response to an event that does not have a 323 | // special case. It matches the command's name to a mapping of defined events 324 | // and then sends the corresponding event struct to the IncomingEvents channel. 325 | // If the event type is not found or the event cannot be unmarshalled into the 326 | // correct struct then this sends an UnmarshallingErrorEvent to the 327 | // IncomingEvents channel. 328 | func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { 329 | v, exists := eventMapping[typeStr] 330 | if !exists { 331 | rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event)) 332 | err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event)) 333 | rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} 334 | return 335 | } 336 | t := reflect.TypeOf(v) 337 | recvEvent := reflect.New(t).Interface() 338 | err := json.Unmarshal(event, recvEvent) 339 | if err != nil { 340 | rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event)) 341 | err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event)) 342 | rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} 343 | return 344 | } 345 | rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent} 346 | } 347 | 348 | // eventMapping holds a mapping of event names to their corresponding struct 349 | // implementations. The structs should be instances of the unmarshalling 350 | // target for the matching event type. 351 | var eventMapping = map[string]interface{}{ 352 | "message": MessageEvent{}, 353 | "presence_change": PresenceChangeEvent{}, 354 | "user_typing": UserTypingEvent{}, 355 | 356 | "channel_marked": ChannelMarkedEvent{}, 357 | "channel_created": ChannelCreatedEvent{}, 358 | "channel_joined": ChannelJoinedEvent{}, 359 | "channel_left": ChannelLeftEvent{}, 360 | "channel_deleted": ChannelDeletedEvent{}, 361 | "channel_rename": ChannelRenameEvent{}, 362 | "channel_archive": ChannelArchiveEvent{}, 363 | "channel_unarchive": ChannelUnarchiveEvent{}, 364 | "channel_history_changed": ChannelHistoryChangedEvent{}, 365 | 366 | "dnd_updated": DNDUpdatedEvent{}, 367 | "dnd_updated_user": DNDUpdatedEvent{}, 368 | 369 | "im_created": IMCreatedEvent{}, 370 | "im_open": IMOpenEvent{}, 371 | "im_close": IMCloseEvent{}, 372 | "im_marked": IMMarkedEvent{}, 373 | "im_history_changed": IMHistoryChangedEvent{}, 374 | 375 | "group_marked": GroupMarkedEvent{}, 376 | "group_open": GroupOpenEvent{}, 377 | "group_joined": GroupJoinedEvent{}, 378 | "group_left": GroupLeftEvent{}, 379 | "group_close": GroupCloseEvent{}, 380 | "group_rename": GroupRenameEvent{}, 381 | "group_archive": GroupArchiveEvent{}, 382 | "group_unarchive": GroupUnarchiveEvent{}, 383 | "group_history_changed": GroupHistoryChangedEvent{}, 384 | 385 | "file_created": FileCreatedEvent{}, 386 | "file_shared": FileSharedEvent{}, 387 | "file_unshared": FileUnsharedEvent{}, 388 | "file_public": FilePublicEvent{}, 389 | "file_private": FilePrivateEvent{}, 390 | "file_change": FileChangeEvent{}, 391 | "file_deleted": FileDeletedEvent{}, 392 | "file_comment_added": FileCommentAddedEvent{}, 393 | "file_comment_edited": FileCommentEditedEvent{}, 394 | "file_comment_deleted": FileCommentDeletedEvent{}, 395 | 396 | "pin_added": PinAddedEvent{}, 397 | "pin_removed": PinRemovedEvent{}, 398 | 399 | "star_added": StarAddedEvent{}, 400 | "star_removed": StarRemovedEvent{}, 401 | 402 | "reaction_added": ReactionAddedEvent{}, 403 | "reaction_removed": ReactionRemovedEvent{}, 404 | 405 | "pref_change": PrefChangeEvent{}, 406 | 407 | "team_join": TeamJoinEvent{}, 408 | "team_rename": TeamRenameEvent{}, 409 | "team_pref_change": TeamPrefChangeEvent{}, 410 | "team_domain_change": TeamDomainChangeEvent{}, 411 | "team_migration_started": TeamMigrationStartedEvent{}, 412 | 413 | "manual_presence_change": ManualPresenceChangeEvent{}, 414 | 415 | "user_change": UserChangeEvent{}, 416 | 417 | "emoji_changed": EmojiChangedEvent{}, 418 | 419 | "commands_changed": CommandsChangedEvent{}, 420 | 421 | "email_domain_changed": EmailDomainChangedEvent{}, 422 | 423 | "bot_added": BotAddedEvent{}, 424 | "bot_changed": BotChangedEvent{}, 425 | 426 | "accounts_changed": AccountsChangedEvent{}, 427 | 428 | "reconnect_url": ReconnectUrlEvent{}, 429 | } 430 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_misc.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // AckMessage is used for messages received in reply to other messages 9 | type AckMessage struct { 10 | ReplyTo int `json:"reply_to"` 11 | Timestamp string `json:"ts"` 12 | Text string `json:"text"` 13 | RTMResponse 14 | } 15 | 16 | // RTMResponse encapsulates response details as returned by the Slack API 17 | type RTMResponse struct { 18 | Ok bool `json:"ok"` 19 | Error *RTMError `json:"error"` 20 | } 21 | 22 | // RTMError encapsulates error information as returned by the Slack API 23 | type RTMError struct { 24 | Code int 25 | Msg string 26 | } 27 | 28 | func (s RTMError) Error() string { 29 | return fmt.Sprintf("Code %d - %s", s.Code, s.Msg) 30 | } 31 | 32 | // MessageEvent represents a Slack Message (used as the event type for an incoming message) 33 | type MessageEvent Message 34 | 35 | // RTMEvent is the main wrapper. You will find all the other messages attached 36 | type RTMEvent struct { 37 | Type string 38 | Data interface{} 39 | } 40 | 41 | // HelloEvent represents the hello event 42 | type HelloEvent struct{} 43 | 44 | // PresenceChangeEvent represents the presence change event 45 | type PresenceChangeEvent struct { 46 | Type string `json:"type"` 47 | Presence string `json:"presence"` 48 | User string `json:"user"` 49 | } 50 | 51 | // UserTypingEvent represents the user typing event 52 | type UserTypingEvent struct { 53 | Type string `json:"type"` 54 | User string `json:"user"` 55 | Channel string `json:"channel"` 56 | } 57 | 58 | // PrefChangeEvent represents a user preferences change event 59 | type PrefChangeEvent struct { 60 | Type string `json:"type"` 61 | Name string `json:"name"` 62 | Value json.RawMessage `json:"value"` 63 | } 64 | 65 | // ManualPresenceChangeEvent represents the manual presence change event 66 | type ManualPresenceChangeEvent struct { 67 | Type string `json:"type"` 68 | Presence string `json:"presence"` 69 | } 70 | 71 | // UserChangeEvent represents the user change event 72 | type UserChangeEvent struct { 73 | Type string `json:"type"` 74 | User User `json:"user"` 75 | } 76 | 77 | // EmojiChangedEvent represents the emoji changed event 78 | type EmojiChangedEvent struct { 79 | Type string `json:"type"` 80 | EventTimestamp string `json:"event_ts"` 81 | } 82 | 83 | // CommandsChangedEvent represents the commands changed event 84 | type CommandsChangedEvent struct { 85 | Type string `json:"type"` 86 | EventTimestamp string `json:"event_ts"` 87 | } 88 | 89 | // EmailDomainChangedEvent represents the email domain changed event 90 | type EmailDomainChangedEvent struct { 91 | Type string `json:"type"` 92 | EventTimestamp string `json:"event_ts"` 93 | EmailDomain string `json:"email_domain"` 94 | } 95 | 96 | // BotAddedEvent represents the bot added event 97 | type BotAddedEvent struct { 98 | Type string `json:"type"` 99 | Bot Bot `json:"bot"` 100 | } 101 | 102 | // BotChangedEvent represents the bot changed event 103 | type BotChangedEvent struct { 104 | Type string `json:"type"` 105 | Bot Bot `json:"bot"` 106 | } 107 | 108 | // AccountsChangedEvent represents the accounts changed event 109 | type AccountsChangedEvent struct { 110 | Type string `json:"type"` 111 | } 112 | 113 | // ReconnectUrlEvent represents the receiving reconnect url event 114 | type ReconnectUrlEvent struct { 115 | Type string `json:"type"` 116 | URL string `json:"url"` 117 | } 118 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_pins.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type pinEvent struct { 4 | Type string `json:"type"` 5 | User string `json:"user"` 6 | Item Item `json:"item"` 7 | Channel string `json:"channel_id"` 8 | EventTimestamp string `json:"event_ts"` 9 | HasPins bool `json:"has_pins,omitempty"` 10 | } 11 | 12 | // PinAddedEvent represents the Pin added event 13 | type PinAddedEvent pinEvent 14 | 15 | // PinRemovedEvent represents the Pin removed event 16 | type PinRemovedEvent pinEvent 17 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_proxy.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "net" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/url" 10 | "os" 11 | "strings" 12 | 13 | "golang.org/x/net/websocket" 14 | ) 15 | 16 | // Taken and reworked from: https://gist.github.com/madmo/8548738 17 | func websocketHTTPConnect(proxy, urlString string) (net.Conn, error) { 18 | p, err := net.Dial("tcp", proxy) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | turl, err := url.Parse(urlString) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | req := http.Request{ 29 | Method: "CONNECT", 30 | URL: &url.URL{}, 31 | Host: turl.Host, 32 | } 33 | 34 | cc := httputil.NewProxyClientConn(p, nil) 35 | cc.Do(&req) 36 | if err != nil && err != httputil.ErrPersistEOF { 37 | return nil, err 38 | } 39 | 40 | rwc, _ := cc.Hijack() 41 | 42 | return rwc, nil 43 | } 44 | 45 | func websocketProxyDial(urlString, origin string) (ws *websocket.Conn, err error) { 46 | if os.Getenv("HTTP_PROXY") == "" { 47 | return websocket.Dial(urlString, "", origin) 48 | } 49 | 50 | purl, err := url.Parse(os.Getenv("HTTP_PROXY")) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | config, err := websocket.NewConfig(urlString, origin) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | client, err := websocketHTTPConnect(purl.Host, urlString) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | switch config.Location.Scheme { 66 | case "ws": 67 | case "wss": 68 | tlsClient := tls.Client(client, &tls.Config{ 69 | ServerName: strings.Split(config.Location.Host, ":")[0], 70 | }) 71 | err := tlsClient.Handshake() 72 | if err != nil { 73 | tlsClient.Close() 74 | return nil, err 75 | } 76 | client = tlsClient 77 | 78 | default: 79 | return nil, errors.New("invalid websocket schema") 80 | } 81 | 82 | return websocket.NewClient(config, client) 83 | } 84 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_reactions.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // reactionItem is a lighter-weight item than is returned by the reactions list. 4 | type reactionItem struct { 5 | Type string `json:"type"` 6 | Channel string `json:"channel,omitempty"` 7 | File string `json:"file,omitempty"` 8 | FileComment string `json:"file_comment,omitempty"` 9 | Timestamp string `json:"ts,omitempty"` 10 | } 11 | 12 | type reactionEvent struct { 13 | Type string `json:"type"` 14 | User string `json:"user"` 15 | ItemUser string `json:"item_user"` 16 | Item reactionItem `json:"item"` 17 | Reaction string `json:"reaction"` 18 | EventTimestamp string `json:"event_ts"` 19 | } 20 | 21 | // ReactionAddedEvent represents the Reaction added event 22 | type ReactionAddedEvent reactionEvent 23 | 24 | // ReactionRemovedEvent represents the Reaction removed event 25 | type ReactionRemovedEvent reactionEvent 26 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_stars.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | type starEvent struct { 4 | Type string `json:"type"` 5 | User string `json:"user"` 6 | Item StarredItem `json:"item"` 7 | EventTimestamp string `json:"event_ts"` 8 | } 9 | 10 | // StarAddedEvent represents the Star added event 11 | type StarAddedEvent starEvent 12 | 13 | // StarRemovedEvent represents the Star removed event 14 | type StarRemovedEvent starEvent 15 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_teams.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // TeamJoinEvent represents the Team join event 4 | type TeamJoinEvent struct { 5 | Type string `json:"type"` 6 | User User `json:"user"` 7 | } 8 | 9 | // TeamRenameEvent represents the Team rename event 10 | type TeamRenameEvent struct { 11 | Type string `json:"type"` 12 | Name string `json:"name,omitempty"` 13 | EventTimestamp string `json:"event_ts,omitempty"` 14 | } 15 | 16 | // TeamPrefChangeEvent represents the Team preference change event 17 | type TeamPrefChangeEvent struct { 18 | Type string `json:"type"` 19 | Name string `json:"name,omitempty"` 20 | Value []string `json:"value,omitempty"` 21 | } 22 | 23 | // TeamDomainChangeEvent represents the Team domain change event 24 | type TeamDomainChangeEvent struct { 25 | Type string `json:"type"` 26 | URL string `json:"url"` 27 | Domain string `json:"domain"` 28 | } 29 | 30 | // TeamMigrationStartedEvent represents the Team migration started event 31 | type TeamMigrationStartedEvent struct { 32 | Type string `json:"type"` 33 | } 34 | -------------------------------------------------------------------------------- /vendor/github.com/nlopes/slack/websocket_utils.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | ) 7 | 8 | var portMapping = map[string]string{"ws": "80", "wss": "443"} 9 | 10 | func websocketizeURLPort(orig string) (string, error) { 11 | urlObj, err := url.ParseRequestURI(orig) 12 | if err != nil { 13 | return "", err 14 | } 15 | _, _, err = net.SplitHostPort(urlObj.Host) 16 | if err != nil { 17 | return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil 18 | } 19 | return orig, nil 20 | } 21 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Go project. 5 | 6 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this section) 8 | patent license to make, have made, use, offer to sell, sell, import, 9 | transfer and otherwise run, modify and propagate the contents of this 10 | implementation of Go, where such license applies only to those patent 11 | claims, both currently owned or controlled by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by this 13 | implementation of Go. This grant does not include claims that would be 14 | infringed only as a consequence of further modification of this 15 | implementation. If you or your agent or exclusive licensee institute or 16 | order or agree to the institution of patent litigation against any 17 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 18 | that this implementation of Go or any code incorporated within this 19 | implementation of Go constitutes direct or contributory patent 20 | infringement, or inducement of patent infringement, then any patent 21 | rights granted to you under this License for this implementation of Go 22 | shall terminate as of the date such litigation is filed. 23 | -------------------------------------------------------------------------------- /vendor/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 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/websocket/hybi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package websocket 6 | 7 | // This file implements a protocol of hybi draft. 8 | // http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 9 | 10 | import ( 11 | "bufio" 12 | "bytes" 13 | "crypto/rand" 14 | "crypto/sha1" 15 | "encoding/base64" 16 | "encoding/binary" 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "net/http" 21 | "net/url" 22 | "strings" 23 | ) 24 | 25 | const ( 26 | websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 27 | 28 | closeStatusNormal = 1000 29 | closeStatusGoingAway = 1001 30 | closeStatusProtocolError = 1002 31 | closeStatusUnsupportedData = 1003 32 | closeStatusFrameTooLarge = 1004 33 | closeStatusNoStatusRcvd = 1005 34 | closeStatusAbnormalClosure = 1006 35 | closeStatusBadMessageData = 1007 36 | closeStatusPolicyViolation = 1008 37 | closeStatusTooBigData = 1009 38 | closeStatusExtensionMismatch = 1010 39 | 40 | maxControlFramePayloadLength = 125 41 | ) 42 | 43 | var ( 44 | ErrBadMaskingKey = &ProtocolError{"bad masking key"} 45 | ErrBadPongMessage = &ProtocolError{"bad pong message"} 46 | ErrBadClosingStatus = &ProtocolError{"bad closing status"} 47 | ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"} 48 | ErrNotImplemented = &ProtocolError{"not implemented"} 49 | 50 | handshakeHeader = map[string]bool{ 51 | "Host": true, 52 | "Upgrade": true, 53 | "Connection": true, 54 | "Sec-Websocket-Key": true, 55 | "Sec-Websocket-Origin": true, 56 | "Sec-Websocket-Version": true, 57 | "Sec-Websocket-Protocol": true, 58 | "Sec-Websocket-Accept": true, 59 | } 60 | ) 61 | 62 | // A hybiFrameHeader is a frame header as defined in hybi draft. 63 | type hybiFrameHeader struct { 64 | Fin bool 65 | Rsv [3]bool 66 | OpCode byte 67 | Length int64 68 | MaskingKey []byte 69 | 70 | data *bytes.Buffer 71 | } 72 | 73 | // A hybiFrameReader is a reader for hybi frame. 74 | type hybiFrameReader struct { 75 | reader io.Reader 76 | 77 | header hybiFrameHeader 78 | pos int64 79 | length int 80 | } 81 | 82 | func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) { 83 | n, err = frame.reader.Read(msg) 84 | if err != nil { 85 | return 0, err 86 | } 87 | if frame.header.MaskingKey != nil { 88 | for i := 0; i < n; i++ { 89 | msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4] 90 | frame.pos++ 91 | } 92 | } 93 | return n, err 94 | } 95 | 96 | func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode } 97 | 98 | func (frame *hybiFrameReader) HeaderReader() io.Reader { 99 | if frame.header.data == nil { 100 | return nil 101 | } 102 | if frame.header.data.Len() == 0 { 103 | return nil 104 | } 105 | return frame.header.data 106 | } 107 | 108 | func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil } 109 | 110 | func (frame *hybiFrameReader) Len() (n int) { return frame.length } 111 | 112 | // A hybiFrameReaderFactory creates new frame reader based on its frame type. 113 | type hybiFrameReaderFactory struct { 114 | *bufio.Reader 115 | } 116 | 117 | // NewFrameReader reads a frame header from the connection, and creates new reader for the frame. 118 | // See Section 5.2 Base Framing protocol for detail. 119 | // http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2 120 | func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) { 121 | hybiFrame := new(hybiFrameReader) 122 | frame = hybiFrame 123 | var header []byte 124 | var b byte 125 | // First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits) 126 | b, err = buf.ReadByte() 127 | if err != nil { 128 | return 129 | } 130 | header = append(header, b) 131 | hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0 132 | for i := 0; i < 3; i++ { 133 | j := uint(6 - i) 134 | hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0 135 | } 136 | hybiFrame.header.OpCode = header[0] & 0x0f 137 | 138 | // Second byte. Mask/Payload len(7bits) 139 | b, err = buf.ReadByte() 140 | if err != nil { 141 | return 142 | } 143 | header = append(header, b) 144 | mask := (b & 0x80) != 0 145 | b &= 0x7f 146 | lengthFields := 0 147 | switch { 148 | case b <= 125: // Payload length 7bits. 149 | hybiFrame.header.Length = int64(b) 150 | case b == 126: // Payload length 7+16bits 151 | lengthFields = 2 152 | case b == 127: // Payload length 7+64bits 153 | lengthFields = 8 154 | } 155 | for i := 0; i < lengthFields; i++ { 156 | b, err = buf.ReadByte() 157 | if err != nil { 158 | return 159 | } 160 | if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits 161 | b &= 0x7f 162 | } 163 | header = append(header, b) 164 | hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b) 165 | } 166 | if mask { 167 | // Masking key. 4 bytes. 168 | for i := 0; i < 4; i++ { 169 | b, err = buf.ReadByte() 170 | if err != nil { 171 | return 172 | } 173 | header = append(header, b) 174 | hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b) 175 | } 176 | } 177 | hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length) 178 | hybiFrame.header.data = bytes.NewBuffer(header) 179 | hybiFrame.length = len(header) + int(hybiFrame.header.Length) 180 | return 181 | } 182 | 183 | // A HybiFrameWriter is a writer for hybi frame. 184 | type hybiFrameWriter struct { 185 | writer *bufio.Writer 186 | 187 | header *hybiFrameHeader 188 | } 189 | 190 | func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) { 191 | var header []byte 192 | var b byte 193 | if frame.header.Fin { 194 | b |= 0x80 195 | } 196 | for i := 0; i < 3; i++ { 197 | if frame.header.Rsv[i] { 198 | j := uint(6 - i) 199 | b |= 1 << j 200 | } 201 | } 202 | b |= frame.header.OpCode 203 | header = append(header, b) 204 | if frame.header.MaskingKey != nil { 205 | b = 0x80 206 | } else { 207 | b = 0 208 | } 209 | lengthFields := 0 210 | length := len(msg) 211 | switch { 212 | case length <= 125: 213 | b |= byte(length) 214 | case length < 65536: 215 | b |= 126 216 | lengthFields = 2 217 | default: 218 | b |= 127 219 | lengthFields = 8 220 | } 221 | header = append(header, b) 222 | for i := 0; i < lengthFields; i++ { 223 | j := uint((lengthFields - i - 1) * 8) 224 | b = byte((length >> j) & 0xff) 225 | header = append(header, b) 226 | } 227 | if frame.header.MaskingKey != nil { 228 | if len(frame.header.MaskingKey) != 4 { 229 | return 0, ErrBadMaskingKey 230 | } 231 | header = append(header, frame.header.MaskingKey...) 232 | frame.writer.Write(header) 233 | data := make([]byte, length) 234 | for i := range data { 235 | data[i] = msg[i] ^ frame.header.MaskingKey[i%4] 236 | } 237 | frame.writer.Write(data) 238 | err = frame.writer.Flush() 239 | return length, err 240 | } 241 | frame.writer.Write(header) 242 | frame.writer.Write(msg) 243 | err = frame.writer.Flush() 244 | return length, err 245 | } 246 | 247 | func (frame *hybiFrameWriter) Close() error { return nil } 248 | 249 | type hybiFrameWriterFactory struct { 250 | *bufio.Writer 251 | needMaskingKey bool 252 | } 253 | 254 | func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) { 255 | frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType} 256 | if buf.needMaskingKey { 257 | frameHeader.MaskingKey, err = generateMaskingKey() 258 | if err != nil { 259 | return nil, err 260 | } 261 | } 262 | return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil 263 | } 264 | 265 | type hybiFrameHandler struct { 266 | conn *Conn 267 | payloadType byte 268 | } 269 | 270 | func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) { 271 | if handler.conn.IsServerConn() { 272 | // The client MUST mask all frames sent to the server. 273 | if frame.(*hybiFrameReader).header.MaskingKey == nil { 274 | handler.WriteClose(closeStatusProtocolError) 275 | return nil, io.EOF 276 | } 277 | } else { 278 | // The server MUST NOT mask all frames. 279 | if frame.(*hybiFrameReader).header.MaskingKey != nil { 280 | handler.WriteClose(closeStatusProtocolError) 281 | return nil, io.EOF 282 | } 283 | } 284 | if header := frame.HeaderReader(); header != nil { 285 | io.Copy(ioutil.Discard, header) 286 | } 287 | switch frame.PayloadType() { 288 | case ContinuationFrame: 289 | frame.(*hybiFrameReader).header.OpCode = handler.payloadType 290 | case TextFrame, BinaryFrame: 291 | handler.payloadType = frame.PayloadType() 292 | case CloseFrame: 293 | return nil, io.EOF 294 | case PingFrame, PongFrame: 295 | b := make([]byte, maxControlFramePayloadLength) 296 | n, err := io.ReadFull(frame, b) 297 | if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { 298 | return nil, err 299 | } 300 | io.Copy(ioutil.Discard, frame) 301 | if frame.PayloadType() == PingFrame { 302 | if _, err := handler.WritePong(b[:n]); err != nil { 303 | return nil, err 304 | } 305 | } 306 | return nil, nil 307 | } 308 | return frame, nil 309 | } 310 | 311 | func (handler *hybiFrameHandler) WriteClose(status int) (err error) { 312 | handler.conn.wio.Lock() 313 | defer handler.conn.wio.Unlock() 314 | w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame) 315 | if err != nil { 316 | return err 317 | } 318 | msg := make([]byte, 2) 319 | binary.BigEndian.PutUint16(msg, uint16(status)) 320 | _, err = w.Write(msg) 321 | w.Close() 322 | return err 323 | } 324 | 325 | func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) { 326 | handler.conn.wio.Lock() 327 | defer handler.conn.wio.Unlock() 328 | w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame) 329 | if err != nil { 330 | return 0, err 331 | } 332 | n, err = w.Write(msg) 333 | w.Close() 334 | return n, err 335 | } 336 | 337 | // newHybiConn creates a new WebSocket connection speaking hybi draft protocol. 338 | func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 339 | if buf == nil { 340 | br := bufio.NewReader(rwc) 341 | bw := bufio.NewWriter(rwc) 342 | buf = bufio.NewReadWriter(br, bw) 343 | } 344 | ws := &Conn{config: config, request: request, buf: buf, rwc: rwc, 345 | frameReaderFactory: hybiFrameReaderFactory{buf.Reader}, 346 | frameWriterFactory: hybiFrameWriterFactory{ 347 | buf.Writer, request == nil}, 348 | PayloadType: TextFrame, 349 | defaultCloseStatus: closeStatusNormal} 350 | ws.frameHandler = &hybiFrameHandler{conn: ws} 351 | return ws 352 | } 353 | 354 | // generateMaskingKey generates a masking key for a frame. 355 | func generateMaskingKey() (maskingKey []byte, err error) { 356 | maskingKey = make([]byte, 4) 357 | if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil { 358 | return 359 | } 360 | return 361 | } 362 | 363 | // generateNonce generates a nonce consisting of a randomly selected 16-byte 364 | // value that has been base64-encoded. 365 | func generateNonce() (nonce []byte) { 366 | key := make([]byte, 16) 367 | if _, err := io.ReadFull(rand.Reader, key); err != nil { 368 | panic(err) 369 | } 370 | nonce = make([]byte, 24) 371 | base64.StdEncoding.Encode(nonce, key) 372 | return 373 | } 374 | 375 | // removeZone removes IPv6 zone identifer from host. 376 | // E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080" 377 | func removeZone(host string) string { 378 | if !strings.HasPrefix(host, "[") { 379 | return host 380 | } 381 | i := strings.LastIndex(host, "]") 382 | if i < 0 { 383 | return host 384 | } 385 | j := strings.LastIndex(host[:i], "%") 386 | if j < 0 { 387 | return host 388 | } 389 | return host[:j] + host[i:] 390 | } 391 | 392 | // getNonceAccept computes the base64-encoded SHA-1 of the concatenation of 393 | // the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string. 394 | func getNonceAccept(nonce []byte) (expected []byte, err error) { 395 | h := sha1.New() 396 | if _, err = h.Write(nonce); err != nil { 397 | return 398 | } 399 | if _, err = h.Write([]byte(websocketGUID)); err != nil { 400 | return 401 | } 402 | expected = make([]byte, 28) 403 | base64.StdEncoding.Encode(expected, h.Sum(nil)) 404 | return 405 | } 406 | 407 | // Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17 408 | func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { 409 | bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") 410 | 411 | // According to RFC 6874, an HTTP client, proxy, or other 412 | // intermediary must remove any IPv6 zone identifier attached 413 | // to an outgoing URI. 414 | bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n") 415 | bw.WriteString("Upgrade: websocket\r\n") 416 | bw.WriteString("Connection: Upgrade\r\n") 417 | nonce := generateNonce() 418 | if config.handshakeData != nil { 419 | nonce = []byte(config.handshakeData["key"]) 420 | } 421 | bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n") 422 | bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") 423 | 424 | if config.Version != ProtocolVersionHybi13 { 425 | return ErrBadProtocolVersion 426 | } 427 | 428 | bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n") 429 | if len(config.Protocol) > 0 { 430 | bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n") 431 | } 432 | // TODO(ukai): send Sec-WebSocket-Extensions. 433 | err = config.Header.WriteSubset(bw, handshakeHeader) 434 | if err != nil { 435 | return err 436 | } 437 | 438 | bw.WriteString("\r\n") 439 | if err = bw.Flush(); err != nil { 440 | return err 441 | } 442 | 443 | resp, err := http.ReadResponse(br, &http.Request{Method: "GET"}) 444 | if err != nil { 445 | return err 446 | } 447 | if resp.StatusCode != 101 { 448 | return ErrBadStatus 449 | } 450 | if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" || 451 | strings.ToLower(resp.Header.Get("Connection")) != "upgrade" { 452 | return ErrBadUpgrade 453 | } 454 | expectedAccept, err := getNonceAccept(nonce) 455 | if err != nil { 456 | return err 457 | } 458 | if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) { 459 | return ErrChallengeResponse 460 | } 461 | if resp.Header.Get("Sec-WebSocket-Extensions") != "" { 462 | return ErrUnsupportedExtensions 463 | } 464 | offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol") 465 | if offeredProtocol != "" { 466 | protocolMatched := false 467 | for i := 0; i < len(config.Protocol); i++ { 468 | if config.Protocol[i] == offeredProtocol { 469 | protocolMatched = true 470 | break 471 | } 472 | } 473 | if !protocolMatched { 474 | return ErrBadWebSocketProtocol 475 | } 476 | config.Protocol = []string{offeredProtocol} 477 | } 478 | 479 | return nil 480 | } 481 | 482 | // newHybiClientConn creates a client WebSocket connection after handshake. 483 | func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn { 484 | return newHybiConn(config, buf, rwc, nil) 485 | } 486 | 487 | // A HybiServerHandshaker performs a server handshake using hybi draft protocol. 488 | type hybiServerHandshaker struct { 489 | *Config 490 | accept []byte 491 | } 492 | 493 | func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) { 494 | c.Version = ProtocolVersionHybi13 495 | if req.Method != "GET" { 496 | return http.StatusMethodNotAllowed, ErrBadRequestMethod 497 | } 498 | // HTTP version can be safely ignored. 499 | 500 | if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" || 501 | !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") { 502 | return http.StatusBadRequest, ErrNotWebSocket 503 | } 504 | 505 | key := req.Header.Get("Sec-Websocket-Key") 506 | if key == "" { 507 | return http.StatusBadRequest, ErrChallengeResponse 508 | } 509 | version := req.Header.Get("Sec-Websocket-Version") 510 | switch version { 511 | case "13": 512 | c.Version = ProtocolVersionHybi13 513 | default: 514 | return http.StatusBadRequest, ErrBadWebSocketVersion 515 | } 516 | var scheme string 517 | if req.TLS != nil { 518 | scheme = "wss" 519 | } else { 520 | scheme = "ws" 521 | } 522 | c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI()) 523 | if err != nil { 524 | return http.StatusBadRequest, err 525 | } 526 | protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol")) 527 | if protocol != "" { 528 | protocols := strings.Split(protocol, ",") 529 | for i := 0; i < len(protocols); i++ { 530 | c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i])) 531 | } 532 | } 533 | c.accept, err = getNonceAccept([]byte(key)) 534 | if err != nil { 535 | return http.StatusInternalServerError, err 536 | } 537 | return http.StatusSwitchingProtocols, nil 538 | } 539 | 540 | // Origin parses the Origin header in req. 541 | // If the Origin header is not set, it returns nil and nil. 542 | func Origin(config *Config, req *http.Request) (*url.URL, error) { 543 | var origin string 544 | switch config.Version { 545 | case ProtocolVersionHybi13: 546 | origin = req.Header.Get("Origin") 547 | } 548 | if origin == "" { 549 | return nil, nil 550 | } 551 | return url.ParseRequestURI(origin) 552 | } 553 | 554 | func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) { 555 | if len(c.Protocol) > 0 { 556 | if len(c.Protocol) != 1 { 557 | // You need choose a Protocol in Handshake func in Server. 558 | return ErrBadWebSocketProtocol 559 | } 560 | } 561 | buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n") 562 | buf.WriteString("Upgrade: websocket\r\n") 563 | buf.WriteString("Connection: Upgrade\r\n") 564 | buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n") 565 | if len(c.Protocol) > 0 { 566 | buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n") 567 | } 568 | // TODO(ukai): send Sec-WebSocket-Extensions. 569 | if c.Header != nil { 570 | err := c.Header.WriteSubset(buf, handshakeHeader) 571 | if err != nil { 572 | return err 573 | } 574 | } 575 | buf.WriteString("\r\n") 576 | return buf.Flush() 577 | } 578 | 579 | func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 580 | return newHybiServerConn(c.Config, buf, rwc, request) 581 | } 582 | 583 | // newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol. 584 | func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { 585 | return newHybiConn(config, buf, rwc, request) 586 | } 587 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/websocket/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package websocket 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | ) 13 | 14 | func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) { 15 | var hs serverHandshaker = &hybiServerHandshaker{Config: config} 16 | code, err := hs.ReadHandshake(buf.Reader, req) 17 | if err == ErrBadWebSocketVersion { 18 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 19 | fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion) 20 | buf.WriteString("\r\n") 21 | buf.WriteString(err.Error()) 22 | buf.Flush() 23 | return 24 | } 25 | if err != nil { 26 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 27 | buf.WriteString("\r\n") 28 | buf.WriteString(err.Error()) 29 | buf.Flush() 30 | return 31 | } 32 | if handshake != nil { 33 | err = handshake(config, req) 34 | if err != nil { 35 | code = http.StatusForbidden 36 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 37 | buf.WriteString("\r\n") 38 | buf.Flush() 39 | return 40 | } 41 | } 42 | err = hs.AcceptHandshake(buf.Writer) 43 | if err != nil { 44 | code = http.StatusBadRequest 45 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 46 | buf.WriteString("\r\n") 47 | buf.Flush() 48 | return 49 | } 50 | conn = hs.NewServerConn(buf, rwc, req) 51 | return 52 | } 53 | 54 | // Server represents a server of a WebSocket. 55 | type Server struct { 56 | // Config is a WebSocket configuration for new WebSocket connection. 57 | Config 58 | 59 | // Handshake is an optional function in WebSocket handshake. 60 | // For example, you can check, or don't check Origin header. 61 | // Another example, you can select config.Protocol. 62 | Handshake func(*Config, *http.Request) error 63 | 64 | // Handler handles a WebSocket connection. 65 | Handler 66 | } 67 | 68 | // ServeHTTP implements the http.Handler interface for a WebSocket 69 | func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { 70 | s.serveWebSocket(w, req) 71 | } 72 | 73 | func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { 74 | rwc, buf, err := w.(http.Hijacker).Hijack() 75 | if err != nil { 76 | panic("Hijack failed: " + err.Error()) 77 | } 78 | // The server should abort the WebSocket connection if it finds 79 | // the client did not send a handshake that matches with protocol 80 | // specification. 81 | defer rwc.Close() 82 | conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) 83 | if err != nil { 84 | return 85 | } 86 | if conn == nil { 87 | panic("unexpected nil conn") 88 | } 89 | s.Handler(conn) 90 | } 91 | 92 | // Handler is a simple interface to a WebSocket browser client. 93 | // It checks if Origin header is valid URL by default. 94 | // You might want to verify websocket.Conn.Config().Origin in the func. 95 | // If you use Server instead of Handler, you could call websocket.Origin and 96 | // check the origin in your Handshake func. So, if you want to accept 97 | // non-browser clients, which do not send an Origin header, set a 98 | // Server.Handshake that does not check the origin. 99 | type Handler func(*Conn) 100 | 101 | func checkOrigin(config *Config, req *http.Request) (err error) { 102 | config.Origin, err = Origin(config, req) 103 | if err == nil && config.Origin == nil { 104 | return fmt.Errorf("null origin") 105 | } 106 | return err 107 | } 108 | 109 | // ServeHTTP implements the http.Handler interface for a WebSocket 110 | func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 111 | s := Server{Handler: h, Handshake: checkOrigin} 112 | s.serveWebSocket(w, req) 113 | } 114 | -------------------------------------------------------------------------------- /vendor/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 // import "golang.org/x/net/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 | // 148 | // Multiple goroutines may invoke methods on a Conn simultaneously. 149 | type Conn struct { 150 | config *Config 151 | request *http.Request 152 | 153 | buf *bufio.ReadWriter 154 | rwc io.ReadWriteCloser 155 | 156 | rio sync.Mutex 157 | frameReaderFactory 158 | frameReader 159 | 160 | wio sync.Mutex 161 | frameWriterFactory 162 | 163 | frameHandler 164 | PayloadType byte 165 | defaultCloseStatus int 166 | } 167 | 168 | // Read implements the io.Reader interface: 169 | // it reads data of a frame from the WebSocket connection. 170 | // if msg is not large enough for the frame data, it fills the msg and next Read 171 | // will read the rest of the frame data. 172 | // it reads Text frame or Binary frame. 173 | func (ws *Conn) Read(msg []byte) (n int, err error) { 174 | ws.rio.Lock() 175 | defer ws.rio.Unlock() 176 | again: 177 | if ws.frameReader == nil { 178 | frame, err := ws.frameReaderFactory.NewFrameReader() 179 | if err != nil { 180 | return 0, err 181 | } 182 | ws.frameReader, err = ws.frameHandler.HandleFrame(frame) 183 | if err != nil { 184 | return 0, err 185 | } 186 | if ws.frameReader == nil { 187 | goto again 188 | } 189 | } 190 | n, err = ws.frameReader.Read(msg) 191 | if err == io.EOF { 192 | if trailer := ws.frameReader.TrailerReader(); trailer != nil { 193 | io.Copy(ioutil.Discard, trailer) 194 | } 195 | ws.frameReader = nil 196 | goto again 197 | } 198 | return n, err 199 | } 200 | 201 | // Write implements the io.Writer interface: 202 | // it writes data as a frame to the WebSocket connection. 203 | func (ws *Conn) Write(msg []byte) (n int, err error) { 204 | ws.wio.Lock() 205 | defer ws.wio.Unlock() 206 | w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType) 207 | if err != nil { 208 | return 0, err 209 | } 210 | n, err = w.Write(msg) 211 | w.Close() 212 | return n, err 213 | } 214 | 215 | // Close implements the io.Closer interface. 216 | func (ws *Conn) Close() error { 217 | err := ws.frameHandler.WriteClose(ws.defaultCloseStatus) 218 | err1 := ws.rwc.Close() 219 | if err != nil { 220 | return err 221 | } 222 | return err1 223 | } 224 | 225 | func (ws *Conn) IsClientConn() bool { return ws.request == nil } 226 | func (ws *Conn) IsServerConn() bool { return ws.request != nil } 227 | 228 | // LocalAddr returns the WebSocket Origin for the connection for client, or 229 | // the WebSocket location for server. 230 | func (ws *Conn) LocalAddr() net.Addr { 231 | if ws.IsClientConn() { 232 | return &Addr{ws.config.Origin} 233 | } 234 | return &Addr{ws.config.Location} 235 | } 236 | 237 | // RemoteAddr returns the WebSocket location for the connection for client, or 238 | // the Websocket Origin for server. 239 | func (ws *Conn) RemoteAddr() net.Addr { 240 | if ws.IsClientConn() { 241 | return &Addr{ws.config.Location} 242 | } 243 | return &Addr{ws.config.Origin} 244 | } 245 | 246 | var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn") 247 | 248 | // SetDeadline sets the connection's network read & write deadlines. 249 | func (ws *Conn) SetDeadline(t time.Time) error { 250 | if conn, ok := ws.rwc.(net.Conn); ok { 251 | return conn.SetDeadline(t) 252 | } 253 | return errSetDeadline 254 | } 255 | 256 | // SetReadDeadline sets the connection's network read deadline. 257 | func (ws *Conn) SetReadDeadline(t time.Time) error { 258 | if conn, ok := ws.rwc.(net.Conn); ok { 259 | return conn.SetReadDeadline(t) 260 | } 261 | return errSetDeadline 262 | } 263 | 264 | // SetWriteDeadline sets the connection's network write deadline. 265 | func (ws *Conn) SetWriteDeadline(t time.Time) error { 266 | if conn, ok := ws.rwc.(net.Conn); ok { 267 | return conn.SetWriteDeadline(t) 268 | } 269 | return errSetDeadline 270 | } 271 | 272 | // Config returns the WebSocket config. 273 | func (ws *Conn) Config() *Config { return ws.config } 274 | 275 | // Request returns the http request upgraded to the WebSocket. 276 | // It is nil for client side. 277 | func (ws *Conn) Request() *http.Request { return ws.request } 278 | 279 | // Codec represents a symmetric pair of functions that implement a codec. 280 | type Codec struct { 281 | Marshal func(v interface{}) (data []byte, payloadType byte, err error) 282 | Unmarshal func(data []byte, payloadType byte, v interface{}) (err error) 283 | } 284 | 285 | // Send sends v marshaled by cd.Marshal as single frame to ws. 286 | func (cd Codec) Send(ws *Conn, v interface{}) (err error) { 287 | data, payloadType, err := cd.Marshal(v) 288 | if err != nil { 289 | return err 290 | } 291 | ws.wio.Lock() 292 | defer ws.wio.Unlock() 293 | w, err := ws.frameWriterFactory.NewFrameWriter(payloadType) 294 | if err != nil { 295 | return err 296 | } 297 | _, err = w.Write(data) 298 | w.Close() 299 | return err 300 | } 301 | 302 | // Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores in v. 303 | func (cd Codec) Receive(ws *Conn, v interface{}) (err error) { 304 | ws.rio.Lock() 305 | defer ws.rio.Unlock() 306 | if ws.frameReader != nil { 307 | _, err = io.Copy(ioutil.Discard, ws.frameReader) 308 | if err != nil { 309 | return err 310 | } 311 | ws.frameReader = nil 312 | } 313 | again: 314 | frame, err := ws.frameReaderFactory.NewFrameReader() 315 | if err != nil { 316 | return err 317 | } 318 | frame, err = ws.frameHandler.HandleFrame(frame) 319 | if err != nil { 320 | return err 321 | } 322 | if frame == nil { 323 | goto again 324 | } 325 | payloadType := frame.PayloadType() 326 | data, err := ioutil.ReadAll(frame) 327 | if err != nil { 328 | return err 329 | } 330 | return cd.Unmarshal(data, payloadType, v) 331 | } 332 | 333 | func marshal(v interface{}) (msg []byte, payloadType byte, err error) { 334 | switch data := v.(type) { 335 | case string: 336 | return []byte(data), TextFrame, nil 337 | case []byte: 338 | return data, BinaryFrame, nil 339 | } 340 | return nil, UnknownFrame, ErrNotSupported 341 | } 342 | 343 | func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) { 344 | switch data := v.(type) { 345 | case *string: 346 | *data = string(msg) 347 | return nil 348 | case *[]byte: 349 | *data = msg 350 | return nil 351 | } 352 | return ErrNotSupported 353 | } 354 | 355 | /* 356 | Message is a codec to send/receive text/binary data in a frame on WebSocket connection. 357 | To send/receive text frame, use string type. 358 | To send/receive binary frame, use []byte type. 359 | 360 | Trivial usage: 361 | 362 | import "websocket" 363 | 364 | // receive text frame 365 | var message string 366 | websocket.Message.Receive(ws, &message) 367 | 368 | // send text frame 369 | message = "hello" 370 | websocket.Message.Send(ws, message) 371 | 372 | // receive binary frame 373 | var data []byte 374 | websocket.Message.Receive(ws, &data) 375 | 376 | // send binary frame 377 | data = []byte{0, 1, 2} 378 | websocket.Message.Send(ws, data) 379 | 380 | */ 381 | var Message = Codec{marshal, unmarshal} 382 | 383 | func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) { 384 | msg, err = json.Marshal(v) 385 | return msg, TextFrame, err 386 | } 387 | 388 | func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) { 389 | return json.Unmarshal(msg, v) 390 | } 391 | 392 | /* 393 | JSON is a codec to send/receive JSON data in a frame from a WebSocket connection. 394 | 395 | Trivial usage: 396 | 397 | import "websocket" 398 | 399 | type T struct { 400 | Msg string 401 | Count int 402 | } 403 | 404 | // receive JSON type T 405 | var data T 406 | websocket.JSON.Receive(ws, &data) 407 | 408 | // send JSON type T 409 | websocket.JSON.Send(ws, data) 410 | */ 411 | var JSON = Codec{jsonMarshal, jsonUnmarshal} 412 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "t1F04VD0ROQUBEk1pLwEcdZx/Vk=", 7 | "path": "github.com/nlopes/slack", 8 | "revision": "e337a7316d3fe9aa55b7063035d6d32f3dbcaeb9", 9 | "revisionTime": "2016-04-30T16:04:49Z" 10 | }, 11 | { 12 | "checksumSHA1": "fiFafSbOoQaM6Ad+zn7z/xAwLtE=", 13 | "path": "golang.org/x/net/websocket", 14 | "revision": "fb93926129b8ec0056f2f458b1f519654814edf0", 15 | "revisionTime": "2016-04-12T22:48:50Z" 16 | } 17 | ], 18 | "rootPath": "github.com/dgraph-io/wisemonk" 19 | } 20 | -------------------------------------------------------------------------------- /yoda.txt: -------------------------------------------------------------------------------- 1 | ____ 2 | _.' : `._ 3 | .-.'`. ; .'`.-. 4 | __ / : ___\ ; /___ ; \ 5 | ,'_ ""--.:__;".-.";: :".-.":__;.--"" _`, 6 | :' `.t""--.. '<@.`;_ ',@>` ..--""j.' `; 7 | `:-.._J '-.-'L__ `-- ' L_..-;' 8 | "-.__ ; .-" "-. : __.-" 9 | L ' /.------.\ ' J 10 | "-. "--" .-" 11 | __.l"-:_JL_;-";.__ 12 | .-j/'.; ;"""" / .'\"-. 13 | .' /:`. "-.: .-" .'; `. 14 | .-" / ; "-. "-..-" .-" : "-. 15 | .+"-. : : "-.__.-" ;-._ \ 16 | ; \ `.; ; : : "+. ; 17 | : ; ; ; : ; : \: 18 | ; : ; : ;: ; : 19 | : \ ; : ; : ; / :: 20 | ; ; : ; : ; : ;: 21 | : : ; : ; : : ; : ; 22 | ;\ : ; : ; ; ; ; 23 | : `."-; : ; : ; / ; 24 | ; -: ; : ; : .-" : 25 | :\ \ : ; : \.-" : 26 | ;`. \ ; : ;.'_..-- / ; 27 | : "-. "-: ; :/." .' : 28 | \ \ : ;/ __ : 29 | \ .-`.\ /t-"" ":-+. : 30 | `. .-" `l __/ /`. : ; ; \ ; 31 | \ .-" .-"-.-" .' .'j \ / ;/ 32 | \ / .-" /. .'.' ;_:' ; 33 | :-""-.`./-.' / `.___.' 34 | \ `t ._ / 35 | "-.t-._:' 36 | --------------------------------------------------------------------------------