Improve websocket types a bit.
This makes it possible to select on the event type and act accordingly.
Norberto Lopes
9 years ago
0 | package main | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | "time" | |
5 | ||
6 | "github.com/nlopes/slack" | |
7 | ) | |
8 | ||
9 | func main() { | |
10 | chSender := make(chan slack.OutgoingMessage) | |
11 | chReceiver := make(chan slack.SlackEvent) | |
12 | ||
13 | api := slack.New("YOUR TOKEN HERE") | |
14 | api.SetDebug(true) | |
15 | wsAPI, err := api.StartRTM("", "http://example.com") | |
16 | if err != nil { | |
17 | fmt.Errorf("%s\n", err) | |
18 | } | |
19 | go wsAPI.HandleIncomingEvents(chReceiver) | |
20 | go wsAPI.Keepalive(20 * time.Second) | |
21 | go func(wsAPI *slack.SlackWS, chSender chan slack.OutgoingMessage) { | |
22 | for { | |
23 | select { | |
24 | case msg := <-chSender: | |
25 | wsAPI.SendMessage(&msg) | |
26 | } | |
27 | } | |
28 | }(wsAPI, chSender) | |
29 | for { | |
30 | select { | |
31 | case msg := <-chReceiver: | |
32 | fmt.Print("Event Received: ") | |
33 | switch msg.Data.(type) { | |
34 | case slack.HelloEvent: | |
35 | // Ignore hello | |
36 | case *slack.MessageEvent: | |
37 | a := msg.Data.(*slack.MessageEvent) | |
38 | fmt.Printf("%v\n", a) | |
39 | case *slack.PresenceChangeEvent: | |
40 | a := msg.Data.(*slack.PresenceChangeEvent) | |
41 | fmt.Printf("%v\n", a) | |
42 | case slack.LatencyReport: | |
43 | a := msg.Data.(slack.LatencyReport) | |
44 | fmt.Printf("Current latency: %v\n", a.Value) | |
45 | default: | |
46 | fmt.Printf("Unknown: %v\n", msg.Data) | |
47 | } | |
48 | } | |
49 | } | |
50 | } |
22 | 22 | Timestamp JSONTime `json:"timestamp"` |
23 | 23 | UserId string `json:"user"` |
24 | 24 | Comment string `json:"comment"` |
25 | Created JSONTime `json:"created,omitempty"` | |
25 | 26 | } |
26 | 27 | |
27 | 28 | // File contains all the information for a file |
22 | 22 | Timestamp string `json:"ts,omitempty"` |
23 | 23 | Text string `json:"text,omitempty"` |
24 | 24 | Team string `json:"team,omitempty"` |
25 | File File `json:"file,omitempty"` | |
25 | 26 | // Type may come if it's part of a message list |
26 | 27 | // e.g.: channel.history |
27 | 28 | Type string `json:"type,omitempty"` |
31 | 32 | Hidden bool `json:"bool,omitempty"` |
32 | 33 | DeletedTimestamp string `json:"deleted_ts,omitempty"` |
33 | 34 | Attachments []Attachment `json:"attachments,omitempty"` |
35 | ReplyTo int `json:"reply_to,omitempty"` | |
36 | Upload bool `json:"upload,omitempty"` | |
34 | 37 | } |
35 | 38 | |
36 | 39 | // Presence XXX: not used yet |
48 | 51 | type Ping struct { |
49 | 52 | Id int `json:"id"` |
50 | 53 | Type string `json:"type"` |
54 | } | |
55 | ||
56 | // Pong contains information about a Pong Event | |
57 | type Pong struct { | |
58 | Type string `json:"type"` | |
59 | ReplyTo int `json:"reply_to"` | |
51 | 60 | } |
52 | 61 | |
53 | 62 | // AckMessage is used for messages received in reply to other messages |
8 | 8 | Added as a var so that we can change this for testing purposes |
9 | 9 | */ |
10 | 10 | var SLACK_API string = "https://slack.com/api/" |
11 | ||
12 | type UserTyping struct { | |
13 | Type string `json:"type"` | |
14 | UserId string `json:"user"` | |
15 | ChannelId string `json:"channel"` | |
16 | } | |
17 | ||
18 | type SlackEvent struct { | |
19 | Type int | |
20 | Data interface{} | |
21 | } | |
22 | 11 | |
23 | 12 | type SlackResponse struct { |
24 | 13 | Ok bool `json:"ok"` |
17 | 17 | Page int |
18 | 18 | } |
19 | 19 | |
20 | // XXX: Verify this. The whole thing is complicated. I don't like the way they mixed things | |
20 | // TODO: Verify this. The whole thing is complicated. I don't like the way they mixed things | |
21 | // It also appears to be a bug in parsing the message | |
21 | 22 | type StarredItem struct { |
22 | 23 | Type string `json:"type"` |
23 | 24 | ChannelId string `json:"channel"` |
24 | Message `json:"message"` | |
25 | File `json:"file"` | |
26 | Comment `json:"comment"` | |
25 | Message `json:"message,omitempty"` | |
26 | File `json:"file,omitempty"` | |
27 | Comment `json:"comment,omitempty"` | |
27 | 28 | } |
28 | 29 | |
29 | 30 | type starsResponseFull struct { |
6 | 6 | "log" |
7 | 7 | "net" |
8 | 8 | "net/url" |
9 | "strconv" | |
9 | 10 | "sync" |
10 | 11 | "time" |
11 | 12 | |
12 | 13 | "golang.org/x/net/websocket" |
13 | 14 | ) |
14 | 15 | |
15 | const ( | |
16 | EV_MESSAGE = iota | |
17 | EV_USER_TYPING | |
18 | ) | |
16 | type MessageEvent Message | |
17 | ||
18 | var eventMapping map[string]interface{} = map[string]interface{}{ | |
19 | "message": &MessageEvent{}, | |
20 | "presence_change": &PresenceChangeEvent{}, | |
21 | "user_typing": &UserTypingEvent{}, | |
22 | ||
23 | "channel_marked": &ChannelMarkedEvent{}, | |
24 | "channel_created": &ChannelCreatedEvent{}, | |
25 | "channel_joined": &ChannelJoinedEvent{}, | |
26 | "channel_left": &ChannelLeftEvent{}, | |
27 | "channel_deleted": &ChannelDeletedEvent{}, | |
28 | "channel_rename": &ChannelRenameEvent{}, | |
29 | "channel_archive": &ChannelArchiveEvent{}, | |
30 | "channel_unarchive": &ChannelUnarchiveEvent{}, | |
31 | "channel_history_changed": &ChannelHistoryChangedEvent{}, | |
32 | ||
33 | "im_created": &IMCreatedEvent{}, | |
34 | "im_open": &IMOpenEvent{}, | |
35 | "im_close": &IMCloseEvent{}, | |
36 | "im_marked": &IMMarkedEvent{}, | |
37 | "im_history_changed": &IMHistoryChangedEvent{}, | |
38 | ||
39 | "group_marked": &GroupMarkedEvent{}, | |
40 | "group_open": &GroupOpenEvent{}, | |
41 | "group_joined": &GroupJoinedEvent{}, | |
42 | "group_left": &GroupLeftEvent{}, | |
43 | "group_close": &GroupCloseEvent{}, | |
44 | "group_rename": &GroupRenameEvent{}, | |
45 | "group_archive": &GroupArchiveEvent{}, | |
46 | "group_unarchive": &GroupUnarchiveEvent{}, | |
47 | "group_history_changed": &GroupHistoryChangedEvent{}, | |
48 | ||
49 | // XXX: Not implemented below here | |
50 | "file_created": &FileCreatedEvent{}, | |
51 | "file_shared": &FileSharedEvent{}, | |
52 | "file_unshared": &FileUnsharedEvent{}, | |
53 | "file_public": &FilePublicEvent{}, | |
54 | "file_private": &FilePrivateEvent{}, | |
55 | "file_change": &FileChangeEvent{}, | |
56 | "file_deleted": &FileDeletedEvent{}, | |
57 | "file_comment_added": &FileCommentAddedEvent{}, | |
58 | "file_comment_edited": &FileCommentEditedEvent{}, | |
59 | "file_comment_deleted": &FileCommentDeletedEvent{}, | |
60 | ||
61 | "team_join": &TeamJoinEvent{}, | |
62 | "team_rename": &TeamRenameEvent{}, | |
63 | "team_pref_change": &TeamPrefChangeEvent{}, | |
64 | "team_domain_change": &TeamDomainChangeEvent{}, | |
65 | "team_migration_started": &TeamMigrationStartedEvent{}, | |
66 | ||
67 | "manual_presence_change": &ManualPresenceChangeEvent{}, | |
68 | ||
69 | "pref_change": &PrefChangeEvent{}, | |
70 | "user_change": &UserChangeEvent{}, | |
71 | ||
72 | "star_added": &StarAddedEvent{}, | |
73 | "star_removed": &StarRemovedEvent{}, | |
74 | ||
75 | "emoji_changed": &EmojiChangedEvent{}, | |
76 | ||
77 | "commands_changed": &CommandsChangedEvent{}, | |
78 | ||
79 | "email_domain_changed": &EmailDomainChangedEvent{}, | |
80 | ||
81 | "bot_added": &BotAddedEvent{}, | |
82 | "bot_changed": &BotChangedEvent{}, | |
83 | ||
84 | "accounts_changed": &AccountsChangedEvent{}, | |
85 | } | |
19 | 86 | |
20 | 87 | type SlackWS struct { |
21 | 88 | conn *websocket.Conn |
22 | 89 | messageId int |
23 | 90 | mutex sync.Mutex |
91 | pings map[int]time.Time | |
24 | 92 | Slack |
25 | 93 | } |
26 | 94 | |
32 | 100 | type SlackWSError struct { |
33 | 101 | Code int |
34 | 102 | Msg string |
103 | } | |
104 | ||
105 | type SlackEvent struct { | |
106 | Type uint64 | |
107 | Data interface{} | |
108 | } | |
109 | ||
110 | type JSONTimeString string | |
111 | ||
112 | // String converts the unix timestamp into a string | |
113 | func (t JSONTimeString) String() string { | |
114 | if t == "" { | |
115 | return "" | |
116 | } | |
117 | floatN, err := strconv.ParseFloat(string(t), 64) | |
118 | if err != nil { | |
119 | log.Panicln(err) | |
120 | return "" | |
121 | } | |
122 | timeStr := int64(floatN) | |
123 | tm := time.Unix(int64(timeStr), 0) | |
124 | return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) | |
35 | 125 | } |
36 | 126 | |
37 | 127 | func (s SlackWSError) Error() string { |
75 | 165 | if err != nil { |
76 | 166 | return nil, err |
77 | 167 | } |
168 | wsApi.pings = make(map[int]time.Time) | |
78 | 169 | return wsApi, nil |
79 | 170 | } |
80 | 171 | |
86 | 177 | if err := websocket.JSON.Send(api.conn, msg); err != nil { |
87 | 178 | return err |
88 | 179 | } |
180 | // TODO: What happens if we already have this id? | |
181 | api.pings[api.messageId] = time.Now() | |
89 | 182 | return nil |
90 | 183 | } |
91 | 184 | |
139 | 232 | if api.debug { |
140 | 233 | log.Println(string(event[:])) |
141 | 234 | } |
142 | handleEvent(ch, event) | |
235 | api.handleEvent(ch, event) | |
143 | 236 | } |
144 | 237 | time.Sleep(time.Millisecond * 500) |
145 | 238 | } |
146 | 239 | } |
147 | 240 | |
148 | func handleEvent(ch chan SlackEvent, event json.RawMessage) { | |
241 | func (api *SlackWS) handleEvent(ch chan SlackEvent, event json.RawMessage) { | |
149 | 242 | em := Event{} |
150 | 243 | err := json.Unmarshal(event, &em) |
151 | 244 | if err != nil { |
152 | 245 | log.Fatal(err) |
153 | 246 | } |
154 | ||
247 | log.Println(em.Type) | |
155 | 248 | switch em.Type { |
156 | 249 | case "": |
157 | 250 | // try ok |
161 | 254 | } |
162 | 255 | |
163 | 256 | if ack.Ok { |
257 | // TODO: Send the ack back (is this useful?) | |
258 | //ch <- SlackEvent{Type: EventAck, Data: ack} | |
164 | 259 | log.Printf("Received an ok for: %d", ack.ReplyTo) |
165 | 260 | return |
166 | 261 | } |
168 | 263 | // TODO: errors end up in this bucket. They shouldn't. |
169 | 264 | log.Printf("Got error(?): %s", event) |
170 | 265 | case "hello": |
171 | return | |
266 | ch <- SlackEvent{Data: HelloEvent{}} | |
172 | 267 | case "pong": |
173 | // XXX: Eventually check to which ping this matched with | |
174 | // Allows us to have stats about latency and what not | |
175 | return | |
176 | case "presence_change": | |
177 | //log.Printf("`%s is %s`\n", info.GetUserById(event.PUserId).Name, event.Presence) | |
178 | case "message": | |
179 | handleMessage(ch, event) | |
180 | case "channel_marked": | |
181 | log.Printf("XXX: To implement %s", em) | |
182 | case "user_typing": | |
183 | handleUserTyping(ch, event) | |
268 | pong := Pong{} | |
269 | if err = json.Unmarshal(event, &pong); err != nil { | |
270 | log.Fatal(err) | |
271 | } | |
272 | api.mutex.Lock() | |
273 | latency := time.Since(api.pings[pong.ReplyTo]) | |
274 | api.mutex.Unlock() | |
275 | ch <- SlackEvent{Data: LatencyReport{Value: latency}} | |
184 | 276 | default: |
185 | log.Println("XXX: " + string(event)) | |
186 | } | |
187 | } | |
188 | ||
189 | func handleUserTyping(ch chan SlackEvent, event json.RawMessage) { | |
190 | msg := UserTyping{} | |
277 | callEvent(em.Type, ch, event) | |
278 | } | |
279 | } | |
280 | ||
281 | func callEvent(eventType string, ch chan SlackEvent, event json.RawMessage) { | |
282 | msg := eventMapping[eventType] | |
283 | if msg == nil { | |
284 | log.Printf("XXX: Not implemented yet: %s -> %v", eventType, event) | |
285 | } | |
191 | 286 | if err := json.Unmarshal(event, &msg); err != nil { |
192 | 287 | log.Fatal(err) |
193 | 288 | } |
194 | ch <- SlackEvent{Type: EV_USER_TYPING, Data: msg} | |
195 | } | |
196 | ||
197 | func handleMessage(ch chan SlackEvent, event json.RawMessage) { | |
198 | msg := Message{} | |
199 | err := json.Unmarshal(event, &msg) | |
200 | if err != nil { | |
201 | log.Fatal(err) | |
202 | } | |
203 | ch <- SlackEvent{Type: EV_MESSAGE, Data: msg} | |
204 | } | |
289 | ch <- SlackEvent{Data: msg} | |
290 | } |
0 | package slack | |
1 | ||
2 | type ChannelCreatedEvent struct { | |
3 | Type string `json:"type"` | |
4 | Channel ChannelCreatedInfo `json:"channel"` | |
5 | EventTimestamp JSONTimeString `json:"event_ts"` | |
6 | } | |
7 | ||
8 | type ChannelCreatedInfo struct { | |
9 | Id string `json:"id"` | |
10 | IsChannel bool `json:"is_channel"` | |
11 | Name string `json:"name"` | |
12 | Created JSONTimeString `json:"created"` | |
13 | Creator string `json:"creator"` | |
14 | } | |
15 | ||
16 | type ChannelJoinedEvent struct { | |
17 | Type string `json:"type"` | |
18 | Channel Channel `json:"channel"` | |
19 | } | |
20 | ||
21 | type ChannelInfoEvent struct { | |
22 | // channel_left | |
23 | // channel_deleted | |
24 | // channel_archive | |
25 | // channel_unarchive | |
26 | Type string `json:"type"` | |
27 | ChannelId string `json:"channel"` | |
28 | UserId string `json:"user,omitempty"` | |
29 | Timestamp JSONTimeString `json:"ts,omitempty"` | |
30 | } | |
31 | ||
32 | type ChannelRenameEvent struct { | |
33 | Type string `json:"type"` | |
34 | Channel ChannelRenameInfo `json:"channel"` | |
35 | } | |
36 | ||
37 | type ChannelRenameInfo struct { | |
38 | Id string `json:"id"` | |
39 | Name string `json:"name"` | |
40 | Created JSONTime `json:"created"` | |
41 | } | |
42 | ||
43 | type ChannelHistoryChangedEvent struct { | |
44 | Type string `json:"type"` | |
45 | Latest JSONTimeString `json:"latest"` | |
46 | Timestamp JSONTimeString `json:"ts"` | |
47 | EventTimestamp JSONTimeString `json:"event_ts"` | |
48 | } | |
49 | ||
50 | type ChannelMarkedEvent ChannelInfoEvent | |
51 | type ChannelLeftEvent ChannelInfoEvent | |
52 | type ChannelDeletedEvent ChannelInfoEvent | |
53 | type ChannelArchiveEvent ChannelInfoEvent | |
54 | type ChannelUnarchiveEvent ChannelInfoEvent |
0 | package slack | |
1 | ||
2 | type IMCreatedEvent struct { | |
3 | Type string `json:"type"` | |
4 | UserId string `json:"user"` | |
5 | Channel ChannelCreatedInfo `json:"channel"` | |
6 | } | |
7 | ||
8 | type IMHistoryChangedEvent ChannelHistoryChangedEvent | |
9 | type IMOpenEvent ChannelInfoEvent | |
10 | type IMCloseEvent ChannelInfoEvent | |
11 | type IMMarkedEvent ChannelInfoEvent | |
12 | type IMMarkedHistoryChanged ChannelInfoEvent |
0 | package slack | |
1 | ||
2 | type fileActionEvent struct { | |
3 | Type string `json:"type"` | |
4 | EventTimestamp JSONTimeString `json:"event_ts"` | |
5 | File File `json:"file"` | |
6 | // FileId is used for FileDeletedEvent | |
7 | FileId string `json:"file_id,omitempty"` | |
8 | } | |
9 | ||
10 | type FileCreatedEvent fileActionEvent | |
11 | type FileSharedEvent fileActionEvent | |
12 | type FilePublicEvent fileActionEvent | |
13 | type FileUnsharedEvent fileActionEvent | |
14 | type FileChangeEvent fileActionEvent | |
15 | type FileDeletedEvent fileActionEvent | |
16 | type FilePrivateEvent fileActionEvent | |
17 | ||
18 | type FileCommentAddedEvent struct { | |
19 | fileActionEvent | |
20 | Comment Comment `json:"comment"` | |
21 | } | |
22 | ||
23 | type FileCommentEditedEvent struct { | |
24 | fileActionEvent | |
25 | Comment Comment `json:"comment"` | |
26 | } | |
27 | ||
28 | type FileCommentDeletedEvent struct { | |
29 | fileActionEvent | |
30 | CommentId string `json:"comment"` | |
31 | } |
0 | package slack | |
1 | ||
2 | type GroupCreatedEvent struct { | |
3 | Type string `json:"type"` | |
4 | UserId string `json:"user"` | |
5 | Channel ChannelCreatedInfo `json:"channel"` | |
6 | } | |
7 | ||
8 | // XXX: Should we really do this? event.Group is probably nicer than event.Channel | |
9 | // even though the api returns "channel" | |
10 | type GroupMarkedEvent ChannelInfoEvent | |
11 | type GroupOpenEvent ChannelInfoEvent | |
12 | type GroupCloseEvent ChannelInfoEvent | |
13 | type GroupArchiveEvent ChannelInfoEvent | |
14 | type GroupUnarchiveEvent ChannelInfoEvent | |
15 | type GroupLeftEvent ChannelInfoEvent | |
16 | type GroupJoinedEvent ChannelJoinedEvent | |
17 | type GroupRenameEvent ChannelRenameEvent | |
18 | type GroupHistoryChangedEvent ChannelHistoryChangedEvent |
0 | package slack | |
1 | ||
2 | import ( | |
3 | "encoding/json" | |
4 | "time" | |
5 | ) | |
6 | ||
7 | // TODO: Probably need an error event | |
8 | ||
9 | type HelloEvent struct{} | |
10 | ||
11 | type PresenceChangeEvent struct { | |
12 | Type string `json:"type"` | |
13 | Presence string `json:"presence"` | |
14 | UserId string `json:"user"` | |
15 | } | |
16 | ||
17 | type UserTypingEvent struct { | |
18 | Type string `json:"type"` | |
19 | UserId string `json:"user"` | |
20 | ChannelId string `json:"channel"` | |
21 | } | |
22 | ||
23 | type LatencyReport struct { | |
24 | Value time.Duration | |
25 | } | |
26 | ||
27 | type PrefChangeEvent struct { | |
28 | Type string `json:"type"` | |
29 | Name string `json:"name"` | |
30 | Value json.RawMessage `json:"value"` | |
31 | } | |
32 | ||
33 | // TODO | |
34 | type ManualPresenceChangeEvent struct{} | |
35 | type UserChangeEvent struct{} | |
36 | type EmojiChangedEvent struct{} | |
37 | type CommandsChangedEvent struct{} | |
38 | type EmailDomainChangedEvent struct{} | |
39 | type BotAddedEvent struct{} | |
40 | type BotChangedEvent struct{} | |
41 | type AccountsChangedEvent struct{} |