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 abd3a8e..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,29 +52,82 @@ 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"` } type slackMessage struct { - Id uint64 `json:"id"` + ID uint64 `json:"id"` Type string `json:"type"` SubType string `json:"subtype"` + Hidden bool `json:"hidden"` Channel string `json:"channel"` Text string `json:"text"` User string `json:"user"` @@ -85,25 +140,26 @@ type slackMessage struct { } type slackReaction struct { - Reaction string `json:"name"` - Channel string `json:"channel"` - Timestamp float64 `json:"timestamp"` + Reaction string `json:"name"` + Channel string `json:"channel"` + Timestamp float64 `json:"timestamp"` } type rtmStart struct { Ok bool `json:"ok"` Error string `json:"error"` - Url string `json:"url"` + URL string `json:"url"` Self struct { - Id string `json:"id"` + ID string `json:"id"` } `json:"self"` } 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), } } @@ -117,7 +173,7 @@ func (s *Slack) RegisterMessageReceived(f func(msg.Message)) { func (s *Slack) SendMessageType(channel, messageType, subType, message string) error { m := slackMessage{ - Id: atomic.AddUint64(&idCounter, 1), + ID: atomic.AddUint64(&idCounter, 1), Type: messageType, SubType: subType, Channel: channel, @@ -143,10 +199,10 @@ func (s *Slack) SendAction(channel, message string) { func (s *Slack) React(channel, reaction string, message msg.Message) { log.Printf("Reacting in %s: %s", channel, reaction) resp, err := http.PostForm("https://slack.com/api/reactions.add", - url.Values{ "token": {s.config.Slack.Token}, - "name": {reaction}, - "channel": {channel}, - "timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}}) + url.Values{"token": {s.config.Slack.Token}, + "name": {reaction}, + "channel": {channel}, + "timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}}) if err != nil { log.Printf("Error sending Slack reaction: %s", err) } @@ -159,7 +215,7 @@ func (s *Slack) GetEmojiList() map[string]string { func (s *Slack) populateEmojiList() { resp, err := http.PostForm("https://slack.com/api/emoji.list", - url.Values{ "token": {s.config.Slack.Token}}) + url.Values{"token": {s.config.Slack.Token}}) if err != nil { log.Printf("Error retrieving emoji list from Slack: %s", err) return @@ -172,7 +228,7 @@ func (s *Slack) populateEmojiList() { } type EmojiListResponse struct { - OK bool `json:ok` + OK bool `json:ok` Emoji map[string]string `json:emoji` } @@ -193,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() @@ -211,7 +274,17 @@ func (s *Slack) Serve() { } switch msg.Type { case "message": - s.messageReceived(s.buildMessage(msg)) + if !msg.Hidden { + 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) + } case "error": log.Printf("Slack error, code: %d, message: %s", msg.Error.Code, msg.Error.Msg) case "": // what even is this? @@ -219,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) } @@ -230,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 @@ -248,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{ @@ -264,7 +334,7 @@ func (s *Slack) buildMessage(m slackMessage) msg.Message { Channel: m.Channel, Command: isCmd, Action: isAction, - Host: string(m.Id), + Host: string(m.ID), Time: tstamp, AdditionalData: map[string]string{ "RAW_SLACK_TIMESTAMP": m.Ts, @@ -272,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 @@ -298,9 +453,11 @@ func (s *Slack) connect() { } s.url = "https://slack.com/api/" - s.id = rtm.Self.Id + s.id = rtm.Self.ID - s.ws, err = websocket.Dial(rtm.Url, "", s.url) + s.markAllChannelsRead() + + s.ws, err = websocket.Dial(rtm.URL, "", s.url) if err != nil { log.Fatal(err) }