Functioning with leaderboard updates

This commit is contained in:
Brian Buller 2016-12-06 17:31:22 -06:00
parent 8a380f4377
commit 8e468c4ce9
6 changed files with 436 additions and 138 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
slack-api-key
cookies
aoc_leaderboard.json
aocbot
aocbot.db
runbot.sh
*.log

18
aoc_structs.go Normal file
View File

@ -0,0 +1,18 @@
package main
import "time"
type Leaderboard struct {
OwnerID string `json:"owner_id"`
Event string `json:"event"`
Members map[string]Member `json:"members"`
}
type Member struct {
ID string `json:"id"`
Stars int `json:"stars"`
RawStarTs string `json:"last_star_ts"`
LastStarTs time.Time
Name string `json:"name"`
SlackID string
}

253
aocbot.go
View File

@ -3,7 +3,12 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"gogs.bullercodeworks.com/brian/boltease"
@ -11,42 +16,14 @@ import (
const programName = "aocbot"
// Message Processors are interfaces for interacting.
type messageProcessor interface {
GetName() string
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
// Message Processors are interfaces for accumulating statistics.
type statProcessor interface {
GetName() string
GetStatKeys() []string
Initialize()
ProcessMessage(m *Message)
ProcessBotMessage(m *Message)
ProcessUserMessage(m *Message)
ProcessBotUserMessage(m *Message)
ProcessChannelMessage(m *Message)
ProcessBotChannelMessage(m *Message)
}
var statProcessors []statProcessor
var db *boltease.DB
var boardID string
var slackChannel string
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "usage: "+programName+" <slack-bot-token>\n")
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "usage: "+programName+" <slack-bot-token> <aoc-leaderboard-number>\n")
os.Exit(1)
}
@ -56,22 +33,68 @@ func main() {
if db, err = getDatabase(); err != nil {
panic(err)
}
slackChannel = "C0G3X1M5K"
// DevICT Leaderboard: 3549
boardID = os.Args[2]
if slack, err = CreateSlack(os.Args[1]); err != nil {
panic(err)
}
statBotMain(slack)
aocBotMain(slack)
}
// This is the main function for the statbot
func statBotMain(slack *Slack) {
func aocBotMain(slack *Slack) {
// start a websocket-based Real Time API session
registerMessageProcessor(new(generalProcessor))
fmt.Println("aoc-bot ready, ^C exits")
writeToLog("== " + time.Now().Format(time.RFC3339) + " - Bot Started ==\n")
var lastAoCUpdate time.Time
go func() {
for {
var err error
lastAoCUpdate = time.Now()
var leaderboard *Leaderboard
fmt.Println(lastAoCUpdate.Format(time.RFC3339) + ": Fetching Online Leaderboard")
if leaderboard, err = getAoCLeaderboard(boardID); err != nil {
fmt.Println(err.Error())
}
for _, v := range leaderboard.Members {
var mbr *Member
if mbr, err = getAoCUser(v.ID); err != nil {
// Member doesn't exist in db, add it
// Notify br0xen ( U030RD9NU )
m := new(Message)
m.Type = "message"
m.Channel = "D0D793N5R"
m.Text = "AoC Leaderboard has a new member! " + v.Name
fmt.Println("New Leaderboard Member Found: " + v.Name)
if err = slack.postMessage(*m); err != nil {
fmt.Println(err.Error())
}
saveAoCUser(&v)
continue
}
if mbr.Stars != v.Stars {
// Number of stars has changed
fmt.Println(v.ID + "(" + v.Name + "): " + v.LastStarTs.Format(time.RFC3339))
m := new(Message)
m.Type = "message"
m.Channel = slackChannel
m.Text = ":christmas_tree: " + v.Name + " now has " + strconv.Itoa(v.Stars) + " stars! :christmas_tree:"
if err = slack.postMessage(*m); err != nil {
fmt.Println(err.Error())
}
saveAoCUser(&v)
}
}
time.Sleep(time.Minute * 10)
}
}()
for {
// read each incoming message
m, err := slack.getMessage()
@ -79,6 +102,7 @@ func statBotMain(slack *Slack) {
writeToLog(" " + time.Now().Format(time.RFC3339) + " - Received Message\n")
processMessage(slack, &m)
}
}
writeToLog("== " + time.Now().Format(time.RFC3339) + " - Bot Stopped ==\n\n")
}
@ -94,33 +118,15 @@ func processMessage(slack *Slack, m *Message) {
var usr *User
// Check if we know who the user is
usr, err = getUserInfo(m.User)
usr, err = getUser(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 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)
saveUser(u)
}
}
// TODO: Process the message
if m.Channel != "" {
// Check if we know what the channel is
chnl, err := getChannelInfo(m.Channel)
@ -137,30 +143,133 @@ func processMessage(slack *Slack, m *Message) {
}
}
if isDirectMessage {
for _, proc := range messageProcessors {
if isAdmin(m.User) {
proc.ProcessAdminUserMessage(slack, m)
if m.User == "U030RD9NU" {
// message from br0xen
if strings.HasPrefix(m.Text, "!aoc") {
var slackId, aocNm, op string
flds := strings.Fields(m.Text)
if len(flds) < 2 {
// No operation specified
return
}
op = flds[1]
if op == "help" {
// TODO: Send help message
}
if op == "match" {
re := regexp.MustCompile("[<\"]([^>]*)[>\"]")
matchUp := re.FindAllString(m.Text, -1)
for i := range matchUp {
if matchUp[i][0] == '<' {
slackId = matchUp[i][1 : len(matchUp[i])-1]
}
if matchUp[i][0] == '"' {
aocNm = matchUp[i][1 : len(matchUp[i])-1]
}
}
var m *Member
if m, err = getAoCUserByName(aocNm); err != nil {
// AoC Member doesn't exist in DB
fmt.Println("AoC User " + aocNm + " doesn't exist in DB")
fmt.Println(err.Error())
return
}
m.SlackID = slackId
saveAoCUser(m)
} else if op == "ping" {
m := new(Message)
m.Type = "message"
m.Channel = "D0D793N5R"
m.Text = ":christmas_tree: PONG :christmas_tree:"
if err = slack.postMessage(*m); err != nil {
fmt.Println(err.Error())
}
}
}
proc.ProcessUserMessage(slack, m)
/*
ID: 0
Type: message
Channel: D0D793N5R
User: U030RD9NU
Name:
Text: testing
Ts: 1481044675.000003
*/
}
} else {
for _, proc := range messageProcessors {
proc.ProcessChannelMessage(slack, m)
fmt.Println("--- Channel Message Received")
fmt.Println(m.Channel, m.User, m.Text)
if m.Channel == slackChannel {
// In the AoC channel
if strings.HasPrefix(m.Text, "!aoc") {
var op string
flds := strings.Fields(m.Text)
if len(flds) < 2 {
// No operation specified
return
}
op = flds[1]
if op == "ping" {
m := new(Message)
m.Type = "message"
m.Channel = slackChannel
m.Text = ":christmas_tree: PONG :christmas_tree:"
if err = slack.postMessage(*m); err != nil {
fmt.Println(err.Error())
}
}
}
}
// TODO: Process Channel Message
/*
for _, proc := range messageProcessors {
proc.ProcessChannelMessage(slack, m)
}
*/
}
}
}
}
func registerMessageProcessor(b messageProcessor) {
// Register a Message Processor
// Make sure that we haven't already registered it
for _, proc := range messageProcessors {
if proc.GetName() == b.GetName() {
panic(fmt.Errorf("Attempted to Re-register Message Processor %s", b.GetName()))
}
func getAoCLeaderboard(boardId string) (*Leaderboard, error) {
var err error
var req *http.Request
var content []byte
var resp *http.Response
var body []byte
leaderboard := new(Leaderboard)
client := &http.Client{}
boardString := "http://adventofcode.com/2016/leaderboard/private/view/" + boardId + ".json"
req, err = http.NewRequest("GET", boardString, nil)
// Read in cookies
content, err = ioutil.ReadFile("./cookies")
if err != nil {
fmt.Println("Error reading Cookies")
return leaderboard, err
}
messageProcessors = append(messageProcessors, b)
line := strings.TrimSpace(string(content))
req.Header.Add("Cookie", line)
resp, err = client.Do(req)
if err != nil {
fmt.Println("Error getting leaderboard")
}
defer resp.Body.Close()
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body.")
return leaderboard, err
}
err = json.Unmarshal(body, &leaderboard)
for k, mbr := range leaderboard.Members {
mbr.LastStarTs, err = time.Parse("2006-01-02T15:04:05-0700", mbr.RawStarTs)
leaderboard.Members[k] = mbr
}
return leaderboard, err
}
func writeToLog(d string) {

196
model.go
View File

@ -1,6 +1,7 @@
package main
import (
"strings"
"time"
"gogs.bullercodeworks.com/brian/boltease"
@ -9,44 +10,46 @@ import (
func getDatabase() (*boltease.DB, error) {
db, err := boltease.Create("aocbot.db", 0644, nil)
if err != nil {
return err
return db, err
}
// Make sure that we have a 'users' bucket
db.MkBucketPath([]string{"users"})
// Make sure that we have a 'channels' bucket
db.MkBucketPath([]string{"channels"})
// Make sure that we have a 'aocmembers' bucket
db.MkBucketPath([]string{"AoCMembers"})
return db, nil
}
func (db *boltease.DB) saveUser(usr *User) error {
func saveUser(usr *User) error {
var err error
if err = db.OpenDB(); err != nil {
return err
}
defer db.CloseDB()
usr := []string{"users", usr.ID}
db.SetValue(append(usr, "Name"), usr.Name)
db.SetBool(append(usr, "Deleted"), usr.Deleted)
db.SetValue(append(usr, "Status"), usr.Status)
db.SetValue(append(usr, "Color"), usr.Color)
db.SetValue(append(usr, "RealName"), usr.RealName)
db.SetValue(append(usr, "TZ"), usr.TZ)
db.SetValue(append(usr, "TZLabel"), usr.TZLabel)
db.SetInt(append(usr, "TZOffset"), usr.TZOffset)
db.SetBool(append(usr, "IsAdmin"), usr.IsAdmin)
db.SetBool(append(usr, "IsOwner"), usr.IsOwner)
db.SetBool(append(usr, "IsPrimaryOwner"), usr.IsPrimaryOwner)
db.SetBool(append(usr, "IsRestricted"), usr.IsRestricted)
db.SetBool(append(usr, "IsUltraRestricted"), usr.IsUltraRestricted)
db.SetBool(append(usr, "IsBot"), usr.IsBot)
db.SetBool(append(usr, "HasFiles"), usr.HasFiles)
db.SetTimestamp(append(usr, "LastUpdated"), time.Now())
usrPath := []string{"users", usr.ID}
db.SetValue(usrPath, "Name", usr.Name)
db.SetBool(usrPath, "Deleted", usr.Deleted)
db.SetValue(usrPath, "Status", usr.Status)
db.SetValue(usrPath, "Color", usr.Color)
db.SetValue(usrPath, "RealName", usr.RealName)
db.SetValue(usrPath, "TZ", usr.TZ)
db.SetValue(usrPath, "TZLabel", usr.TZLabel)
db.SetInt(usrPath, "TZOffset", usr.TZOffset)
db.SetBool(usrPath, "IsAdmin", usr.IsAdmin)
db.SetBool(usrPath, "IsOwner", usr.IsOwner)
db.SetBool(usrPath, "IsPrimaryOwner", usr.IsPrimaryOwner)
db.SetBool(usrPath, "IsRestricted", usr.IsRestricted)
db.SetBool(usrPath, "IsUltraRestricted", usr.IsUltraRestricted)
db.SetBool(usrPath, "IsBot", usr.IsBot)
db.SetBool(usrPath, "HasFiles", usr.HasFiles)
db.SetTimestamp(usrPath, "LastUpdated", time.Now())
return err
}
func (db *boltease.DB) getUser(usrId string) (*User, error) {
func getUser(usrId string) (*User, error) {
usr := new(User)
var err error
if err = db.OpenDB(); err != nil {
@ -54,53 +57,172 @@ func (db *boltease.DB) getUser(usrId string) (*User, error) {
}
defer db.CloseDB()
upath := []string{"users", usrID}
if usr.Name, err = db.GetValue(append(upath, "Name")); err != nil {
upath := []string{"users", usrId}
if usr.Name, err = db.GetValue(upath, "Name"); err != nil {
return usr, err
}
if usr.Deleted, err = db.GetBool(append(upath, "Deleted")); err != nil {
if usr.Deleted, err = db.GetBool(upath, "Deleted"); err != nil {
return usr, err
}
if usr.Status, err = db.GetValue(append(upath, "Status")); err != nil {
if usr.Status, err = db.GetValue(upath, "Status"); err != nil {
return usr, err
}
if usr.Color, err = db.GetValue(append(upath, "Color")); err != nil {
if usr.Color, err = db.GetValue(upath, "Color"); err != nil {
return usr, err
}
if usr.RealName, err = db.GetValue(append(upath, "RealName")); err != nil {
if usr.RealName, err = db.GetValue(upath, "RealName"); err != nil {
return usr, err
}
if usr.TZ, err = db.GetValue(append(upath, "TZ")); err != nil {
if usr.TZ, err = db.GetValue(upath, "TZ"); err != nil {
return usr, err
}
if usr.TZLabel, err = db.GetValue(append(upath, "TZLabel")); err != nil {
if usr.TZLabel, err = db.GetValue(upath, "TZLabel"); err != nil {
return usr, err
}
if usr.TZOffset, err = db.GetInt(append(upath, "TZOffset")); err != nil {
if usr.TZOffset, err = db.GetInt(upath, "TZOffset"); err != nil {
return usr, err
}
if usr.IsAdmin, err = db.GetBool(append(upath, "IsAdmin")); err != nil {
if usr.IsAdmin, err = db.GetBool(upath, "IsAdmin"); err != nil {
return usr, err
}
if usr.IsOwner, err = db.GetBool(append(upath, "IsOwner")); err != nil {
if usr.IsOwner, err = db.GetBool(upath, "IsOwner"); err != nil {
return usr, err
}
if usr.IsPrimaryOwner, err = db.GetBool(append(upath, "IsPrimaryOwner")); err != nil {
if usr.IsPrimaryOwner, err = db.GetBool(upath, "IsPrimaryOwner"); err != nil {
return usr, err
}
if usr.IsRestricted, err = db.GetBool(append(upath, "IsRestricted")); err != nil {
if usr.IsRestricted, err = db.GetBool(upath, "IsRestricted"); err != nil {
return usr, err
}
if usr.IsUltraRestricted, err = db.GetBool(append(upath, "IsUltraRestricted")); err != nil {
if usr.IsUltraRestricted, err = db.GetBool(upath, "IsUltraRestricted"); err != nil {
return usr, err
}
if usr.IsBot, err = db.GetBool(append(upath, "IsBot")); err != nil {
if usr.IsBot, err = db.GetBool(upath, "IsBot"); err != nil {
return usr, err
}
if usr.HasFiles, err = db.GetBool(append(upath, "HasFiles")); err != nil {
if usr.HasFiles, err = db.GetBool(upath, "HasFiles"); err != nil {
return usr, err
}
usr.LastUpdated, err = db.GetTimestamp(append(upath, "LastUpdated"))
usr.LastUpdated, err = db.GetTimestamp(upath, "LastUpdated")
return usr, err
}
func getChannelInfo(chanId string) (*Channel, error) {
ret := new(Channel)
var err error
if err = db.OpenDB(); err != nil {
return ret, err
}
defer db.CloseDB()
cPath := []string{"channels", chanId}
if ret.ID, err = db.GetValue(cPath, "ID"); err != nil {
return ret, err
}
if ret.Name, err = db.GetValue(cPath, "Name"); err != nil {
return ret, err
}
if ret.IsChannel, err = db.GetBool(cPath, "IsChannel"); err != nil {
return ret, err
}
if ret.IsGeneral, err = db.GetBool(cPath, "IsGeneral"); err != nil {
return ret, err
}
if ret.IsMember, err = db.GetBool(cPath, "IsMember"); err != nil {
return ret, err
}
var mbrString string
if mbrString, err = db.GetValue(cPath, "Members"); err != nil {
return ret, err
}
ret.Members = strings.Split(mbrString, ",")
return ret, err
}
func saveChannelInfo(c *Channel) error {
var err error
if err = db.OpenDB(); err != nil {
return err
}
defer db.CloseDB()
cPath := []string{"channels", c.ID}
db.SetValue(cPath, "ID", c.ID)
db.SetValue(cPath, "Name", c.Name)
db.SetBool(cPath, "IsChannel", c.IsChannel)
db.SetBool(cPath, "IsGeneral", c.IsGeneral)
db.SetBool(cPath, "IsMember", c.IsMember)
db.SetValue(cPath, "Members", strings.Join(c.Members, ","))
return nil
}
func saveAoCUser(m *Member) error {
var err error
if err = db.OpenDB(); err != nil {
return err
}
defer db.CloseDB()
mPath := []string{"AoCMembers", m.ID}
db.SetValue(mPath, "ID", m.ID)
db.SetValue(mPath, "Name", m.Name)
db.SetInt(mPath, "Stars", m.Stars)
db.SetTimestamp(mPath, "LastStarTs", m.LastStarTs)
db.SetValue(mPath, "SlackID", m.SlackID)
return nil
}
func getAoCUser(mbrId string) (*Member, error) {
ret := new(Member)
var err error
if err = db.OpenDB(); err != nil {
return ret, err
}
defer db.CloseDB()
mPath := []string{"AoCMembers", mbrId}
if ret.ID, err = db.GetValue(mPath, "ID"); err != nil {
return ret, err
}
if ret.Stars, err = db.GetInt(mPath, "Stars"); err != nil {
return ret, err
}
if ret.LastStarTs, err = db.GetTimestamp(mPath, "LastStarTs"); err != nil {
return ret, err
}
if ret.Name, err = db.GetValue(mPath, "Name"); err != nil {
return ret, err
}
ret.SlackID, err = db.GetValue(mPath, "SlackID")
return ret, err
}
func getAoCUserByName(nm string) (*Member, error) {
var err error
if err = db.OpenDB(); err != nil {
return nil, err
}
defer db.CloseDB()
mPath := []string{"AoCMembers"}
var bktList []string
if bktList, err = db.GetBucketList(mPath); err != nil {
return nil, err
}
var dbNm, dbId string
for i := range bktList {
dbNm, err = db.GetValue(append(mPath, bktList[i]), "Name")
if err != nil {
return nil, err
}
if nm == dbNm {
dbId, err = db.GetValue(append(mPath, bktList[i]), "ID")
if err != nil {
return nil, err
}
return getAoCUser(dbId)
}
}
return nil, err
}

View File

@ -5,6 +5,8 @@ import (
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"sync/atomic"
"time"
@ -91,7 +93,15 @@ func (s *Slack) getMessage() (Message, error) {
func (s *Slack) sendMessageToUser(uid string, msg string) error {
// TODO: Actually send a direct message to uid
return nil
m := new(Message)
m.ID = atomic.AddUint64(&counter, 1)
m.Type = "message"
m.Channel = ""
m.User = uid
m.Name = ""
m.Text = msg
return websocket.JSON.Send(s.socket, m)
}
func (s *Slack) getUserInfo(uid string) (*User, error) {
@ -122,6 +132,41 @@ func (s *Slack) getUserInfo(uid string) (*User, error) {
return respObj.User, nil
}
func (s *Slack) getChannelList() ([]Channel, error) {
url := fmt.Sprintf("https://slack.com/api/channels.list?token=%s", s.apiToken)
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
}
fmt.Println(string(body))
var respObj responseChannelList
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
}
for i := range respObj.Channels {
respObj.Channels[i].LastRead = convertSlackTimestamp(respObj.Channels[i].LastReadRaw)
}
return respObj.Channels, 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)
@ -153,34 +198,6 @@ func (s *Slack) getChannelInfo(cid string) (*Channel, error) {
return respObj.Channel, nil
}
func (s *Slack) joinChannel(c *Channel) error {
url := fmt.Sprintf("https://slack.com/api/channels.join?token=%s&name=%s", s.apiToken, c.Name)
resp, err := http.Get(url)
if err != nil {
return err
}
if resp.StatusCode != 200 {
err = fmt.Errorf("API request failed with code %d", resp.StatusCode)
return err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return err
}
var respObj responseChannelLookup
err = json.Unmarshal(body, &respObj)
if err != nil {
return err
}
if !respObj.Ok {
err = fmt.Errorf("Slack error: %s", respObj.Error)
return err
}
return err
}
// 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 {
@ -205,3 +222,19 @@ type responseChannelLookup struct {
Error string `json:"error"`
Channel *Channel `json:"channel"`
}
type responseChannelList struct {
Ok bool `json:"ok"`
Error string `json:"error"`
Channels []Channel `json:"channels"`
}
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
}

View File

@ -39,6 +39,15 @@ type Channel struct {
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
}
// User object
type User struct {
ID string `json:"id"`