From 8258160238a6951085878bd28d90f83329957d76 Mon Sep 17 00:00:00 2001 From: Norberto Lopes Date: Fri, 9 Jan 2015 21:14:04 +0000 Subject: [PATCH] Initial commit --- channel.go | 61 +++++++++++++++++++++ config.go | 7 +++ const.go | 10 ++++ info.go | 71 ++++++++++++++++++++++++ messages.go | 87 +++++++++++++++++++++++++++++ slack.go | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 391 insertions(+) create mode 100644 channel.go create mode 100644 config.go create mode 100644 const.go create mode 100644 info.go create mode 100644 messages.go create mode 100644 slack.go diff --git a/channel.go b/channel.go new file mode 100644 index 000000000..07b6c4192 --- /dev/null +++ b/channel.go @@ -0,0 +1,61 @@ +package slack + +import ( + "encoding/json" + "log" + "net/http" + "net/url" +) + +type ChannelHistory struct { + Ok bool `json:"ok"` + Latest string `json:"latest"` + Messages []Message `json:"messages"` + HasMore bool `json:"has_more"` + + Error string `json:"error"` +} + +type ChannelTopic struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet JSONTime `json:"last_set"` +} + +type Channel struct { + Id string `json:"id"` + Name string `json:"name"` + IsChannel bool `json:"is_channel"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + IsGeneral bool `json:"is_general"` + Members []string `json:"members"` + Topic ChannelTopic `json:"topic"` + Created JSONTime `json:"created"` + IsMember bool `json:"is_member"` + LastRead string `json:"last_read"` + Latest Message `json:"latest"` + UnreadCount int `json:"unread_count"` +} + +func (api *SlackAPI) GetChannelHistory(channel_id string, latest string) ChannelHistory { + channel_history := ChannelHistory{} + resp, err := http.PostForm(SLACK_API+"channels.history", + url.Values{ + "token": {api.config.token}, + "channel": {channel_id}, + "latest": {latest}, + }) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) + if err = decoder.Decode(&channel_history); err != nil { + log.Fatal(err) + } + if !channel_history.Ok { + log.Fatal(channel_history.Error) + } + return channel_history +} diff --git a/config.go b/config.go new file mode 100644 index 000000000..daf1a8d12 --- /dev/null +++ b/config.go @@ -0,0 +1,7 @@ +package slack + +type Config struct { + token string + origin string + protocol string +} diff --git a/const.go b/const.go new file mode 100644 index 000000000..c3ac58c02 --- /dev/null +++ b/const.go @@ -0,0 +1,10 @@ +package slack + +const ( + SLACK_API = "https://slack.com/api/" +) + +const ( + EV_MESSAGE = iota + EV_USER_TYPING +) diff --git a/info.go b/info.go new file mode 100644 index 000000000..a6e791601 --- /dev/null +++ b/info.go @@ -0,0 +1,71 @@ +package slack + +import ( + "fmt" + "time" +) + +type User struct { + Id string `json:"id"` + Name string `json:"name"` +} + +type UserPrefs struct { + //{ + // "highlight_words":"", + // "user_colors":"", + // "color_names_in_list":true, + // "growls_enabled":true, + // "tz":"Europe\/London", + // "push_dm_alert":true, + // "push_mention_alert":true, + // "push_everything":true, + // "push_idle_wait":2, + // "push_sound":"b2.mp3", + // "push_loud_channels":"", + // "push_mention_channels":"", + // "push_loud_channels_set":"", + // "email_alerts":"instant", + // "email_alerts_sleep_until":0, + // "email_misc":false, + // "email_weekly":true, + // "welcome_message_hidden":false, + // "all_channels_loud":true, + // "loud_channels":"", + // "never_channels":"", + // "loud_channels_set":"", + // "show_member_presence":true, + // "search_sort":"timestamp", + // "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} +} + +type UserDetails struct { + Id string `json:"id"` + Name string `json:"name"` + Created JSONTime `json:"created"` + ManualPresence string `json:"manual_presence"` + Prefs UserPrefs `json:"prefs"` +} + +type JSONTime int64 + +func (t JSONTime) String() string { + tm := time.Unix(int64(t), 0) + return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) +} + +type Team struct { + Id string `json:"id"` + Name string `json:"name"` + Domain string `json:"name"` +} + +type Info struct { + Ok bool `json:"ok"` + Error string `json:"error,omitempty"` + Url string `json:"url,omitempty"` + User UserDetails `json:"self,omitempty"` + Team Team `json:"team,omitempty"` + Users []User `json:"users,omitempty"` + Channels []Channel `json:"channels,omitempty"` +} diff --git a/messages.go b/messages.go new file mode 100644 index 000000000..3db1a85c0 --- /dev/null +++ b/messages.go @@ -0,0 +1,87 @@ +package slack + +type OutgoingMessage struct { + Id int `json:"id"` + ChannelID string `json:"channel,omitempty"` + Text string `json:"text,omitempty"` + Type string `json:"type,omitempty"` +} + +type Message struct { + Msg + SubMessage Msg `json:"message,omitempty"` +} + +type Msg struct { + Id string `json:"id"` + UserID string `json:"user,omitempty"` + ChannelID string `json:"channel,omitempty"` + Timestamp string `json:"ts,omitempty"` + Text string `json:"text,omitempty"` + // Type may come if it's part of a message list + // e.g.: channel.history + Type string `json:"type,omitempty"` + IsStarred bool `json:"is_starred,omitempty"` + // Submessage + SubType string `json:"subtype,omitempty"` + Hidden bool `json:"bool,omitempty"` + DeletedTimestamp string `json:"deleted_ts,omitempty"` +} + +type Presence struct { + Presence string `json:"presence"` + UserID string `json:"user"` +} + +type Event struct { + Type string `json:"type,omitempty"` +} + +type Ping struct { + Id int `json:"id"` + Type string `json:"type"` +} + +type AckMessage struct { + Ok bool `json:"ok"` + ReplyTo int `json:"reply_to"` + Timestamp string `json:"ts"` + Text string `json:"text"` +} + +var message_id int + +func NewOutgoingMessage(text string, channel string) *OutgoingMessage { + message_id++ + return &OutgoingMessage{ + Id: message_id, + Type: "message", + ChannelID: channel, + Text: text, + } +} + +// XXX: maybe support variable arguments so that people +// can send stuff through their ping +func NewPing() *Ping { + message_id++ + return &Ping{Id: message_id, Type: "ping"} +} + +func (info Info) GetUserById(id string) *User { + for _, user := range info.Users { + if user.Id == id { + return &user + } + } + return nil +} + +func (info Info) GetChannelById(id string) *Channel { + for _, channel := range info.Channels { + if channel.Id == id { + return &channel + } + } + return nil +} diff --git a/slack.go b/slack.go new file mode 100644 index 000000000..4454c9a72 --- /dev/null +++ b/slack.go @@ -0,0 +1,155 @@ +package slack + +import ( + "encoding/json" + "errors" + "io" + "log" + "net/http" + "net/url" + "time" + + "golang.org/x/net/websocket" +) + +type UserTyping struct { + Type string `json:"type"` + UserID string `json:"user"` + ChannelID string `json:"channel"` +} + +type SlackEvent struct { + Type int + Data interface{} +} + +type SlackAPI struct { + config Config + conn *websocket.Conn + + info Info +} + +func New(token string) *SlackAPI { + return &SlackAPI{ + config: Config{token: token}, + } +} + +func (api *SlackAPI) GetInfo() Info { + return api.info +} + +func (api *SlackAPI) StartRTM(protocol string, origin string) error { + resp, err := http.PostForm(SLACK_API+"rtm.start", url.Values{"token": {api.config.token}}) + if err != nil { + return err + } + defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) + if err = decoder.Decode(&api.info); err != nil { + return err + } + if !api.info.Ok { + return errors.New(api.info.Error) + } + api.config.protocol, api.config.origin = protocol, origin + api.conn, err = websocket.Dial(api.info.Url, api.config.protocol, api.config.origin) + if err != nil { + return err + } + return nil +} + +func (api *SlackAPI) Ping() error { + if err := websocket.JSON.Send(api.conn, NewPing()); err != nil { + return err + } + return nil +} + +func (api *SlackAPI) SendMessage(msg OutgoingMessage) error { + if err := websocket.JSON.Send(api.conn, msg); err != nil { + return err + } + return nil +} + +func (api *SlackAPI) HandleIncomingEvents(ch *chan SlackEvent) { + event := json.RawMessage{} + for { + if err := websocket.JSON.Receive(api.conn, &event); err == io.EOF { + // should we reconnect here? + if !api.conn.IsClientConn() { + api.conn, err = websocket.Dial(api.info.Url, api.config.protocol, api.config.origin) + if err != nil { + log.Panic(err) + } + } + // XXX: check for timeout and implement exponential backoff + } else if err != nil { + log.Panic(err) + } + if len(event) == 0 { + log.Println("Event Empty. WTF?") + } else { + handle_event(ch, event) + } + time.Sleep(time.Millisecond * 500) + } +} + +func handle_event(ch *chan SlackEvent, event json.RawMessage) { + em := Event{} + err := json.Unmarshal(event, &em) + if err != nil { + log.Fatal(err) + } + switch em.Type { + case "": + // try ok + ack := AckMessage{} + if err = json.Unmarshal(event, &ack); err != nil { + log.Fatal(err) + } + if ack.Ok { + log.Printf("Received an ok for: %d", ack.ReplyTo) + } else { + log.Println(event) + log.Println("XXX: ?") + } + case "hello": + return + case "pong": + // XXX: Eventually check to which ping this matched with + // Allows us to have stats about latency and what not + return + case "presence_change": + //log.Printf("`%s is %s`\n", info.GetUserById(event.PUserID).Name, event.Presence) + case "message": + handle_message(ch, event) + case "channel_marked": + log.Printf("XXX: To implement %s", em) + case "user_typing": + handle_user_typing(ch, event) + default: + log.Println("XXX: " + string(event)) + } +} + +func handle_user_typing(ch *chan SlackEvent, event json.RawMessage) { + msg := UserTyping{} + if err := json.Unmarshal(event, &msg); err != nil { + log.Fatal(err) + } + *ch <- SlackEvent{Type: EV_USER_TYPING, Data: msg} +} + +func handle_message(ch *chan SlackEvent, event json.RawMessage) { + msg := Message{} + err := json.Unmarshal(event, &msg) + if err != nil { + log.Fatal(err) + } + *ch <- SlackEvent{Type: EV_MESSAGE, Data: msg} +}