package main import ( "fmt" "strconv" "strings" "time" "github.com/boltdb/bolt" ) const databaseFile = programName + ".db" var db *bolt.DB var dbOpened bool func openDatabase() error { if !dbOpened { var err error db, err = bolt.Open(databaseFile, 0600, nil) if err != nil { return err } dbOpened = true } return nil } func closeDatabase() error { if !dbOpened { return nil } dbOpened = false return db.Close() } func initDatabase() error { openDatabase() // Make sure the needed buckets exists err := db.Update(func(tx *bolt.Tx) error { // The config bucket holds config info for statbot cB, err := tx.CreateBucketIfNotExists([]byte("config")) if err != nil { return err } _, err = cB.CreateBucketIfNotExists([]byte("admins")) if err != nil { return err } // The 'users' bucket holds user info, including direct messages _, err = tx.CreateBucketIfNotExists([]byte("users")) if err != nil { return err } // The 'channels' bucket holds channel info, including channel messages _, err = tx.CreateBucketIfNotExists([]byte("channels")) if err != nil { return err } return nil }) closeDatabase() 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() err := db.View(func(tx *bolt.Tx) error { var cB, caB *bolt.Bucket cB = tx.Bucket([]byte("config")) if cB == nil { return fmt.Errorf("Error opening 'config' bucket") } if caB = cB.Bucket([]byte("admins")); caB == nil { return fmt.Errorf("Error opening 'config/admins' bucket") } _, err := bktGetInt(caB, uid) return err }) closeDatabase() foundUser = err == nil return foundUser } func addAdmin(uid string) error { if !isAdmin(uid) { openDatabase() err := db.Update(func(tx *bolt.Tx) error { var err error var cB, caB *bolt.Bucket cB = tx.Bucket([]byte("config")) if cB == nil { return fmt.Errorf("Error opening 'config' bucket") } if caB = cB.Bucket([]byte("admins")); caB == nil { return fmt.Errorf("Error opening 'config/admins' bucket") } err = bktPutInt(caB, uid, 1) return err }) closeDatabase() return err } return fmt.Errorf("User is already an admin") } func removeAdmin(uid string) error { if isAdmin(uid) { openDatabase() err := db.Update(func(tx *bolt.Tx) error { var err error var cB, caB *bolt.Bucket cB = tx.Bucket([]byte("config")) if cB == nil { return fmt.Errorf("Error opening 'config' bucket") } if caB = cB.Bucket([]byte("admins")); caB == nil { return fmt.Errorf("Error opening 'config/admins' bucket") } // Find the admin level for the user to be removed // (assume it's a normal admin) adminLevel := 1 if adminLevel, err = bktGetInt(caB, uid); err != nil { return err } if adminLevel > 0 { return caB.Delete([]byte(uid)) } return fmt.Errorf("Unable to remove privileges. User's admin level is too high.") }) closeDatabase() return err } return fmt.Errorf("User is not an admin") } // Message We save the message struct into the db, // it's also what we send/receive from slack type Message struct { ID uint64 `json:"id"` Type string `json:"type"` Channel string `json:"channel"` User string `json:"user"` Name string `json:"name"` Text string `json:"text"` Ts string `json:"ts"` Time time.Time } // Channel object type Channel struct { ID string `json:"id"` Name string `json:"name"` IsChannel bool `json:"is_channel"` CreatedRaw int `json:"created"` Created time.Time Creator string `json:"creator"` IsArchived bool `json:"is_archived"` IsGeneral bool `json:"is_general"` IsMember bool `json:"is_member"` LastReadRaw string `json:"last_read"` LastRead time.Time Latest *Message `json:"latest"` UnreadCount int `json:"unread_count"` UnreadCountDisplay int `json:"unread_count_display"` Members []string `json:"members"` Topic *ChannelTopic `json:"topic"` Purpose *ChannelTopic `json:"purpose"` // LastUpdated is the last time we updated the info in the DB LastUpdated time.Time } // ChannelTopic A simple 'Channel Topic' object // used for several things in the slack api (channel topic/purpose, etc. type ChannelTopic struct { Value string `json:"value"` Creator string `json:"creator"` LastSetRaw int `json:"last_set"` LastSet time.Time } func getChannelInfo(chnl string) (*Channel, error) { openDatabase() ret := Channel{ID: chnl} err := db.View(func(tx *bolt.Tx) error { var b, chB, chIB *bolt.Bucket var err error b = tx.Bucket([]byte("channels")) if b == nil { return fmt.Errorf("Error opening 'channels' bucket") } if chB = b.Bucket([]byte(chnl)); chB == nil { return fmt.Errorf("Error opening channel bucket (%s)", chnl) } if chIB = chB.Bucket([]byte("info")); chIB == nil { return fmt.Errorf("Error opening channel info bucket (%s/info)", chnl) } if ret.Name, err = bktGetString(chIB, "name"); err != nil { return err } if ret.IsChannel, err = bktGetBool(chIB, "is_channel"); err != nil { return err } if ret.Created, err = bktGetTime(chIB, "created"); err != nil { return err } if ret.Creator, err = bktGetString(chIB, "creator"); err != nil { return err } if ret.IsArchived, err = bktGetBool(chIB, "is_archived"); err != nil { return err } if ret.IsGeneral, err = bktGetBool(chIB, "is_general"); err != nil { return err } if ret.IsMember, err = bktGetBool(chIB, "is_member"); err != nil { return err } if ret.LastRead, err = bktGetTime(chIB, "last_read"); err != nil { return err } if ret.UnreadCount, err = bktGetInt(chIB, "unread_count"); err != nil { return err } if ret.UnreadCountDisplay, err = bktGetInt(chIB, "unread_count_display"); err != nil { return err } // Get all of the members into a []string chMembersB := chIB.Bucket([]byte("members")) if chMembersB != nil { chMembersB.ForEach(func(k, v []byte) error { ret.Members = append(ret.Members, string(v)) return nil }) } ret.Topic = new(ChannelTopic) if ret.Topic.Value, err = bktGetString(chIB, "topic_value"); err != nil { return err } if ret.Topic.Creator, err = bktGetString(chIB, "topic_creator"); err != nil { return err } if ret.Topic.LastSet, err = bktGetTime(chIB, "topic_last_set"); err != nil { return err } ret.Purpose = new(ChannelTopic) if ret.Purpose.Value, err = bktGetString(chIB, "purpose_value"); err != nil { return err } if ret.Purpose.Creator, err = bktGetString(chIB, "purpose_creator"); err != nil { return err } if ret.Purpose.LastSet, err = bktGetTime(chIB, "purpose_last_set"); err != nil { return err } return nil }) closeDatabase() return &ret, err } func saveChannelInfo(chnl *Channel) error { openDatabase() err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("channels")) var err error var chB, chIB *bolt.Bucket if chB, err = b.CreateBucketIfNotExists([]byte(chnl.ID)); err != nil { return err } if chIB, err = chB.CreateBucketIfNotExists([]byte("info")); err != nil { return err } if err = bktPutString(chIB, "id", chnl.ID); err != nil { return err } if err = bktPutString(chIB, "name", chnl.Name); err != nil { return err } if err = bktPutBool(chIB, "is_channel", chnl.IsChannel); err != nil { return err } if err = bktPutTime(chIB, "created", chnl.Created); err != nil { return err } if err = bktPutString(chIB, "creator", chnl.Creator); err != nil { return err } if err = bktPutBool(chIB, "is_archived", chnl.IsArchived); err != nil { return err } if err = bktPutBool(chIB, "is_general", chnl.IsGeneral); err != nil { return err } if err = bktPutBool(chIB, "is_member", chnl.IsMember); err != nil { return err } if err = bktPutTime(chIB, "last_read", chnl.LastRead); err != nil { return err } if err = bktPutInt(chIB, "unread_count", chnl.UnreadCount); err != nil { return err } if err = bktPutInt(chIB, "unread_count_display", chnl.UnreadCountDisplay); err != nil { return err } chMembersB := chIB.Bucket([]byte("members")) if chMembersB != nil { // Recreate the Members bucket chIB.DeleteBucket([]byte("members")) } if chMembersB, err = chIB.CreateBucket([]byte("members")); err != nil { return err } mbIdx := 0 for _, mm := range chnl.Members { idxKey := strconv.Itoa(mbIdx) 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 { return err } if err = bktPutString(chIB, "topic_creator", chnl.Topic.Creator); err != nil { return err } if err = bktPutTime(chIB, "topic_last_set", chnl.Topic.LastSet); err != nil { return err } } if chnl.Purpose != nil { if err = bktPutString(chIB, "purpose_value", chnl.Topic.Value); err != nil { return err } if err = bktPutString(chIB, "purpose_creator", chnl.Topic.Creator); err != nil { return err } if err = bktPutTime(chIB, "purpose_last_set", chnl.Topic.LastSet); err != nil { return err } } err = bktPutTime(chIB, "last_updated", chnl.LastUpdated) return err }) closeDatabase() return err } func saveChannelStat(channel string, key string, val int) error { openDatabase() err := db.Update(func(tx *bolt.Tx) error { 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 err }) closeDatabase() return err } func addChannelStat(channel string, key string, addVal int) error { openDatabase() 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 { return addChannelStat(channel, key, -1) } func getChannelName(chnl string) string { var ret string openDatabase() db.View(func(tx *bolt.Tx) error { var b, chB, chIB *bolt.Bucket var err error b = tx.Bucket([]byte("channels")) if b == nil { return fmt.Errorf("Error opening 'channels' bucket") } if chB = b.Bucket([]byte(chnl)); chB == nil { return fmt.Errorf("Error opening channel bucket (%s)", chnl) } if chIB = chB.Bucket([]byte("info")); chIB == nil { return fmt.Errorf("Error opening channel info bucket (%s/info)", chnl) } ret, err = bktGetString(chIB, "name") return err }) closeDatabase() return ret } func getChannelMemberCount(chnl string) int { var ret int openDatabase() db.View(func(tx *bolt.Tx) error { var b, chB, chIB *bolt.Bucket b = tx.Bucket([]byte("channels")) if b == nil { return fmt.Errorf("Error opening 'channels' bucket") } if chB = b.Bucket([]byte(chnl)); chB == nil { return fmt.Errorf("Error opening channel bucket (%s)", chnl) } if chIB = chB.Bucket([]byte("info")); chIB == nil { return fmt.Errorf("Error opening channel info bucket (%s/info)", chnl) } // Get all of the members into a []string chMembersB := chIB.Bucket([]byte("members")) ret = chMembersB.Stats().KeyN return nil }) closeDatabase() return ret } func getChannelStat(channel string, key string) (int, error) { openDatabase() var ret int err := db.View(func(tx *bolt.Tx) error { 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") } if chB = b.Bucket([]byte(channel)); chB != nil { if chSB = chB.Bucket([]byte("stats")); chSB != nil { ret, err = bktGetInt(chSB, key) return err } fmt.Println("Unable to find channel stats bucket: " + string(channel)) } else { fmt.Println("Unable to find channel bucket: " + string(channel)) } return err }) closeDatabase() return ret, err } func saveChannelMessage(channel string, message *Message) error { openDatabase() err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("channels")) var err error var chB, chMB, msgBkt *bolt.Bucket if chB, err = b.CreateBucketIfNotExists([]byte(channel)); err != nil { return err } if chMB, err = chB.CreateBucketIfNotExists([]byte("messages")); err != nil { return err } idx, _ := chMB.NextSequence() idxKey := []byte(strconv.FormatUint(idx, 10)) if msgBkt, err = chMB.CreateBucketIfNotExists(idxKey); err != nil { return err } if err = bktPutString(msgBkt, "type", message.Type); err != nil { return err } if err = bktPutString(msgBkt, "user", message.User); err != nil { return err } if err = bktPutString(msgBkt, "text", message.Text); err != nil { return err } if err = bktPutString(msgBkt, "name", message.Name); err != nil { return err } if err = bktPutTime(msgBkt, "time", message.Time); err != nil { return err } return nil }) closeDatabase() return err } // User object type User struct { ID string `json:"id"` Name string `json:"name"` Deleted bool `json:"deleted"` Status string `json:"status"` Color string `json:"color"` RealName string `json:"real_name"` TZ string `json:"tz"` TZLabel string `json:"tz_label"` TZOffset int `json:"tz_offset"` IsAdmin bool `json:"is_admin"` IsOwner bool `json:"is_owner"` IsPrimaryOwner bool `json:"is_primary_owner"` IsRestricted bool `json:"is_restricted"` IsUltraRestricted bool `json:"is_ultra_restricted"` IsBot bool `json:"is_bot"` HasFiles bool `json:"has_files"` // LastUpdated is the last time we updated the info in the DB LastUpdated time.Time } func getUserInfo(usr string) (*User, error) { openDatabase() ret := User{ID: usr} err := db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("users")) if uB := b.Bucket([]byte(usr)); uB != nil { if uIB := uB.Bucket([]byte("info")); uIB != nil { var err error if ret.Name, err = bktGetString(uIB, "name"); err != nil { return err } if ret.Deleted, err = bktGetBool(uIB, "deleted"); err != nil { return err } if ret.Status, err = bktGetString(uIB, "status"); err != nil { return err } if ret.Color, err = bktGetString(uIB, "color"); err != nil { return err } if ret.RealName, err = bktGetString(uIB, "real_name"); err != nil { return err } if ret.TZ, err = bktGetString(uIB, "tz"); err != nil { return err } if ret.TZLabel, err = bktGetString(uIB, "tz_label"); err != nil { return err } if ret.TZOffset, err = bktGetInt(uIB, "tz_offset"); err != nil { return err } if ret.IsAdmin, err = bktGetBool(uIB, "is_admin"); err != nil { return err } if ret.IsOwner, err = bktGetBool(uIB, "is_owner"); err != nil { return err } if ret.IsPrimaryOwner, err = bktGetBool(uIB, "is_primary_owner"); err != nil { return err } if ret.IsRestricted, err = bktGetBool(uIB, "is_restricted"); err != nil { return err } if ret.IsUltraRestricted, err = bktGetBool(uIB, "is_ultra_restricted"); err != nil { return err } if ret.IsBot, err = bktGetBool(uIB, "is_bot"); err != nil { return err } if ret.HasFiles, err = bktGetBool(uIB, "has_files"); err != nil { return err } if ret.LastUpdated, err = bktGetTime(uB, "last_updated"); err != nil { return err } } return nil } return fmt.Errorf("User (%s) bucket not found.", usr) }) closeDatabase() return &ret, err } func getUserInfoFromName(usrName string) (*User, error) { openDatabase() ret := User{} err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("users")) c := b.Cursor() var tstName string var uB, uIB *bolt.Bucket var err error var userID string for k, v := c.First(); k != nil && strings.ToLower(tstName) != strings.ToLower(usrName); k, v = c.Next() { if v == nil { if uB = b.Bucket(k); uB != nil { if uIB = uB.Bucket([]byte("info")); uIB != nil { if tstName, err = bktGetString(uIB, "name"); err != nil { return err } } userID = string(k) } } } if tstName != usrName { return fmt.Errorf("Requested user (" + usrName + ") not found") } ret.ID = userID ret.Name = tstName if ret.Deleted, err = bktGetBool(uIB, "deleted"); err != nil { return err } if ret.Status, err = bktGetString(uIB, "status"); err != nil { return err } if ret.Color, err = bktGetString(uIB, "color"); err != nil { return err } if ret.RealName, err = bktGetString(uIB, "real_name"); err != nil { return err } if ret.TZ, err = bktGetString(uIB, "tz"); err != nil { return err } if ret.TZLabel, err = bktGetString(uIB, "tz_label"); err != nil { return err } if ret.TZOffset, err = bktGetInt(uIB, "tz_offset"); err != nil { return err } if ret.IsAdmin, err = bktGetBool(uIB, "is_admin"); err != nil { return err } if ret.IsOwner, err = bktGetBool(uIB, "is_owner"); err != nil { return err } if ret.IsPrimaryOwner, err = bktGetBool(uIB, "is_primary_owner"); err != nil { return err } if ret.IsRestricted, err = bktGetBool(uIB, "is_restricted"); err != nil { return err } if ret.IsUltraRestricted, err = bktGetBool(uIB, "is_ultra_restricted"); err != nil { return err } if ret.IsBot, err = bktGetBool(uIB, "is_bot"); err != nil { return err } if ret.HasFiles, err = bktGetBool(uIB, "has_files"); err != nil { return err } if ret.LastUpdated, err = bktGetTime(uIB, "last_updated"); err != nil { return err } return err }) closeDatabase() return &ret, err } func saveUserInfo(usr *User) error { openDatabase() err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("users")) var err error var uB, uIB *bolt.Bucket if uB, err = b.CreateBucketIfNotExists([]byte(usr.ID)); err != nil { return err } if uIB, err = uB.CreateBucketIfNotExists([]byte("info")); err != nil { return err } if err = bktPutString(uIB, "id", usr.ID); err != nil { return err } if err = bktPutString(uIB, "name", usr.Name); err != nil { return err } if err = bktPutBool(uIB, "deleted", usr.Deleted); err != nil { return err } if err = bktPutString(uIB, "status", usr.Status); err != nil { return err } if err = bktPutString(uIB, "color", usr.Color); err != nil { return err } if err = bktPutString(uIB, "real_name", usr.RealName); err != nil { return err } if err = bktPutString(uIB, "tz", usr.TZ); err != nil { return err } if err = bktPutString(uIB, "tz_label", usr.TZLabel); err != nil { return err } if err = bktPutInt(uIB, "tz_offset", usr.TZOffset); err != nil { return err } if err = bktPutBool(uIB, "is_admin", usr.IsAdmin); err != nil { return err } if err = bktPutBool(uIB, "is_owner", usr.IsOwner); err != nil { return err } if err = bktPutBool(uIB, "is_primary_owner", usr.IsPrimaryOwner); err != nil { return err } if err = bktPutBool(uIB, "is_restricted", usr.IsRestricted); err != nil { return err } if err = bktPutBool(uIB, "is_ultra_restricted", usr.IsUltraRestricted); err != nil { return err } if err = bktPutBool(uIB, "is_bot", usr.IsBot); err != nil { return err } if err = bktPutBool(uIB, "has_files", usr.HasFiles); err != nil { return err } err = bktPutTime(uIB, "last_updated", usr.LastUpdated) return err }) closeDatabase() return err } func saveUserMessage(user string, message *Message) error { openDatabase() err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("users")) var err error var uB, uMB, msgBkt *bolt.Bucket if uB, err = b.CreateBucketIfNotExists([]byte(user)); err != nil { return err } if uMB, err = uB.CreateBucketIfNotExists([]byte("messages")); err != nil { return err } idx, _ := uMB.NextSequence() idxKey := []byte(strconv.FormatUint(idx, 10)) if msgBkt, err = uMB.CreateBucketIfNotExists(idxKey); err != nil { return err } if err = bktPutString(msgBkt, "type", message.Type); err != nil { return err } if err = bktPutString(msgBkt, "text", message.Text); err != nil { return err } if err = bktPutString(msgBkt, "name", message.Name); err != nil { return err } if err = bktPutTime(msgBkt, "time", message.Time); err != nil { return err } return nil }) closeDatabase() return err } func getUserStat(user string, key string) (int, error) { openDatabase() var ret int err := db.View(func(tx *bolt.Tx) error { var b, uB, uSB *bolt.Bucket var err error b = tx.Bucket([]byte("users")) if b == nil { return fmt.Errorf("Unable to open 'users' bucket") } if uB = b.Bucket([]byte(user)); uB != nil { if uSB = uB.Bucket([]byte("stats")); uSB != nil { ret, err = bktGetInt(uSB, key) return err } } return err }) closeDatabase() return ret, err } func saveUserStat(user string, key string, val int) error { openDatabase() err := db.Update(func(tx *bolt.Tx) error { var b, uB, uSB *bolt.Bucket var err error b = tx.Bucket([]byte("users")) if uB, err = b.CreateBucketIfNotExists([]byte(user)); err == nil { if uSB, err = uB.CreateBucketIfNotExists([]byte("stats")); err == nil { err = bktPutInt(uSB, key, val) } } return err }) closeDatabase() return err } func addUserStat(user string, key string, addVal int) error { openDatabase() v, err := getUserStat(user, key) err = saveUserStat(user, key, v+addVal) v, err = getUserStat(user, key) closeDatabase() return err } func incrementUserStat(user string, key string) error { return addUserStat(user, key, 1) } func decrementUserStat(user string, key string) error { return addUserStat(user, key, -1) } func getAllUserStats(user string) (map[string]int, error) { ret := make(map[string]int) openDatabase() err := db.Update(func(tx *bolt.Tx) error { var b, uB, uSB *bolt.Bucket var err error b = tx.Bucket([]byte("users")) if uB, err = b.CreateBucketIfNotExists([]byte(user)); err == nil { if uSB, err = uB.CreateBucketIfNotExists([]byte("stats")); err == nil { var st int var key string uSB.ForEach(func(k, v []byte) error { key = string(k) if st, err = bktGetInt(uSB, key); err == nil { ret[key] = ret[key] + st return nil } return err }) } } return err }) closeDatabase() return ret, err } func getAllUsersStats() (map[string]int, error) { ret := make(map[string]int) openDatabase() err := db.Update(func(tx *bolt.Tx) error { var b, uB, uSB *bolt.Bucket var err error b = tx.Bucket([]byte("users")) err = b.ForEach(func(k, v []byte) error { if v == nil { // Bucket if uB, err = b.CreateBucketIfNotExists([]byte(k)); err == nil { if uSB, err = uB.CreateBucketIfNotExists([]byte("stats")); err == nil { var st int var key string uSB.ForEach(func(k, v []byte) error { key = string(k) if st, err = bktGetInt(uSB, key); err == nil { ret[key] = ret[key] + st return nil } return err }) } } } return err }) return err }) closeDatabase() return ret, err } func getUserList() []string { openDatabase() // First build channel list var users []string db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("users")) return b.ForEach(func(k, v []byte) error { users = append(users, string(k)) return nil }) }) closeDatabase() return users } func getUserName(uid string) string { var ret string openDatabase() db.View(func(tx *bolt.Tx) error { var b, uB, uIB *bolt.Bucket var err error b = tx.Bucket([]byte("users")) if b == nil { return fmt.Errorf("Error opening 'users' bucket") } if uB = b.Bucket([]byte(uid)); uB == nil { return fmt.Errorf("Error opening user bucket (%s)", uid) } if uIB = uB.Bucket([]byte("info")); uIB == nil { return fmt.Errorf("Error opening user info bucket (%s/info)", uid) } ret, err = bktGetString(uIB, "name") return err }) closeDatabase() return ret } func getChannelMessageCount(channel string) int { var ret int openDatabase() db.View(func(tx *bolt.Tx) error { var b, chB, chMB *bolt.Bucket var err error b = tx.Bucket([]byte("channels")) if chB = b.Bucket([]byte(channel)); chB != nil { if chMB = chB.Bucket([]byte("messages")); chMB != nil { ret = chMB.Stats().BucketN } } return err }) closeDatabase() return ret } func getChannelList() []string { openDatabase() // First build channel list var chans []string db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("channels")) return b.ForEach(func(k, v []byte) error { chans = append(chans, string(k)) return nil }) }) closeDatabase() return chans } func getTotalChannelMsgCount() int { var ret int openDatabase() // First build channel list chans := getChannelList() for _, k := range chans { ret = ret + getChannelMessageCount(k) } closeDatabase() return ret } func bktGetBucket(b *bolt.Bucket, key string) (*bolt.Bucket, error) { bkt := b.Bucket([]byte(key)) if bkt != nil { return nil, fmt.Errorf("Bucket (%s) not found.", key) } return bkt, nil } func bktGetBytes(b *bolt.Bucket, key string) ([]byte, error) { bb := b.Get([]byte(key)) if bb == nil { return nil, fmt.Errorf("Error reading key (%s)", key) } return bb, nil } func bktGetString(b *bolt.Bucket, key string) (string, error) { bb, err := bktGetBytes(b, key) return string(bb), err } func bktPutString(b *bolt.Bucket, key string, val string) error { return b.Put([]byte(key), []byte(val)) } func bktGetBool(b *bolt.Bucket, key string) (bool, error) { bb, err := bktGetBytes(b, key) return (string(bb) == "true"), err } func bktPutBool(b *bolt.Bucket, key string, val bool) error { if val { return b.Put([]byte(key), []byte("true")) } return b.Put([]byte(key), []byte("false")) } func bktGetInt(b *bolt.Bucket, key string) (int, error) { bb, err := bktGetBytes(b, key) if err != nil { return 0, err } return strconv.Atoi(string(bb)) } func bktPutInt(b *bolt.Bucket, key string, val int) error { return b.Put([]byte(key), []byte(fmt.Sprintf("%d", val))) } func bktGetTime(b *bolt.Bucket, key string) (time.Time, error) { bs, err := bktGetString(b, key) if err != nil { return time.Time{}, err } return time.Parse(time.RFC3339, bs) } func bktPutTime(b *bolt.Bucket, key string, val time.Time) error { bs := val.Format(time.RFC3339) 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) // | |-info (bucket) // | | |-userinfo1 (pair) // | | \-userinfo2 (pair) // | | // | |-messages (bucket) // | | |-message1 (message bucket) // | | \-message2 (message bucket) // | | // | \-stats (bucket) // | |-stat1 (pair) // | \-stat2 (pair) // | // \- username2 (bucket) // |-info (bucket) // | |-userinfo1 (pair) // | \-userinfo2 (pair) // | // |-messages (bucket) // | |-message1 (message bucket) // | \-message2 (message bucket) // | // \-stats (bucket) // |-stat1 (pair) // \-stat2 (pair) // // And channel stats are saved in the db like so: // channels (bucket) // |- channel1 (bucket) // | |-info (bucket) // | | |-chaninfo1 (pair) // | | \-chaninfo2 (pair) // | | // | |-messages (bucket) // | | |-message1 (message bucket) // | | \-message2 (message bucket) // | | // | \-stats (bucket) // | |-stat1 (pair) // | \-stat2 (pair) // | // \- channel2 (bucket) // |-info (bucket) // | |-chaninfo1 (pair) // | \-chaninfo2 (pair) // | // |-messages (bucket) // | |-message1 (message bucket) // | \-message2 (message bucket) // | // \-stats (bucket) // |-stat1 (pair) // \-stat2 (pair) // // A (message bucket) looks like this: // messageid (unique to parent bucket) // |-Type (slack event type) // |-User (slack user ID) // |-Text (Text of the message) // \-Time RFC3339 format timestamp