Initial commit
Norberto Lopes
9 years ago
0 | package slack | |
1 | ||
2 | import ( | |
3 | "encoding/json" | |
4 | "log" | |
5 | "net/http" | |
6 | "net/url" | |
7 | ) | |
8 | ||
9 | type ChannelHistory struct { | |
10 | Ok bool `json:"ok"` | |
11 | Latest string `json:"latest"` | |
12 | Messages []Message `json:"messages"` | |
13 | HasMore bool `json:"has_more"` | |
14 | ||
15 | Error string `json:"error"` | |
16 | } | |
17 | ||
18 | type ChannelTopic struct { | |
19 | Value string `json:"value"` | |
20 | Creator string `json:"creator"` | |
21 | LastSet JSONTime `json:"last_set"` | |
22 | } | |
23 | ||
24 | type Channel struct { | |
25 | Id string `json:"id"` | |
26 | Name string `json:"name"` | |
27 | IsChannel bool `json:"is_channel"` | |
28 | Creator string `json:"creator"` | |
29 | IsArchived bool `json:"is_archived"` | |
30 | IsGeneral bool `json:"is_general"` | |
31 | Members []string `json:"members"` | |
32 | Topic ChannelTopic `json:"topic"` | |
33 | Created JSONTime `json:"created"` | |
34 | IsMember bool `json:"is_member"` | |
35 | LastRead string `json:"last_read"` | |
36 | Latest Message `json:"latest"` | |
37 | UnreadCount int `json:"unread_count"` | |
38 | } | |
39 | ||
40 | func (api *SlackAPI) GetChannelHistory(channel_id string, latest string) ChannelHistory { | |
41 | channel_history := ChannelHistory{} | |
42 | resp, err := http.PostForm(SLACK_API+"channels.history", | |
43 | url.Values{ | |
44 | "token": {api.config.token}, | |
45 | "channel": {channel_id}, | |
46 | "latest": {latest}, | |
47 | }) | |
48 | if err != nil { | |
49 | log.Fatal(err) | |
50 | } | |
51 | defer resp.Body.Close() | |
52 | decoder := json.NewDecoder(resp.Body) | |
53 | if err = decoder.Decode(&channel_history); err != nil { | |
54 | log.Fatal(err) | |
55 | } | |
56 | if !channel_history.Ok { | |
57 | log.Fatal(channel_history.Error) | |
58 | } | |
59 | return channel_history | |
60 | } |
0 | package slack | |
1 | ||
2 | const ( | |
3 | SLACK_API = "https://slack.com/api/" | |
4 | ) | |
5 | ||
6 | const ( | |
7 | EV_MESSAGE = iota | |
8 | EV_USER_TYPING | |
9 | ) |
0 | package slack | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | "time" | |
5 | ) | |
6 | ||
7 | type User struct { | |
8 | Id string `json:"id"` | |
9 | Name string `json:"name"` | |
10 | } | |
11 | ||
12 | type UserPrefs struct { | |
13 | //{ | |
14 | // "highlight_words":"", | |
15 | // "user_colors":"", | |
16 | // "color_names_in_list":true, | |
17 | // "growls_enabled":true, | |
18 | // "tz":"Europe\/London", | |
19 | // "push_dm_alert":true, | |
20 | // "push_mention_alert":true, | |
21 | // "push_everything":true, | |
22 | // "push_idle_wait":2, | |
23 | // "push_sound":"b2.mp3", | |
24 | // "push_loud_channels":"", | |
25 | // "push_mention_channels":"", | |
26 | // "push_loud_channels_set":"", | |
27 | // "email_alerts":"instant", | |
28 | // "email_alerts_sleep_until":0, | |
29 | // "email_misc":false, | |
30 | // "email_weekly":true, | |
31 | // "welcome_message_hidden":false, | |
32 | // "all_channels_loud":true, | |
33 | // "loud_channels":"", | |
34 | // "never_channels":"", | |
35 | // "loud_channels_set":"", | |
36 | // "show_member_presence":true, | |
37 | // "search_sort":"timestamp", | |
38 | // "expand_inline_imgs":true,"expand_internal_inline_imgs":true,"expand_snippets":false,"posts_formatting_guide":true,"seen_welcome_2":true,"seen_ssb_prompt":false,"search_only_my_channels":false,"emoji_mode":"default","has_invited":true,"has_uploaded":false,"has_created_channel":true,"search_exclude_channels":"","messages_theme":"default","webapp_spellcheck":true,"no_joined_overlays":false,"no_created_overlays":true,"dropbox_enabled":false,"seen_user_menu_tip_card":true,"seen_team_menu_tip_card":true,"seen_channel_menu_tip_card":true,"seen_message_input_tip_card":true,"seen_channels_tip_card":true,"seen_domain_invite_reminder":false,"seen_member_invite_reminder":false,"seen_flexpane_tip_card":true,"seen_search_input_tip_card":true,"mute_sounds":false,"arrow_history":false,"tab_ui_return_selects":true,"obey_inline_img_limit":true,"new_msg_snd":"knock_brush.mp3","collapsible":false,"collapsible_by_click":true,"require_at":false,"mac_ssb_bounce":"","mac_ssb_bullet":true,"win_ssb_bullet":true,"expand_non_media_attachments":true,"show_typing":true,"pagekeys_handled":true,"last_snippet_type":"","display_real_names_override":0,"time24":false,"enter_is_special_in_tbt":false,"graphic_emoticons":false,"convert_emoticons":true,"autoplay_chat_sounds":true,"ss_emojis":true,"sidebar_behavior":"","mark_msgs_read_immediately":true,"start_scroll_at_oldest":true,"snippet_editor_wrap_long_lines":false,"ls_disabled":false,"sidebar_theme":"default","sidebar_theme_custom_values":"","f_key_search":false,"k_key_omnibox":true,"speak_growls":false,"mac_speak_voice":"com.apple.speech.synthesis.voice.Alex","mac_speak_speed":250,"comma_key_prefs":false,"at_channel_suppressed_channels":"","push_at_channel_suppressed_channels":"","prompted_for_email_disabling":false,"full_text_extracts":false,"no_text_in_notifications":false,"muted_channels":"","no_macssb1_banner":false,"privacy_policy_seen":true,"search_exclude_bots":false,"fuzzy_matching":false} | |
39 | } | |
40 | ||
41 | type UserDetails struct { | |
42 | Id string `json:"id"` | |
43 | Name string `json:"name"` | |
44 | Created JSONTime `json:"created"` | |
45 | ManualPresence string `json:"manual_presence"` | |
46 | Prefs UserPrefs `json:"prefs"` | |
47 | } | |
48 | ||
49 | type JSONTime int64 | |
50 | ||
51 | func (t JSONTime) String() string { | |
52 | tm := time.Unix(int64(t), 0) | |
53 | return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) | |
54 | } | |
55 | ||
56 | type Team struct { | |
57 | Id string `json:"id"` | |
58 | Name string `json:"name"` | |
59 | Domain string `json:"name"` | |
60 | } | |
61 | ||
62 | type Info struct { | |
63 | Ok bool `json:"ok"` | |
64 | Error string `json:"error,omitempty"` | |
65 | Url string `json:"url,omitempty"` | |
66 | User UserDetails `json:"self,omitempty"` | |
67 | Team Team `json:"team,omitempty"` | |
68 | Users []User `json:"users,omitempty"` | |
69 | Channels []Channel `json:"channels,omitempty"` | |
70 | } |
0 | package slack | |
1 | ||
2 | type OutgoingMessage struct { | |
3 | Id int `json:"id"` | |
4 | ChannelID string `json:"channel,omitempty"` | |
5 | Text string `json:"text,omitempty"` | |
6 | Type string `json:"type,omitempty"` | |
7 | } | |
8 | ||
9 | type Message struct { | |
10 | Msg | |
11 | SubMessage Msg `json:"message,omitempty"` | |
12 | } | |
13 | ||
14 | type Msg struct { | |
15 | Id string `json:"id"` | |
16 | UserID string `json:"user,omitempty"` | |
17 | ChannelID string `json:"channel,omitempty"` | |
18 | Timestamp string `json:"ts,omitempty"` | |
19 | Text string `json:"text,omitempty"` | |
20 | // Type may come if it's part of a message list | |
21 | // e.g.: channel.history | |
22 | Type string `json:"type,omitempty"` | |
23 | IsStarred bool `json:"is_starred,omitempty"` | |
24 | // Submessage | |
25 | SubType string `json:"subtype,omitempty"` | |
26 | Hidden bool `json:"bool,omitempty"` | |
27 | DeletedTimestamp string `json:"deleted_ts,omitempty"` | |
28 | } | |
29 | ||
30 | type Presence struct { | |
31 | Presence string `json:"presence"` | |
32 | UserID string `json:"user"` | |
33 | } | |
34 | ||
35 | type Event struct { | |
36 | Type string `json:"type,omitempty"` | |
37 | } | |
38 | ||
39 | type Ping struct { | |
40 | Id int `json:"id"` | |
41 | Type string `json:"type"` | |
42 | } | |
43 | ||
44 | type AckMessage struct { | |
45 | Ok bool `json:"ok"` | |
46 | ReplyTo int `json:"reply_to"` | |
47 | Timestamp string `json:"ts"` | |
48 | Text string `json:"text"` | |
49 | } | |
50 | ||
51 | var message_id int | |
52 | ||
53 | func NewOutgoingMessage(text string, channel string) *OutgoingMessage { | |
54 | message_id++ | |
55 | return &OutgoingMessage{ | |
56 | Id: message_id, | |
57 | Type: "message", | |
58 | ChannelID: channel, | |
59 | Text: text, | |
60 | } | |
61 | } | |
62 | ||
63 | // XXX: maybe support variable arguments so that people | |
64 | // can send stuff through their ping | |
65 | func NewPing() *Ping { | |
66 | message_id++ | |
67 | return &Ping{Id: message_id, Type: "ping"} | |
68 | } | |
69 | ||
70 | func (info Info) GetUserById(id string) *User { | |
71 | for _, user := range info.Users { | |
72 | if user.Id == id { | |
73 | return &user | |
74 | } | |
75 | } | |
76 | return nil | |
77 | } | |
78 | ||
79 | func (info Info) GetChannelById(id string) *Channel { | |
80 | for _, channel := range info.Channels { | |
81 | if channel.Id == id { | |
82 | return &channel | |
83 | } | |
84 | } | |
85 | return nil | |
86 | } |
0 | package slack | |
1 | ||
2 | import ( | |
3 | "encoding/json" | |
4 | "errors" | |
5 | "io" | |
6 | "log" | |
7 | "net/http" | |
8 | "net/url" | |
9 | "time" | |
10 | ||
11 | "golang.org/x/net/websocket" | |
12 | ) | |
13 | ||
14 | type UserTyping struct { | |
15 | Type string `json:"type"` | |
16 | UserID string `json:"user"` | |
17 | ChannelID string `json:"channel"` | |
18 | } | |
19 | ||
20 | type SlackEvent struct { | |
21 | Type int | |
22 | Data interface{} | |
23 | } | |
24 | ||
25 | type SlackAPI struct { | |
26 | config Config | |
27 | conn *websocket.Conn | |
28 | ||
29 | info Info | |
30 | } | |
31 | ||
32 | func New(token string) *SlackAPI { | |
33 | return &SlackAPI{ | |
34 | config: Config{token: token}, | |
35 | } | |
36 | } | |
37 | ||
38 | func (api *SlackAPI) GetInfo() Info { | |
39 | return api.info | |
40 | } | |
41 | ||
42 | func (api *SlackAPI) StartRTM(protocol string, origin string) error { | |
43 | resp, err := http.PostForm(SLACK_API+"rtm.start", url.Values{"token": {api.config.token}}) | |
44 | if err != nil { | |
45 | return err | |
46 | } | |
47 | defer resp.Body.Close() | |
48 | decoder := json.NewDecoder(resp.Body) | |
49 | if err = decoder.Decode(&api.info); err != nil { | |
50 | return err | |
51 | } | |
52 | if !api.info.Ok { | |
53 | return errors.New(api.info.Error) | |
54 | } | |
55 | api.config.protocol, api.config.origin = protocol, origin | |
56 | api.conn, err = websocket.Dial(api.info.Url, api.config.protocol, api.config.origin) | |
57 | if err != nil { | |
58 | return err | |
59 | } | |
60 | return nil | |
61 | } | |
62 | ||
63 | func (api *SlackAPI) Ping() error { | |
64 | if err := websocket.JSON.Send(api.conn, NewPing()); err != nil { | |
65 | return err | |
66 | } | |
67 | return nil | |
68 | } | |
69 | ||
70 | func (api *SlackAPI) SendMessage(msg OutgoingMessage) error { | |
71 | if err := websocket.JSON.Send(api.conn, msg); err != nil { | |
72 | return err | |
73 | } | |
74 | return nil | |
75 | } | |
76 | ||
77 | func (api *SlackAPI) HandleIncomingEvents(ch *chan SlackEvent) { | |
78 | event := json.RawMessage{} | |
79 | for { | |
80 | if err := websocket.JSON.Receive(api.conn, &event); err == io.EOF { | |
81 | // should we reconnect here? | |
82 | if !api.conn.IsClientConn() { | |
83 | api.conn, err = websocket.Dial(api.info.Url, api.config.protocol, api.config.origin) | |
84 | if err != nil { | |
85 | log.Panic(err) | |
86 | } | |
87 | } | |
88 | // XXX: check for timeout and implement exponential backoff | |
89 | } else if err != nil { | |
90 | log.Panic(err) | |
91 | } | |
92 | if len(event) == 0 { | |
93 | log.Println("Event Empty. WTF?") | |
94 | } else { | |
95 | handle_event(ch, event) | |
96 | } | |
97 | time.Sleep(time.Millisecond * 500) | |
98 | } | |
99 | } | |
100 | ||
101 | func handle_event(ch *chan SlackEvent, event json.RawMessage) { | |
102 | em := Event{} | |
103 | err := json.Unmarshal(event, &em) | |
104 | if err != nil { | |
105 | log.Fatal(err) | |
106 | } | |
107 | switch em.Type { | |
108 | case "": | |
109 | // try ok | |
110 | ack := AckMessage{} | |
111 | if err = json.Unmarshal(event, &ack); err != nil { | |
112 | log.Fatal(err) | |
113 | } | |
114 | if ack.Ok { | |
115 | log.Printf("Received an ok for: %d", ack.ReplyTo) | |
116 | } else { | |
117 | log.Println(event) | |
118 | log.Println("XXX: ?") | |
119 | } | |
120 | case "hello": | |
121 | return | |
122 | case "pong": | |
123 | // XXX: Eventually check to which ping this matched with | |
124 | // Allows us to have stats about latency and what not | |
125 | return | |
126 | case "presence_change": | |
127 | //log.Printf("`%s is %s`\n", info.GetUserById(event.PUserID).Name, event.Presence) | |
128 | case "message": | |
129 | handle_message(ch, event) | |
130 | case "channel_marked": | |
131 | log.Printf("XXX: To implement %s", em) | |
132 | case "user_typing": | |
133 | handle_user_typing(ch, event) | |
134 | default: | |
135 | log.Println("XXX: " + string(event)) | |
136 | } | |
137 | } | |
138 | ||
139 | func handle_user_typing(ch *chan SlackEvent, event json.RawMessage) { | |
140 | msg := UserTyping{} | |
141 | if err := json.Unmarshal(event, &msg); err != nil { | |
142 | log.Fatal(err) | |
143 | } | |
144 | *ch <- SlackEvent{Type: EV_USER_TYPING, Data: msg} | |
145 | } | |
146 | ||
147 | func handle_message(ch *chan SlackEvent, event json.RawMessage) { | |
148 | msg := Message{} | |
149 | err := json.Unmarshal(event, &msg) | |
150 | if err != nil { | |
151 | log.Fatal(err) | |
152 | } | |
153 | *ch <- SlackEvent{Type: EV_MESSAGE, Data: msg} | |
154 | } |