diff --git a/processor_general.go b/processor_general.go index 90910b0..9a7dad4 100644 --- a/processor_general.go +++ b/processor_general.go @@ -18,8 +18,9 @@ func (p *generalProcessor) GetHelp() string { return "" } -func (p *generalProcessor) ProcessAdminMessage(slack *Slack, m *Message) {} func (p *generalProcessor) ProcessMessage(slack *Slack, m *Message) {} +func (p *generalProcessor) ProcessBotMessage(slack *Slack, m *Message) {} +func (p *generalProcessor) ProcessAdminMessage(slack *Slack, m *Message) {} func (p *generalProcessor) ProcessUserMessage(slack *Slack, m *Message) {} func (p *generalProcessor) ProcessAdminUserMessage(slack *Slack, m *Message) { @@ -68,9 +69,11 @@ func (p *generalProcessor) ProcessAdminUserMessage(slack *Slack, m *Message) { } } } +func (p *generalProcessor) ProcessBotUserMessage(slack *Slack, m *Message) {} func (p *generalProcessor) ProcessChannelMessage(slack *Slack, m *Message) {} func (p *generalProcessor) ProcessAdminChannelMessage(slack *Slack, m *Message) {} +func (p *generalProcessor) ProcessBotChannelMessage(slack *Slack, m *Message) {} /* *General Statistics Processor @@ -96,15 +99,18 @@ func (p *generalStatProcessor) ProcessMessage(m *Message) { incrementUserStat(m.User, "message-dow-"+m.Time.Format("Mon")) incrementUserStat(m.User, "message-dom-"+m.Time.Format("_2")) } +func (p *generalStatProcessor) ProcessBotMessage(m *Message) {} func (p *generalStatProcessor) ProcessUserMessage(m *Message) { incrementUserStat(m.User, "bot-message") } +func (p *generalStatProcessor) ProcessBotUserMessage(m *Message) {} func (p *generalStatProcessor) ProcessChannelMessage(m *Message) { incrementUserStat(m.User, "channel-message") - incrementChannelStat(m.User, "message-hour-"+m.Time.Format("15")) - incrementChannelStat(m.User, "message-dow-"+m.Time.Format("Mon")) - incrementChannelStat(m.User, "message-dom-"+m.Time.Format("_2")) + incrementChannelStat(m.Channel, "message-hour-"+m.Time.Format("15")) + incrementChannelStat(m.Channel, "message-dow-"+m.Time.Format("Mon")) + incrementChannelStat(m.Channel, "message-dom-"+m.Time.Format("_2")) } +func (p *generalStatProcessor) ProcessBotChannelMessage(m *Message) {} diff --git a/processor_levelup.go b/processor_levelup.go index bcc0c77..356eada 100644 --- a/processor_levelup.go +++ b/processor_levelup.go @@ -12,9 +12,13 @@ func (p *levelUpStatProcessor) GetStatKeys() []string { } } -func (p *levelUpStatProcessor) ProcessMessage(m *Message) {} +func (p *levelUpStatProcessor) ProcessMessage(m *Message) {} +func (p *levelUpStatProcessor) ProcessAdminMessage(m *Message) {} +func (p *levelUpStatProcessor) ProcessBotMessage(m *Message) {} -func (p *levelUpStatProcessor) ProcessUserMessage(m *Message) {} +func (p *levelUpStatProcessor) ProcessUserMessage(m *Message) {} +func (p *levelUpStatProcessor) ProcessAdminUserMessage(m *Message) {} +func (p *levelUpStatProcessor) ProcessBotUserMessage(m *Message) {} func (p *levelUpStatProcessor) ProcessChannelMessage(m *Message) { // levelup XP, for now, is awarded like this: @@ -33,3 +37,5 @@ func (p *levelUpStatProcessor) ProcessChannelMessage(m *Message) { } } } +func (p *levelUpStatProcessor) ProcessAdminChannelMessage(m *Message) {} +func (p *levelUpStatProcessor) ProcessBotChannelMessage(m *Message) {} diff --git a/slack.go b/slack.go index 2e3b798..0e88d95 100644 --- a/slack.go +++ b/slack.go @@ -5,8 +5,6 @@ import ( "fmt" "io/ioutil" "net/http" - "strconv" - "strings" "sync/atomic" "time" @@ -86,11 +84,7 @@ func (s *Slack) getMessage() (Message, error) { var m Message err := websocket.JSON.Receive(s.socket, &m) - txtArr := strings.Split(m.Ts, ".") - if t, err := strconv.Atoi(txtArr[0]); err == nil { - rawts := int64(t) - m.Time = time.Unix(0, rawts*1000000000) - } + m.Time = convertSlackTimestamp(m.Ts) return m, err } @@ -153,6 +147,9 @@ func (s *Slack) getChannelInfo(cid string) (*Channel, error) { err = fmt.Errorf("Slack error: %s", respObj.Error) return nil, err } + + respObj.Channel.LastRead = convertSlackTimestamp(respObj.Channel.LastReadRaw) + return respObj.Channel, nil } diff --git a/statbot.go b/statbot.go index 8dd708f..d3e0e97 100644 --- a/statbot.go +++ b/statbot.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "os" "strings" @@ -15,10 +14,13 @@ type messageProcessor interface { GetHelp() string ProcessMessage(s *Slack, m *Message) ProcessAdminMessage(s *Slack, m *Message) + ProcessBotMessage(s *Slack, m *Message) ProcessUserMessage(s *Slack, m *Message) ProcessAdminUserMessage(s *Slack, m *Message) + ProcessBotUserMessage(s *Slack, m *Message) ProcessChannelMessage(s *Slack, m *Message) ProcessAdminChannelMessage(s *Slack, m *Message) + ProcessBotChannelMessage(s *Slack, m *Message) } var messageProcessors []messageProcessor @@ -27,8 +29,11 @@ type statProcessor interface { GetName() string GetStatKeys() []string ProcessMessage(m *Message) + ProcessBotMessage(m *Message) ProcessUserMessage(m *Message) + ProcessBotUserMessage(m *Message) ProcessChannelMessage(m *Message) + ProcessBotChannelMessage(m *Message) } var statProcessors []statProcessor @@ -77,61 +82,84 @@ func statBotMain(slack *Slack) { } func processMessage(slack *Slack, m *Message) { - if mb, me := json.Marshal(m); me == nil { - // Write the JSON representation to the log - writeToLog(string(mb) + "\n") - } + /* + if mb, me := json.Marshal(m); me == nil { + // Write the JSON representation to the log + writeToLog(string(mb) + "\n") + } + */ // TODO: Handle reaction_added messages // TODO: Handle reaction_removed messages if m.Type == "message" { - for _, proc := range statProcessors { - proc.ProcessMessage(m) - } - for _, proc := range messageProcessors { - if isAdmin(m.User) { - proc.ProcessAdminMessage(slack, m) - } - proc.ProcessMessage(slack, m) - } + var err error + var usr *User // Check if we know who the user is - usr, err := getUserInfo(m.User) + usr, err = getUserInfo(m.User) // If the user information hasn't been updated in the last day, update it. if err != nil || usr.LastUpdated.IsZero() || time.Since(usr.LastUpdated) > (time.Hour*24) { if u, ue := slack.getUserInfo(m.User); ue == nil { saveUserInfo(u) } } + + for _, stats := range statProcessors { + if err == nil { + if usr.IsBot { + stats.ProcessBotMessage(m) + } else { + stats.ProcessMessage(m) + } + } + } + for _, proc := range messageProcessors { + if isAdmin(m.User) { + proc.ProcessAdminMessage(slack, m) + } + if usr.IsBot { + proc.ProcessBotMessage(slack, m) + } else { + proc.ProcessMessage(slack, m) + } + } + if m.Channel != "" { // Check if we know what the channel is chnl, err := getChannelInfo(m.Channel) + var isDirectMessage bool + // If the channel information hasn't been updated in the last day, update it. if err != nil || chnl.LastUpdated.IsZero() || time.Since(chnl.LastUpdated) > (time.Hour*24) { // Either we don't have this channel, or it's a direct message - if c, ce := slack.getChannelInfo(m.Channel); ce != nil { - // Invalid channel, save as a direct message - saveUserMessage(m.User, m) - for _, proc := range statProcessors { - proc.ProcessUserMessage(m) - } - for _, proc := range messageProcessors { - if isAdmin(m.User) { - proc.ProcessAdminUserMessage(slack, m) - } - proc.ProcessUserMessage(slack, m) - } - } else { + if c, ce := slack.getChannelInfo(m.Channel); ce == nil { // Save channel info saveChannelInfo(c) - // And save the channel message - saveChannelMessage(m.Channel, m) - for _, proc := range statProcessors { - proc.ProcessChannelMessage(m) - } - for _, proc := range messageProcessors { - proc.ProcessChannelMessage(slack, m) + } else { + isDirectMessage = true + } + } + if isDirectMessage { + // Invalid channel, save as a direct message + saveUserMessage(m.User, m) + for _, stats := range statProcessors { + stats.ProcessUserMessage(m) + } + for _, proc := range messageProcessors { + if isAdmin(m.User) { + proc.ProcessAdminUserMessage(slack, m) } + proc.ProcessUserMessage(slack, m) + } + } else { + // And save the channel message + err = saveChannelMessage(m.Channel, m) + + for _, proc := range statProcessors { + proc.ProcessChannelMessage(m) + } + for _, proc := range messageProcessors { + proc.ProcessChannelMessage(slack, m) } } } diff --git a/statbot_model.go b/statbot_model.go index cc176ab..e19e827 100644 --- a/statbot_model.go +++ b/statbot_model.go @@ -3,6 +3,7 @@ package main import ( "fmt" "strconv" + "strings" "time" "github.com/boltdb/bolt" @@ -69,6 +70,23 @@ func initDatabase() error { return err } +func isBot(uid string) (bool, error) { + openDatabase() + var ret bool + err := db.View(func(tx *bolt.Tx) error { + var b, uB *bolt.Bucket + var err error + b = tx.Bucket([]byte("users")) + if b == nil { + return fmt.Errorf("Unable to open 'users' bucket") + } + uB = b.Bucket([]byte(uid)) + ret, err = bktGetBool(uB, "is_bot") + return err + }) + return ret, err +} + func isAdmin(uid string) bool { var foundUser bool openDatabase() @@ -165,7 +183,7 @@ type Channel struct { IsArchived bool `json:"is_archived"` IsGeneral bool `json:"is_general"` IsMember bool `json:"is_member"` - LastReadRaw int `json:"last_read"` + LastReadRaw string `json:"last_read"` LastRead time.Time Latest *Message `json:"latest"` UnreadCount int `json:"unread_count"` @@ -327,6 +345,7 @@ func saveChannelInfo(chnl *Channel) error { if err = bktPutString(chMembersB, idxKey, mm); err != nil { return err } + mbIdx++ } if chnl.Topic != nil { if err = bktPutString(chIB, "topic_value", chnl.Topic.Value); err != nil { @@ -357,56 +376,59 @@ func saveChannelInfo(chnl *Channel) error { return err } -func saveChannelStat(channel string, key string, val string) error { +func saveChannelStat(channel string, key string, val int) error { openDatabase() err := db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("channels")) - if chB, err := b.CreateBucketIfNotExists([]byte(channel)); err == nil { - chB.Put([]byte(key), []byte(val)) + var b, chB, chSB *bolt.Bucket + var err error + + b = tx.Bucket([]byte("channels")) + if chB, err = b.CreateBucketIfNotExists([]byte(channel)); err == nil { + if chSB, err = chB.CreateBucketIfNotExists([]byte("stats")); err == nil { + err = bktPutInt(chSB, key, val) + } } - return nil + return err }) closeDatabase() return err } -func incrementChannelStat(channel string, key string) error { +func addChannelStat(channel string, key string, addVal int) error { openDatabase() - strRet, err := getChannelStat(channel, key) - if err == nil { - if i, err := strconv.Atoi(strRet); err == nil { - i++ - return saveChannelStat(channel, key, fmt.Sprintf("%d", i)) - } - return err - } + v, err := getChannelStat(channel, key) + err = saveChannelStat(channel, key, v+addVal) closeDatabase() return err } +func incrementChannelStat(channel string, key string) error { + return addChannelStat(channel, key, 1) +} + func decrementChannelStat(channel string, key string) error { - openDatabase() - strRet, err := getChannelStat(channel, key) - if err == nil { - if i, err := strconv.Atoi(strRet); err == nil { - i-- - return saveChannelStat(channel, key, fmt.Sprintf("%d", i)) - } - return err - } - closeDatabase() - return err + return addChannelStat(channel, key, -1) } -func getChannelStat(channel string, key string) (string, error) { +func getChannelStat(channel string, key string) (int, error) { openDatabase() - var ret string + var ret int err := db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("channels")) - if chB := b.Bucket([]byte(channel)); chB != nil { - ret = string(chB.Get([]byte(key))) + var b, chB, chSB *bolt.Bucket + var err error + + b = tx.Bucket([]byte("channels")) + if b == nil { + return fmt.Errorf("Unable to open 'channels' bucket") } - return nil + if chB, err = b.CreateBucketIfNotExists([]byte(channel)); err == nil { + if chSB, err = chB.CreateBucketIfNotExists([]byte("stats")); err == nil { + ret, err = bktGetInt(chSB, key) + return err + } + } + return err + }) closeDatabase() return ret, err @@ -641,7 +663,7 @@ func saveUserMessage(user string, message *Message) error { func getUserStat(user string, key string) (int, error) { openDatabase() var ret int - err := db.Update(func(tx *bolt.Tx) error { + err := db.View(func(tx *bolt.Tx) error { var b, uB, uSB *bolt.Bucket var err error @@ -651,10 +673,6 @@ func getUserStat(user string, key string) (int, error) { } if uB, err = b.CreateBucketIfNotExists([]byte(user)); err == nil { if uSB, err = uB.CreateBucketIfNotExists([]byte("stats")); err == nil { - uSB.ForEach(func(k, v []byte) error { - return nil - }) - ret, err = bktGetInt(uSB, key) return err } @@ -674,12 +692,10 @@ func saveUserStat(user string, key string, val int) error { b = tx.Bucket([]byte("users")) if uB, err = b.CreateBucketIfNotExists([]byte(user)); err == nil { if uSB, err = uB.CreateBucketIfNotExists([]byte("stats")); err == nil { - if err = bktPutInt(uSB, key, val); err != nil { - return err - } + err = bktPutInt(uSB, key, val) } } - return nil + return err }) closeDatabase() return err @@ -763,6 +779,16 @@ func bktPutTime(b *bolt.Bucket, key string, val time.Time) error { return b.Put([]byte(key), []byte(bs)) } +func convertSlackTimestamp(ts string) time.Time { + var ret time.Time + txtArr := strings.Split(ts, ".") + if t, err := strconv.Atoi(txtArr[0]); err == nil { + rawts := int64(t) + ret = time.Unix(0, rawts*1000000000) + } + return ret +} + // All user stats are saved in the db like so: // users (bucket) // |- username1 (bucket)