From 755cfc38cd80c8d570fcc65df74a3bfe3140e160 Mon Sep 17 00:00:00 2001 From: cws Date: Tue, 25 Jul 2017 13:58:04 -0400 Subject: [PATCH] slack: mark channels read, keep a current marker --- bot/bot.go | 21 +---- config/config.go | 34 ++++++++- slack/slack.go | 195 +++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 205 insertions(+), 45 deletions(-) diff --git a/bot/bot.go b/bot/bot.go index e2f958c..7c5c1a6 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -7,11 +7,9 @@ import ( "html/template" "log" "net/http" - "regexp" "strings" "github.com/jmoiron/sqlx" - "github.com/mattn/go-sqlite3" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msglog" "github.com/velour/catbase/bot/user" @@ -54,25 +52,8 @@ type Variable struct { Variable, Value string } -func init() { - regex := func(re, s string) (bool, error) { - return regexp.MatchString(re, s) - } - sql.Register("sqlite3_custom", - &sqlite3.SQLiteDriver{ - ConnectHook: func(conn *sqlite3.SQLiteConn) error { - return conn.RegisterFunc("REGEXP", regex, true) - }, - }) -} - // Newbot creates a bot for a given connection and set of handlers. func New(config *config.Config, connector Connector) Bot { - sqlDB, err := sqlx.Open("sqlite3_custom", config.DB.File) - if err != nil { - log.Fatal(err) - } - logIn := make(chan msg.Message) logOut := make(chan msg.Messages) @@ -91,7 +72,7 @@ func New(config *config.Config, connector Connector) Bot { conn: connector, users: users, me: users[0], - db: sqlDB, + db: config.DBConn, logIn: logIn, logOut: logOut, version: config.Version, diff --git a/config/config.go b/config/config.go index 7a0553d..063be8f 100644 --- a/config/config.go +++ b/config/config.go @@ -2,13 +2,23 @@ package config -import "encoding/json" -import "fmt" -import "io/ioutil" +import ( + "database/sql" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "regexp" + + "github.com/jmoiron/sqlx" + sqlite3 "github.com/mattn/go-sqlite3" +) // Config stores any system-wide startup information that cannot be easily configured via // the database type Config struct { + DBConn *sqlx.DB + DB struct { File string Name string @@ -81,6 +91,18 @@ type Config struct { } } +func init() { + regex := func(re, s string) (bool, error) { + return regexp.MatchString(re, s) + } + sql.Register("sqlite3_custom", + &sqlite3.SQLiteDriver{ + ConnectHook: func(conn *sqlite3.SQLiteConn) error { + return conn.RegisterFunc("REGEXP", regex, true) + }, + }) +} + // Readconfig loads the config data out of a JSON file located in cfile func Readconfig(version, cfile string) *Config { fmt.Printf("Using %s as config file.\n", cfile) @@ -102,5 +124,11 @@ func Readconfig(version, cfile string) *Config { fmt.Printf("godeepintir version %s running.\n", c.Version) + sqlDB, err := sqlx.Open("sqlite3_custom", c.DB.File) + if err != nil { + log.Fatal(err) + } + c.DBConn = sqlDB + return &c } diff --git a/slack/slack.go b/slack/slack.go index e688fa4..d6f8ace 100644 --- a/slack/slack.go +++ b/slack/slack.go @@ -32,6 +32,8 @@ type Slack struct { id string ws *websocket.Conn + lastRecieved time.Time + users map[string]string emoji map[string]string @@ -50,22 +52,74 @@ type slackUserInfoResp struct { } `json:"user"` } +type slackChannelListItem struct { + ID string `json:"id"` + Name string `json:"name"` + IsChannel bool `json:"is_channel"` + Created int `json:"created"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + IsGeneral bool `json:"is_general"` + NameNormalized string `json:"name_normalized"` + IsShared bool `json:"is_shared"` + IsOrgShared bool `json:"is_org_shared"` + IsMember bool `json:"is_member"` + Members []string `json:"members"` + Topic struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet int `json:"last_set"` + } `json:"topic"` + Purpose struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet int `json:"last_set"` + } `json:"purpose"` + PreviousNames []interface{} `json:"previous_names"` + NumMembers int `json:"num_members"` +} + +type slackChannelListResp struct { + Ok bool `json:"ok"` + Channels []slackChannelListItem `json:"channels"` +} + type slackChannelInfoResp struct { Ok bool `json:"ok"` Channel struct { - ID string `json:"id"` - Name string `json:"name"` - - Created int64 `json:"created"` - Creator string `json:"creator"` - - Members []string `json:"members"` - - Topic struct { + ID string `json:"id"` + Name string `json:"name"` + IsChannel bool `json:"is_channel"` + Created int `json:"created"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + IsGeneral bool `json:"is_general"` + NameNormalized string `json:"name_normalized"` + IsReadOnly bool `json:"is_read_only"` + IsShared bool `json:"is_shared"` + IsOrgShared bool `json:"is_org_shared"` + IsMember bool `json:"is_member"` + LastRead string `json:"last_read"` + Latest struct { + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + Ts string `json:"ts"` + } `json:"latest"` + UnreadCount int `json:"unread_count"` + UnreadCountDisplay int `json:"unread_count_display"` + Members []string `json:"members"` + Topic struct { Value string `json:"value"` Creator string `json:"creator"` LastSet int64 `json:"last_set"` } `json:"topic"` + Purpose struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet int `json:"last_set"` + } `json:"purpose"` + PreviousNames []string `json:"previous_names"` } `json:"channel"` } @@ -102,9 +156,10 @@ type rtmStart struct { func New(c *config.Config) *Slack { return &Slack{ - config: c, - users: make(map[string]string), - emoji: make(map[string]string), + config: c, + lastRecieved: time.Now(), + users: make(map[string]string), + emoji: make(map[string]string), } } @@ -194,11 +249,18 @@ func (s *Slack) receiveMessage() (slackMessage, error) { log.Println("Error decoding WS message") return m, err } - log.Printf("Raw response from Slack: %s", msg) err2 := json.Unmarshal(msg, &m) return m, err2 } +// I think it's horseshit that I have to do this +func slackTStoTime(t string) time.Time { + ts := strings.Split(t, ".") + sec, _ := strconv.ParseInt(ts[0], 10, 64) + nsec, _ := strconv.ParseInt(ts[1], 10, 64) + return time.Unix(sec, nsec) +} + func (s *Slack) Serve() { s.connect() s.populateEmojiList() @@ -212,9 +274,14 @@ func (s *Slack) Serve() { } switch msg.Type { case "message": - log.Printf("msg: %+v", msg) if !msg.Hidden { - s.messageReceived(s.buildMessage(msg)) + m := s.buildMessage(msg) + if m.Time.Before(s.lastRecieved) { + log.Printf("Ignoring message: %+v\nlastRecieved: %v msg: %v", msg.ID, s.lastRecieved, m.Time) + } else { + s.lastRecieved = m.Time + s.messageReceived(s.buildMessage(msg)) + } } else { log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID) } @@ -225,7 +292,9 @@ func (s *Slack) Serve() { case "presence_change": case "user_typing": case "reconnect_url": + case "desktop_notification": // squeltch this stuff + continue default: log.Printf("Unhandled Slack message type: '%s'", msg.Type) } @@ -236,7 +305,6 @@ var urlDetector = regexp.MustCompile(`<(.+)://([^|^>]+).*>`) // Convert a slackMessage to a msg.Message func (s *Slack) buildMessage(m slackMessage) msg.Message { - log.Printf("DEBUG: msg: %#v", m) text := html.UnescapeString(m.Text) // remove <> from URLs, URLs may also be @@ -254,11 +322,7 @@ func (s *Slack) buildMessage(m slackMessage) msg.Message { u = m.Username } - // I think it's horseshit that I have to do this - ts := strings.Split(m.Ts, ".") - sec, _ := strconv.ParseInt(ts[0], 10, 64) - nsec, _ := strconv.ParseInt(ts[1], 10, 64) - tstamp := time.Unix(sec, nsec) + tstamp := slackTStoTime(m.Ts) return msg.Message{ User: &user.User{ @@ -278,9 +342,94 @@ func (s *Slack) buildMessage(m slackMessage) msg.Message { } } +// markAllChannelsRead gets a list of all channels and marks each as read +func (s *Slack) markAllChannelsRead() { + chs := s.getAllChannels() + log.Printf("Got list of channels to mark read: %+v", chs) + for _, ch := range chs { + s.markChannelAsRead(ch.ID) + } + log.Printf("Finished marking channels read") +} + +// getAllChannels returns info for all channels joined +func (s *Slack) getAllChannels() []slackChannelListItem { + u := s.url + "channels.list" + resp, err := http.PostForm(u, + url.Values{"token": {s.config.Slack.Token}}) + if err != nil { + log.Printf("Error posting user info request: %s", + err) + return nil + } + if resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d", + resp.StatusCode) + return nil + } + defer resp.Body.Close() + var chanInfo slackChannelListResp + err = json.NewDecoder(resp.Body).Decode(&chanInfo) + if err != nil || !chanInfo.Ok { + log.Println("Error decoding response: ", err) + return nil + } + return chanInfo.Channels +} + +// markAsRead marks a channel read +func (s *Slack) markChannelAsRead(slackChanId string) error { + u := s.url + "channels.info" + resp, err := http.PostForm(u, + url.Values{"token": {s.config.Slack.Token}, "channel": {slackChanId}}) + if err != nil { + log.Printf("Error posting user info request: %s", + err) + return err + } + if resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d", + resp.StatusCode) + return err + } + defer resp.Body.Close() + var chanInfo slackChannelInfoResp + err = json.NewDecoder(resp.Body).Decode(&chanInfo) + log.Printf("%+v, %+v", err, chanInfo) + if err != nil || !chanInfo.Ok { + log.Println("Error decoding response: ", err) + return err + } + + u = s.url + "channels.mark" + resp, err = http.PostForm(u, + url.Values{"token": {s.config.Slack.Token}, "channel": {slackChanId}, "ts": {chanInfo.Channel.Latest.Ts}}) + if err != nil { + log.Printf("Error posting user info request: %s", + err) + return err + } + if resp.StatusCode != 200 { + log.Printf("Error posting user info request: %d", + resp.StatusCode) + return err + } + defer resp.Body.Close() + var markInfo map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&markInfo) + log.Printf("%+v, %+v", err, markInfo) + if err != nil { + log.Println("Error decoding response: ", err) + return err + } + + log.Printf("Marked %s as read", slackChanId) + return nil +} + func (s *Slack) connect() { token := s.config.Slack.Token - url := fmt.Sprintf("https://slack.com/api/rtm.start?token=%s", token) + url := fmt.Sprintf("https://slack.com/api/rtm.connect?token=%s", token) resp, err := http.Get(url) if err != nil { return @@ -306,6 +455,8 @@ func (s *Slack) connect() { s.url = "https://slack.com/api/" s.id = rtm.Self.ID + s.markAllChannelsRead() + s.ws, err = websocket.Dial(rtm.URL, "", s.url) if err != nil { log.Fatal(err)