Got some stats displaying

This commit is contained in:
Brian Buller 2015-11-02 13:48:29 -06:00
parent 1f5eae33b9
commit 083f9f3901
10 changed files with 29438 additions and 48 deletions

View File

@ -186,6 +186,31 @@ ul.menu-list-dropped {
width: 150px;
}
aside {
padding: 0.3em 1em;
border-radius: 3px;
color: #fff;
}
/* Other Styles :D */
.success {
background: rgb(28, 184, 65); /* this is a green */
}
.error {
background: rgb(202, 60, 60); /* this is a maroon */
}
.warning {
background: rgb(223, 117, 20); /* this is an orange */
}
.secondary {
background: rgb(66, 184, 221); /* this is a light blue*/
}
.primary {
background: #1f8dd6;
}
/* -- Responsive Styles (Media Queries) ------------------------------------- */
/*

29014
assets/js/highcharts.js Normal file

File diff suppressed because it is too large Load Diff

113
assets/js/main_stats.js Normal file
View File

@ -0,0 +1,113 @@
document.addEventListener("DOMContentLoaded", function(event) {
var main_channel_names = ['general','random'],
main_member_numbers = [],
main_message_numbers = [],
other_channel_names = [],
other_member_numbers = [],
other_message_numbers = [];
for(var i = 0; i < stats.channels.length; i++){
if(stats.channels[i].name == "general") {
main_member_numbers[0] = stats.channels[i].member_count;
main_message_numbers[0] = stats.channels[i].message_count;
} else if(stats.channels[i].name == "random") {
main_member_numbers[1] = stats.channels[i].member_count;
main_message_numbers[1] = stats.channels[i].message_count;
} else {
other_channel_names.push(stats.channels[i].name);
other_member_numbers.push(stats.channels[i].member_count);
other_message_numbers.push(stats.channels[i].message_count);
}
}
var mainChart = new Highcharts.Chart({
chart: {
renderTo: 'mainStatsBarChart',
type: 'column'
},
title: {
text: 'Main Channels'
},
yAxis: [{
min: 0,
title: { text: 'Messages' }
},{
min: 0,
title: { text: 'Members' },
opposite: true
}],
plotOptions: {
column: {
grouping: false,
shadow: false,
borderWidth: 0
}
},
xAxis: {
categories: main_channel_names
},
series: [{
name: 'Messages',
color: 'rgba(126,86,134,0.9)',
data: main_message_numbers,
pointPadding: 0.3
},{
name: 'Members',
color: 'rgba(165,170,217,1)',
data: main_member_numbers,
pointPadding: 0.4,
yAxis: 1
}]
});
var otherChart = new Highcharts.Chart({
chart: {
renderTo: 'statsBarChart',
type: 'column'
},
title: {
text: 'Other Channels'
},
yAxis: [{
min: 0,
title: { text: 'Members' }
},{
min: 0,
title: { text: 'Messages' },
opposite: true
}],
tooltip: { shared: true },
plotOptions: {
column: {
grouping: false,
shadow: false,
borderWidth: 0
}
},
xAxis: {
categories: other_channel_names
},
series: [{
name: 'Messages',
color: 'rgba(126,86,134,0.9)',
data: other_message_numbers,
pointPadding: 0.3
},{
name: 'Members',
color: 'rgba(165,170,217,1)',
data: other_member_numbers,
pointPadding: 0.4,
yAxis: 1
}]
});
});

View File

@ -2,6 +2,7 @@ package main
import (
"fmt"
"net/http"
"strings"
)
@ -97,7 +98,7 @@ func (p *generalStatProcessor) GetStatKeys() []string {
func (p *generalStatProcessor) ProcessMessage(m *Message) {
incrementUserStat(m.User, "message-hour-"+m.Time.Format("15"))
incrementUserStat(m.User, "message-dow-"+m.Time.Format("Mon"))
incrementUserStat(m.User, "message-dom-"+m.Time.Format("_2"))
incrementUserStat(m.User, "message-dom-"+m.Time.Format("02"))
}
func (p *generalStatProcessor) ProcessBotMessage(m *Message) {}
@ -111,6 +112,99 @@ func (p *generalStatProcessor) ProcessChannelMessage(m *Message) {
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"))
incrementChannelStat(m.Channel, "message-dom-"+m.Time.Format("02"))
}
func (p *generalStatProcessor) ProcessBotChannelMessage(m *Message) {}
/*
* Web Site Module
*/
type generalWebModule struct{}
func (wm *generalWebModule) GetName() string {
return "General Web Module"
}
func (wm *generalWebModule) GetRoutes() map[string]func(http.ResponseWriter, *http.Request) {
ret := make(map[string]func(http.ResponseWriter, *http.Request))
ret["/"] = wm.handleStats
return ret
}
func (wm *generalWebModule) Register() {
for k, v := range wm.GetRoutes() {
r.HandleFunc(k, v)
}
}
func (wm *generalWebModule) GetMenuEntries() []menuItem {
var ret []menuItem
ret = append(ret, menuItem{Text: "Stats", Link: "/"})
return ret
}
func (wm *generalWebModule) GetBottomMenuEntries() []menuItem {
var ret []menuItem
return ret
}
func (wm *generalWebModule) handleStats(w http.ResponseWriter, req *http.Request) {
initRequest(w, req)
type ChannelStat struct {
Name string
MemberCount int
MessageCount int
}
type StatData struct {
TotalChannelMessages int
TotalChannels int
ChannelStats []ChannelStat
Error string
}
// Get the global stats
var s StatData
var err error
openDatabase()
chanlst := getChannelList()
var chanstats []ChannelStat
for _, k := range chanlst {
chanstats = append(chanstats, ChannelStat{
Name: getChannelName(k),
MemberCount: getChannelMemberCount(k),
MessageCount: getChannelMessageCount(k),
})
}
s.ChannelStats = chanstats
s.TotalChannelMessages = getTotalChannelMsgCount()
s.TotalChannels = len(chanlst)
closeDatabase()
sc := "var stats = {totalchannelmessages:"
sc = fmt.Sprintf("%s%d,", sc, s.TotalChannelMessages)
sc = sc + "\"channels\":["
for _, k := range s.ChannelStats {
sc = fmt.Sprintf("%s{name:\"%s\",member_count:%d,message_count:%d},",
sc,
k.Name,
k.MemberCount,
k.MessageCount,
)
}
// Trim the last ,
sc = sc[:len(sc)-1]
sc = sc + "]"
sc = sc + "};"
addToInlineScript(sc)
site.Scripts = append(site.Scripts, "/assets/js/main_stats.js")
if err != nil {
setFlashMessage("Error Counting Channel Messages", "error")
}
site.TemplateData = s
setMenuItemActive("Stats")
showPage("stats.html", site, w)
}

View File

@ -1,5 +1,7 @@
package main
import "net/http"
type levelUpStatProcessor struct{}
func (p *levelUpStatProcessor) GetName() string {
@ -39,3 +41,36 @@ func (p *levelUpStatProcessor) ProcessChannelMessage(m *Message) {
}
func (p *levelUpStatProcessor) ProcessAdminChannelMessage(m *Message) {}
func (p *levelUpStatProcessor) ProcessBotChannelMessage(m *Message) {}
/*
* Web Site Module
*/
type levelUpWebModule struct{}
func (wm *levelUpWebModule) GetName() string {
return "LevelUp Web Module"
}
func (wm *levelUpWebModule) GetRoutes() map[string]func(http.ResponseWriter, *http.Request) {
ret := make(map[string]func(http.ResponseWriter, *http.Request))
//ret["/levelup/"] = wm.
return ret
}
func (wm *levelUpWebModule) Register() {
for k, v := range wm.GetRoutes() {
r.HandleFunc(k, v)
}
}
func (wm *levelUpWebModule) GetMenuEntries() []menuItem {
var ret []menuItem
ret = append(ret, menuItem{Text: "LevelUp!", Link: "/levelup/"})
return ret
}
func (wm *levelUpWebModule) GetBottomMenuEntries() []menuItem {
var ret []menuItem
return ret
}
func (wm *levelUpWebModule) handleLevelUpGeneral(w http.ResponseWriter, req *http.Request) {
initRequest(w, req)
}

View File

@ -55,7 +55,6 @@ func main() {
panic(err)
}
// For now, we're not running the web server
statWebMain(slack)
statBotMain(slack)
}

View File

@ -398,6 +398,7 @@ func addChannelStat(channel string, key string, addVal int) error {
openDatabase()
v, err := getChannelStat(channel, key)
err = saveChannelStat(channel, key, v+addVal)
v, err = getChannelStat(channel, key)
closeDatabase()
return err
}
@ -410,6 +411,55 @@ 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
@ -421,8 +471,8 @@ func getChannelStat(channel string, key string) (int, error) {
if b == nil {
return fmt.Errorf("Unable to open 'channels' bucket")
}
if chB, err = b.CreateBucketIfNotExists([]byte(channel)); err == nil {
if chSB, err = chB.CreateBucketIfNotExists([]byte("stats")); err == nil {
if chB = b.Bucket([]byte(channel)); chB != nil {
if chSB = chB.Bucket([]byte("stats")); chSB != nil {
ret, err = bktGetInt(chSB, key)
return err
}
@ -671,8 +721,8 @@ func getUserStat(user string, key string) (int, error) {
if b == nil {
return fmt.Errorf("Unable to open 'users' bucket")
}
if uB, err = b.CreateBucketIfNotExists([]byte(user)); err == nil {
if uSB, err = uB.CreateBucketIfNotExists([]byte("stats")); err == nil {
if uB = b.Bucket([]byte(user)); uB != nil {
if uSB = uB.Bucket([]byte("stats")); uSB != nil {
ret, err = bktGetInt(uSB, key)
return err
}
@ -705,6 +755,7 @@ 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
}
@ -777,6 +828,52 @@ func getAllUsersStats() (map[string]int, error) {
return ret, err
}
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 {

View File

@ -19,8 +19,9 @@ type SiteData struct {
Port int
SessionName string
Stylesheets []string
Scripts []string
Stylesheets []string
Scripts []string
InlineScript string
Flash flashMessage // Quick message at top of page
Menu []menuItem // Top-aligned menu items
@ -46,6 +47,16 @@ var site SiteData
var sessionStore = sessions.NewCookieStore([]byte("gostatbot secret cookie nobody will guess"))
var r *mux.Router
type webModule interface {
GetName() string
GetRoutes() map[string]func(http.ResponseWriter, *http.Request)
Register()
GetMenuEntries() []menuItem
GetBottomMenuEntries() []menuItem
}
var webModules []webModule
// This is the main function for the web server
func statWebMain(slack *Slack) {
site.Title = "stat_bot"
@ -58,7 +69,9 @@ func statWebMain(slack *Slack) {
assetHandler := http.FileServer(http.Dir("./assets/"))
http.Handle("/assets/", http.StripPrefix("/assets/", assetHandler))
r.HandleFunc("/", handleStats)
registerWebModule(new(generalWebModule))
http.Handle("/", r)
go func() {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", site.Port), context.ClearHandler(http.DefaultServeMux)))
@ -73,33 +86,39 @@ func initRequest(w http.ResponseWriter, req *http.Request) {
site.Stylesheets = append(site.Stylesheets, "https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css")
site.Scripts = make([]string, 0, 0)
site.Scripts = append(site.Scripts, "/assets/js/highcharts.js")
site.Menu = make([]menuItem, 0, 0)
site.Menu = append(site.Menu, menuItem{Text: "Stats", Link: "/stats/"})
site.BottomMenu = make([]menuItem, 0, 0)
site.BottomMenu = append(site.BottomMenu, menuItem{Text: "Admin", Link: "/admin/"})
for _, mod := range webModules {
for _, m := range mod.GetMenuEntries() {
site.Menu = append(site.Menu, m)
}
for _, m := range mod.GetBottomMenuEntries() {
site.BottomMenu = append(site.BottomMenu, m)
}
}
}
func handleStats(w http.ResponseWriter, req *http.Request) {
initRequest(w, req)
type StatData struct {
TotalChannelMessages int
Error string
func registerWebModule(b webModule) {
// Register a web module
// Make sure that we haven't already registered it (or another that has the same route)
for _, mod := range webModules {
if mod.GetName() == b.GetName() {
panic(fmt.Errorf("Attempted to Re-register Web Module %s", b.GetName()))
} else {
for k := range mod.GetRoutes() {
for nk := range b.GetRoutes() {
if k == nk {
panic(fmt.Errorf("Attempted to Re-register Web Route %s", k))
}
}
}
}
}
// Get the global stats
var s StatData
if stats, err := getAllUsersStats(); err == nil {
s = StatData{TotalChannelMessages: stats["channel-message"], Error: ""}
} else {
s = StatData{TotalChannelMessages: -1, Error: fmt.Sprintf("%s", err)}
}
site.TemplateData = s
setMenuItemActive("Stats")
showPage("stats.html", site, w)
b.Register()
webModules = append(webModules, b)
}
// showPage
@ -159,6 +178,14 @@ func getSessionStringValue(key string, w http.ResponseWriter, req *http.Request)
return retVal, nil
}
func setFlashMessage(msg string, stat string) {
site.Flash = flashMessage{Message: msg, Status: stat}
}
func addToInlineScript(s string) {
site.InlineScript = fmt.Sprintf("%s%s", site.InlineScript, s)
}
func assertError(err error, w http.ResponseWriter) bool {
if err != nil {
http.Error(w, err.Error(), 500)

View File

@ -3,5 +3,6 @@
{{ range $i, $v := .Scripts }}
<script src="{{ $v }}"></script>
{{ end }}
<script>{{ .InlineScript }}</script>
</body>
</html>

View File

@ -1,21 +1,6 @@
<div>
devICT Slack!
</div>
<div>
{{ .TemplateData.Error }}
</div>
<div>
<table class="pure-table">
<thead>
<tr>
<th colspan="2">Statistics</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total Channel Messages</td>
<td>{{ .TemplateData.TotalChannelMessages }}</td>
</tr>
</tbody>
</table>
</div>
<div id="mainStatsBarChart"></div>
<hr />
<div id="statsBarChart"></div>