ADDING THE FILES FOR THAT PROGRESS!
This commit is contained in:
parent
7f93845e9e
commit
12ae393998
10
README.md
Normal file
10
README.md
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
# mybot
|
||||
|
||||
`mybot` is an working Slack bot written in Go. Fork it and use it to build
|
||||
your very own cool Slack bot!
|
||||
|
||||
Check the [blog post](https://www.opsdash.com/blog/slack-bot-in-golang.html)
|
||||
for a description of mybot internals.
|
||||
|
||||
Follow us on Twitter today! [@therapidloop](https://twitter.com/therapidloop)
|
188
slack.go
Normal file
188
slack.go
Normal file
@ -0,0 +1,188 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// Slack manages the slack connection and implements API calls
|
||||
type Slack struct {
|
||||
apiToken string
|
||||
id string
|
||||
socket *websocket.Conn
|
||||
alive bool
|
||||
}
|
||||
|
||||
// CreateSlack creates the slack object, opening a websocket
|
||||
func CreateSlack(token string) (*Slack, error) {
|
||||
s := Slack{apiToken: token}
|
||||
url := fmt.Sprintf("https://slack.com/api/rtm.start?token=%s", token)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("API request failed with code %d", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var respObj responseRtmStart
|
||||
err = json.Unmarshal(body, &respObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !respObj.Ok {
|
||||
err = fmt.Errorf("Slack error: %s", respObj.Error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wsurl := respObj.URL
|
||||
s.id = respObj.Self.ID
|
||||
|
||||
ws, err := websocket.Dial(wsurl, "", "https://api.slack.com/")
|
||||
s.socket = ws
|
||||
s.keepAlive()
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (s *Slack) keepAlive() {
|
||||
s.alive = true
|
||||
go func() {
|
||||
for {
|
||||
if s.alive {
|
||||
s.ping()
|
||||
}
|
||||
time.Sleep(time.Second * 30)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var counter uint64
|
||||
|
||||
func (s *Slack) ping() error {
|
||||
return s.postMessage(Message{Type: "ping"})
|
||||
}
|
||||
|
||||
func (s *Slack) postMessage(m Message) error {
|
||||
m.ID = atomic.AddUint64(&counter, 1)
|
||||
return websocket.JSON.Send(s.socket, m)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
func (s *Slack) sendMessageToUser(uid string, msg string) error {
|
||||
// TODO: Actually send a direct message to uid
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Slack) getUserInfo(uid string) (*User, error) {
|
||||
url := fmt.Sprintf("https://slack.com/api/users.info?token=%s&user=%s", s.apiToken, uid)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("API request failed with code %d", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
writeToLog("User Lookup: \n")
|
||||
writeToLog(string(body))
|
||||
writeToLog("\n\n")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var respObj responseUserLookup
|
||||
err = json.Unmarshal(body, &respObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !respObj.Ok {
|
||||
err = fmt.Errorf("Slack error: %s", respObj.Error)
|
||||
return nil, err
|
||||
}
|
||||
return respObj.User, nil
|
||||
}
|
||||
|
||||
func (s *Slack) getChannelInfo(cid string) (*Channel, error) {
|
||||
url := fmt.Sprintf("https://slack.com/api/channels.info?token=%s&channel=%s", s.apiToken, cid)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("API request failed with code %d", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
writeToLog("Channel Lookup: \n")
|
||||
writeToLog(string(body))
|
||||
writeToLog("\n\n")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var respObj responseChannelLookup
|
||||
err = json.Unmarshal(body, &respObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !respObj.Ok {
|
||||
err = fmt.Errorf("Slack error: %s", respObj.Error)
|
||||
return nil, err
|
||||
}
|
||||
return respObj.Channel, nil
|
||||
}
|
||||
|
||||
// These structures represent the response of the Slack API events
|
||||
// Only some fields are included. The rest are ignored by json.Unmarshal.
|
||||
type responseRtmStart struct {
|
||||
Ok bool `json:"ok"`
|
||||
Error string `json:"error"`
|
||||
URL string `json:"url"`
|
||||
Self responseSelf `json:"self"`
|
||||
}
|
||||
|
||||
type responseSelf struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type responseUserLookup struct {
|
||||
Ok bool `json:"ok"`
|
||||
Error string `json:"error"`
|
||||
User *User `json:"user"`
|
||||
}
|
||||
|
||||
type responseChannelLookup struct {
|
||||
Ok bool `json:"ok"`
|
||||
Error string `json:"error"`
|
||||
Channel *Channel `json:"channel"`
|
||||
}
|
107
statbot.go
Normal file
107
statbot.go
Normal file
@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const programName = "statbot"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintf(os.Stderr, "usage: statbot slack-bot-token\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
writeToLog("\n\n\n== " + time.Now().Format(time.RFC3339) + " ==\n")
|
||||
|
||||
// start a websocket-based Real Time API session
|
||||
var slack *Slack
|
||||
var err error
|
||||
|
||||
if err = initDatabase(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if slack, err = CreateSlack(os.Args[1]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("statbot ready, ^C exits")
|
||||
|
||||
for {
|
||||
// read each incoming message
|
||||
m, err := slack.getMessage()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
processMessage(slack, &m)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
// TODO: Handle reaction_added messages
|
||||
// TODO: Handle reaction_removed messages
|
||||
|
||||
if m.Type == "message" {
|
||||
// Check if we know who the user is
|
||||
usr, err := getUserInfo(m.User)
|
||||
// If the user information hasn't been updated in the past 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)
|
||||
}
|
||||
}
|
||||
// If 'channel' is defined, save the message to the 'channels' bucket
|
||||
if m.Channel != "" {
|
||||
// Check if we know what the channel is
|
||||
chnl, err := getChannelInfo(m.Channel)
|
||||
// If the channel information hasn't been updated in the past 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)
|
||||
} else {
|
||||
// Save channel info
|
||||
saveChannelInfo(c)
|
||||
// And save the channel message
|
||||
saveChannelMessage(m.Channel, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if we're mentioned
|
||||
/*
|
||||
if m.Type == "message" && strings.HasPrefix(m.Text, "<@"+slack.id+">") {
|
||||
parts := strings.Fields(m.Text)
|
||||
if len(parts) == 3 && parts[1] == "stock" {
|
||||
// looks good, get the quote and reply with the result
|
||||
go func(m Message) {
|
||||
m.Text = getQuote(parts[2])
|
||||
postMessage(ws, m)
|
||||
}(m)
|
||||
// NOTE: the Message object is copied, this is intentional
|
||||
} else {
|
||||
// huh?
|
||||
m.Text = fmt.Sprintf("Beep boop beep, that does not compute\n")
|
||||
postMessage(ws, m)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
func writeToLog(d string) {
|
||||
f, err := os.OpenFile("statbot.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0664)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.WriteString(d)
|
||||
f.Close()
|
||||
}
|
680
statbot_model.go
Normal file
680
statbot_model.go
Normal file
@ -0,0 +1,680 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const databaseFile = programName + ".db"
|
||||
|
||||
var db *bolt.DB
|
||||
|
||||
func openDatabase() error {
|
||||
var err error
|
||||
db, err = bolt.Open(databaseFile, 0600, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeDatabase() error {
|
||||
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
|
||||
_, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||
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
|
||||
}
|
||||
|
||||
// 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 int `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("channel"))
|
||||
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
|
||||
}
|
||||
}
|
||||
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 string) 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))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
closeDatabase()
|
||||
return 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
|
||||
}
|
||||
|
||||
func incrementChannelStat(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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func getChannelStat(channel string, key string) (string, error) {
|
||||
openDatabase()
|
||||
var ret string
|
||||
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)))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
closeDatabase()
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var err error
|
||||
if ret.Name, err = bktGetString(uB, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Deleted, err = bktGetBool(uB, "deleted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Status, err = bktGetString(uB, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Color, err = bktGetString(uB, "color"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.RealName, err = bktGetString(uB, "real_name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZ, err = bktGetString(uB, "tz"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZLabel, err = bktGetString(uB, "tz_label"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZOffset, err = bktGetInt(uB, "tz_offset"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsAdmin, err = bktGetBool(uB, "is_admin"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsOwner, err = bktGetBool(uB, "is_owner"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsPrimaryOwner, err = bktGetBool(uB, "is_primary_owner"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsRestricted, err = bktGetBool(uB, "is_restricted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsUltraRestricted, err = bktGetBool(uB, "is_ultra_restricted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsBot, err = bktGetBool(uB, "is_bot"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.HasFiles, err = bktGetBool(uB, "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 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_udpated", 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 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))
|
||||
}
|
||||
|
||||
// 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
|
Loading…
Reference in New Issue
Block a user