Starting Web Server work
Fixed some issues with stat adding.
This commit is contained in:
parent
12ae393998
commit
d5947f745f
11
assets/css/pure-min.css
vendored
Normal file
11
assets/css/pure-min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
231
assets/css/statbot.css
Normal file
231
assets/css/statbot.css
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
body {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stuff for the Side Menu */
|
||||||
|
/* Add transition to containers so they can push in and out. */
|
||||||
|
#layout,
|
||||||
|
#menu,
|
||||||
|
.menu-link {
|
||||||
|
-webkit-transition: all 0.2s ease-out;
|
||||||
|
-moz-transition: all 0.2s ease-out;
|
||||||
|
-ms-transition: all 0.2s ease-out;
|
||||||
|
-o-transition: all 0.2s ease-out;
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This is the parent `<div>` that contains the menu and the content area. */
|
||||||
|
#layout {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
#layout.active #menu {
|
||||||
|
left: 150px;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout.active .menu-link {
|
||||||
|
left: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The content `<div>` is where all the content goes. */
|
||||||
|
.content {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2em;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
line-height: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content header */
|
||||||
|
.header {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2.5em 2em 0;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
font-size: 3em;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.header h2 {
|
||||||
|
font-weight: 300;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-subhead {
|
||||||
|
margin: 50px 0 20px 0;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The `#menu` `<div>` is the parent `<div>` that contains the `.pure-menu` that
|
||||||
|
appears on the left side of the page.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
margin-left: -150px; /* "#menu" width */
|
||||||
|
width: 150px;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000; /* so the menu or its navicon stays above all content */
|
||||||
|
background: #191818;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
/* All anchors inside the menu should be styled like this. */
|
||||||
|
#menu a {
|
||||||
|
color: #999;
|
||||||
|
border: none;
|
||||||
|
padding: 0.6em 0 0.6em 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove all background/borders, since we are applying them to #menu. */
|
||||||
|
#menu .pure-menu,
|
||||||
|
#menu .pure-menu ul {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add that light border to separate items into groups. */
|
||||||
|
#menu .pure-menu ul,
|
||||||
|
#menu .pure-menu .menu-item-divided {
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
/* Change color of the anchor links on hover/focus. */
|
||||||
|
#menu .pure-menu li a:hover,
|
||||||
|
#menu .pure-menu li a:focus {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This styles the selected menu item `<li>`. */
|
||||||
|
#menu .pure-menu-selected {
|
||||||
|
background: #0b4992;
|
||||||
|
}
|
||||||
|
#menu .pure-menu-heading {
|
||||||
|
background: #1f8dd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This styles a link within a selected menu item `<li>`. */
|
||||||
|
#menu .pure-menu-selected a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This styles the menu heading.
|
||||||
|
*/
|
||||||
|
#menu .pure-menu-heading {
|
||||||
|
font-size: 110%;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Dynamic Button For Responsive Menu -------------------------------------*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
The button to open/close the Menu is custom-made and not part of Pure. Here's
|
||||||
|
how it works:
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
`.menu-link` represents the responsive menu toggle that shows/hides on
|
||||||
|
small screens.
|
||||||
|
*/
|
||||||
|
.menu-link {
|
||||||
|
position: fixed;
|
||||||
|
display: block; /* show this only on small screens */
|
||||||
|
top: 0;
|
||||||
|
left: 0; /* "#menu width" */
|
||||||
|
background: #000;
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
font-size: 10px; /* change this value to increase/decrease button size */
|
||||||
|
z-index: 10;
|
||||||
|
width: 2em;
|
||||||
|
height: auto;
|
||||||
|
padding: 2.1em 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link:hover,
|
||||||
|
.menu-link:focus {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link span {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link span,
|
||||||
|
.menu-link span:before,
|
||||||
|
.menu-link span:after {
|
||||||
|
background-color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link span:before,
|
||||||
|
.menu-link span:after {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: -0.6em;
|
||||||
|
content: " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link span:after {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.menu-list-dropped {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Responsive Styles (Media Queries) ------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hides the menu at `48em`, but modify this based on your app's needs.
|
||||||
|
*/
|
||||||
|
@media (min-width: 48em) {
|
||||||
|
.header,
|
||||||
|
.content {
|
||||||
|
padding-left: 2em;
|
||||||
|
padding-right: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout {
|
||||||
|
padding-left: 150px; /* left col width "#menu" */
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
#menu {
|
||||||
|
left: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link {
|
||||||
|
position: fixed;
|
||||||
|
left: 150px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout.active .menu-link {
|
||||||
|
left: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 48em) {
|
||||||
|
/* Only apply this when the window is small. Otherwise, the following
|
||||||
|
case results in extra padding on the left:
|
||||||
|
* Make the window small.
|
||||||
|
* Tap the menu to trigger the active state.
|
||||||
|
* Make the window large again.
|
||||||
|
*/
|
||||||
|
#layout.active {
|
||||||
|
position: relative;
|
||||||
|
left: 150px;
|
||||||
|
}
|
||||||
|
}
|
110
processor_general.go
Normal file
110
processor_general.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* General Message Processor
|
||||||
|
*/
|
||||||
|
type generalProcessor struct{}
|
||||||
|
|
||||||
|
func (p *generalProcessor) GetName() string {
|
||||||
|
return "stat_bot General Processor"
|
||||||
|
}
|
||||||
|
|
||||||
|
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) ProcessUserMessage(slack *Slack, m *Message) {}
|
||||||
|
|
||||||
|
func (p *generalProcessor) ProcessAdminUserMessage(slack *Slack, m *Message) {
|
||||||
|
// Check if we were mentioned
|
||||||
|
if strings.HasPrefix(m.Text, "<@"+slack.id+">") {
|
||||||
|
parts := strings.Fields(m.Text)
|
||||||
|
var action, target string
|
||||||
|
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
action = parts[1]
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
target = parts[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "mkadmin" && target != "" {
|
||||||
|
// Make a user an admin
|
||||||
|
if strings.HasPrefix(target, "<@") && strings.HasSuffix(target, ">") {
|
||||||
|
target = strings.Trim(target, "<@>")
|
||||||
|
if e := addAdmin(target); e == nil {
|
||||||
|
m.Text = "User <@" + target + "> has been made an admin"
|
||||||
|
} else {
|
||||||
|
m.Text = fmt.Sprintf("%s", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.Text = "Please specify an existing user starting with a '@'"
|
||||||
|
}
|
||||||
|
slack.postMessage(*m)
|
||||||
|
} else if action == "rmadmin" && target != "" {
|
||||||
|
// Revoke a user as an admin
|
||||||
|
if strings.HasPrefix(target, "<@") && strings.HasSuffix(target, ">") {
|
||||||
|
target = strings.Trim(target, "<@>")
|
||||||
|
if e := removeAdmin(target); e == nil {
|
||||||
|
m.Text = "Admin privileges revoked from <@" + target + ">"
|
||||||
|
} else {
|
||||||
|
m.Text = fmt.Sprintf("%s", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.Text = "Please specify an existing user starting with a '@'"
|
||||||
|
}
|
||||||
|
slack.postMessage(*m)
|
||||||
|
} else {
|
||||||
|
// huh?
|
||||||
|
m.Text = fmt.Sprintf("Beep boop beep, that does not compute\n")
|
||||||
|
slack.postMessage(*m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *generalProcessor) ProcessChannelMessage(slack *Slack, m *Message) {}
|
||||||
|
func (p *generalProcessor) ProcessAdminChannelMessage(slack *Slack, m *Message) {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
*General Statistics Processor
|
||||||
|
*/
|
||||||
|
type generalStatProcessor struct{}
|
||||||
|
|
||||||
|
func (p *generalStatProcessor) GetName() string {
|
||||||
|
return "General Statistics"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *generalStatProcessor) GetStatKeys() []string {
|
||||||
|
return []string{
|
||||||
|
"bot-message",
|
||||||
|
"channel-message",
|
||||||
|
"message-hour-*",
|
||||||
|
"message-dow-*",
|
||||||
|
"message-dom-*",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *generalStatProcessor) ProcessUserMessage(m *Message) {
|
||||||
|
incrementUserStat(m.User, "bot-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"))
|
||||||
|
}
|
35
processor_levelup.go
Normal file
35
processor_levelup.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type levelUpStatProcessor struct{}
|
||||||
|
|
||||||
|
func (p *levelUpStatProcessor) GetName() string {
|
||||||
|
return "LevelUp Statistics"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *levelUpStatProcessor) GetStatKeys() []string {
|
||||||
|
return []string{
|
||||||
|
"levelup-*",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *levelUpStatProcessor) ProcessMessage(m *Message) {}
|
||||||
|
|
||||||
|
func (p *levelUpStatProcessor) ProcessUserMessage(m *Message) {}
|
||||||
|
|
||||||
|
func (p *levelUpStatProcessor) ProcessChannelMessage(m *Message) {
|
||||||
|
// levelup XP, for now, is awarded like this:
|
||||||
|
// Message in #random: 1 xp
|
||||||
|
// Message in #general: 2 xp
|
||||||
|
// Message in any other channel: 3 xp + 1 xp for that channel
|
||||||
|
chnl, err := getChannelInfo(m.Channel)
|
||||||
|
if err == nil {
|
||||||
|
if chnl.Name == "random" {
|
||||||
|
addUserStat(m.User, "levelup-xp", 1)
|
||||||
|
} else if chnl.Name == "general" {
|
||||||
|
addUserStat(m.User, "levelup-xp", 2)
|
||||||
|
} else {
|
||||||
|
addUserStat(m.User, "levelup-xp", 3)
|
||||||
|
addUserStat(m.User, "levelup-xp-"+chnl.Name, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
slack.go
34
slack.go
@ -112,9 +112,6 @@ func (s *Slack) getUserInfo(uid string) (*User, error) {
|
|||||||
}
|
}
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
writeToLog("User Lookup: \n")
|
|
||||||
writeToLog(string(body))
|
|
||||||
writeToLog("\n\n")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -143,9 +140,6 @@ func (s *Slack) getChannelInfo(cid string) (*Channel, error) {
|
|||||||
}
|
}
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
writeToLog("Channel Lookup: \n")
|
|
||||||
writeToLog(string(body))
|
|
||||||
writeToLog("\n\n")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -162,6 +156,34 @@ func (s *Slack) getChannelInfo(cid string) (*Channel, error) {
|
|||||||
return respObj.Channel, nil
|
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
|
// These structures represent the response of the Slack API events
|
||||||
// Only some fields are included. The rest are ignored by json.Unmarshal.
|
// Only some fields are included. The rest are ignored by json.Unmarshal.
|
||||||
type responseRtmStart struct {
|
type responseRtmStart struct {
|
||||||
|
122
statbot.go
122
statbot.go
@ -3,23 +3,44 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const programName = "statbot"
|
const programName = "statbot"
|
||||||
|
|
||||||
|
type messageProcessor interface {
|
||||||
|
GetName() string
|
||||||
|
GetHelp() string
|
||||||
|
ProcessMessage(s *Slack, m *Message)
|
||||||
|
ProcessAdminMessage(s *Slack, m *Message)
|
||||||
|
ProcessUserMessage(s *Slack, m *Message)
|
||||||
|
ProcessAdminUserMessage(s *Slack, m *Message)
|
||||||
|
ProcessChannelMessage(s *Slack, m *Message)
|
||||||
|
ProcessAdminChannelMessage(s *Slack, m *Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageProcessors []messageProcessor
|
||||||
|
|
||||||
|
type statProcessor interface {
|
||||||
|
GetName() string
|
||||||
|
GetStatKeys() []string
|
||||||
|
ProcessMessage(m *Message)
|
||||||
|
ProcessUserMessage(m *Message)
|
||||||
|
ProcessChannelMessage(m *Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statProcessors []statProcessor
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) != 2 {
|
if len(os.Args) != 2 {
|
||||||
fmt.Fprintf(os.Stderr, "usage: statbot slack-bot-token\n")
|
fmt.Fprintf(os.Stderr, "usage: statbot slack-bot-token\n")
|
||||||
os.Exit(1)
|
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
|
var err error
|
||||||
|
var slack *Slack
|
||||||
|
|
||||||
if err = initDatabase(); err != nil {
|
if err = initDatabase(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -29,16 +50,30 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For now, we're not running the web server
|
||||||
|
//statWebMain(slack)
|
||||||
|
statBotMain(slack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the main function for the statbot
|
||||||
|
func statBotMain(slack *Slack) {
|
||||||
|
// start a websocket-based Real Time API session
|
||||||
|
registerStatProcessor(new(levelUpStatProcessor))
|
||||||
|
registerStatProcessor(new(generalStatProcessor))
|
||||||
|
|
||||||
|
registerMessageProcessor(new(generalProcessor))
|
||||||
|
|
||||||
fmt.Println("statbot ready, ^C exits")
|
fmt.Println("statbot ready, ^C exits")
|
||||||
|
|
||||||
|
writeToLog("== " + time.Now().Format(time.RFC3339) + " - Bot Started ==\n")
|
||||||
for {
|
for {
|
||||||
// read each incoming message
|
// read each incoming message
|
||||||
m, err := slack.getMessage()
|
m, err := slack.getMessage()
|
||||||
if err != nil {
|
if err == nil {
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
processMessage(slack, &m)
|
processMessage(slack, &m)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
writeToLog("== " + time.Now().Format(time.RFC3339) + " - Bot Stopped ==\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func processMessage(slack *Slack, m *Message) {
|
func processMessage(slack *Slack, m *Message) {
|
||||||
@ -50,51 +85,86 @@ func processMessage(slack *Slack, m *Message) {
|
|||||||
// TODO: Handle reaction_removed messages
|
// TODO: Handle reaction_removed messages
|
||||||
|
|
||||||
if m.Type == "message" {
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we know who the user is
|
// 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 past day, update it.
|
// 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 err != nil || usr.LastUpdated.IsZero() || time.Since(usr.LastUpdated) > (time.Hour*24) {
|
||||||
if u, ue := slack.getUserInfo(m.User); ue == nil {
|
if u, ue := slack.getUserInfo(m.User); ue == nil {
|
||||||
saveUserInfo(u)
|
saveUserInfo(u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If 'channel' is defined, save the message to the 'channels' bucket
|
|
||||||
if m.Channel != "" {
|
if m.Channel != "" {
|
||||||
// Check if we know what the channel is
|
// Check if we know what the channel is
|
||||||
chnl, err := getChannelInfo(m.Channel)
|
chnl, err := getChannelInfo(m.Channel)
|
||||||
// If the channel information hasn't been updated in the past day, update it.
|
// 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) {
|
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
|
// Either we don't have this channel, or it's a direct message
|
||||||
if c, ce := slack.getChannelInfo(m.Channel); ce != nil {
|
if c, ce := slack.getChannelInfo(m.Channel); ce != nil {
|
||||||
// Invalid channel, save as a direct message
|
// Invalid channel, save as a direct message
|
||||||
saveUserMessage(m.User, m)
|
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 {
|
} else {
|
||||||
// Save channel info
|
// Save channel info
|
||||||
saveChannelInfo(c)
|
saveChannelInfo(c)
|
||||||
// And save the channel message
|
// And save the channel message
|
||||||
saveChannelMessage(m.Channel, m)
|
saveChannelMessage(m.Channel, m)
|
||||||
|
for _, proc := range statProcessors {
|
||||||
|
proc.ProcessChannelMessage(m)
|
||||||
|
}
|
||||||
|
for _, proc := range messageProcessors {
|
||||||
|
proc.ProcessChannelMessage(slack, 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 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()))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
messageProcessors = append(messageProcessors, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerStatProcessor(b statProcessor) {
|
||||||
|
// Register a Statistic Processor
|
||||||
|
// First make sure that we don't have any 'key' collisions
|
||||||
|
for _, proc := range statProcessors {
|
||||||
|
for _, testKey := range proc.GetStatKeys() {
|
||||||
|
for _, k := range b.GetStatKeys() {
|
||||||
|
if strings.Replace(testKey, "*", "", -1) == strings.Replace(k, "*", "", -1) {
|
||||||
|
panic(fmt.Errorf("Stat Key Collision (%s=>%s and %s=>%s)",
|
||||||
|
b.GetName(), k,
|
||||||
|
proc.GetName(), testKey,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statProcessors = append(statProcessors, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeToLog(d string) {
|
func writeToLog(d string) {
|
||||||
|
251
statbot_model.go
251
statbot_model.go
@ -12,17 +12,25 @@ const databaseFile = programName + ".db"
|
|||||||
|
|
||||||
var db *bolt.DB
|
var db *bolt.DB
|
||||||
|
|
||||||
|
var dbOpened bool
|
||||||
|
|
||||||
func openDatabase() error {
|
func openDatabase() error {
|
||||||
|
if !dbOpened {
|
||||||
var err error
|
var err error
|
||||||
db, err = bolt.Open(databaseFile, 0600, nil)
|
db, err = bolt.Open(databaseFile, 0600, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
dbOpened = true
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeDatabase() error {
|
func closeDatabase() error {
|
||||||
|
if !dbOpened {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dbOpened = false
|
||||||
return db.Close()
|
return db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +41,11 @@ func initDatabase() error {
|
|||||||
err := db.Update(func(tx *bolt.Tx) error {
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
|
||||||
// The config bucket holds config info for statbot
|
// The config bucket holds config info for statbot
|
||||||
_, err := tx.CreateBucketIfNotExists([]byte("config"))
|
cB, err := tx.CreateBucketIfNotExists([]byte("config"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = cB.CreateBucketIfNotExists([]byte("admins"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -57,6 +69,91 @@ func initDatabase() error {
|
|||||||
return err
|
return 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
|
// Channel object
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@ -174,7 +271,7 @@ func getChannelInfo(chnl string) (*Channel, error) {
|
|||||||
func saveChannelInfo(chnl *Channel) error {
|
func saveChannelInfo(chnl *Channel) error {
|
||||||
openDatabase()
|
openDatabase()
|
||||||
err := db.Update(func(tx *bolt.Tx) error {
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket([]byte("channel"))
|
b := tx.Bucket([]byte("channels"))
|
||||||
var err error
|
var err error
|
||||||
var chB, chIB *bolt.Bucket
|
var chB, chIB *bolt.Bucket
|
||||||
if chB, err = b.CreateBucketIfNotExists([]byte(chnl.ID)); err != nil {
|
if chB, err = b.CreateBucketIfNotExists([]byte(chnl.ID)); err != nil {
|
||||||
@ -273,44 +370,6 @@ func saveChannelStat(channel string, key string, val string) error {
|
|||||||
return err
|
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 {
|
func incrementChannelStat(channel string, key string) error {
|
||||||
openDatabase()
|
openDatabase()
|
||||||
strRet, err := getChannelStat(channel, key)
|
strRet, err := getChannelStat(channel, key)
|
||||||
@ -353,17 +412,42 @@ func getChannelStat(channel string, key string) (string, error) {
|
|||||||
return ret, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message We save the message struct into the db,
|
func saveChannelMessage(channel string, message *Message) error {
|
||||||
// it's also what we send/receive from slack
|
openDatabase()
|
||||||
type Message struct {
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
ID uint64 `json:"id"`
|
b := tx.Bucket([]byte("channels"))
|
||||||
Type string `json:"type"`
|
var err error
|
||||||
Channel string `json:"channel"`
|
var chB, chMB, msgBkt *bolt.Bucket
|
||||||
User string `json:"user"`
|
if chB, err = b.CreateBucketIfNotExists([]byte(channel)); err != nil {
|
||||||
Name string `json:"name"`
|
return err
|
||||||
Text string `json:"text"`
|
}
|
||||||
Ts string `json:"ts"`
|
if chMB, err = chB.CreateBucketIfNotExists([]byte("messages")); err != nil {
|
||||||
Time time.Time
|
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
|
// User object
|
||||||
@ -554,6 +638,69 @@ func saveUserMessage(user string, message *Message) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getUserStat(user string, key string) (int, error) {
|
||||||
|
openDatabase()
|
||||||
|
var ret int
|
||||||
|
err := db.Update(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, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
if err = bktPutInt(uSB, key, val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
closeDatabase()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUserStat(user string, key string, addVal int) error {
|
||||||
|
openDatabase()
|
||||||
|
v, err := getUserStat(user, key)
|
||||||
|
err = saveUserStat(user, key, v+addVal)
|
||||||
|
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 bktGetBucket(b *bolt.Bucket, key string) (*bolt.Bucket, error) {
|
func bktGetBucket(b *bolt.Bucket, key string) (*bolt.Bucket, error) {
|
||||||
bkt := b.Bucket([]byte(key))
|
bkt := b.Bucket([]byte(key))
|
||||||
if bkt != nil {
|
if bkt != nil {
|
||||||
|
155
statbotweb.go
Normal file
155
statbotweb.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/gorilla/context"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SiteData is the basic data needed for the site/pages
|
||||||
|
type SiteData struct {
|
||||||
|
Title string
|
||||||
|
SubTitle string
|
||||||
|
Port int
|
||||||
|
SessionName string
|
||||||
|
|
||||||
|
Stylesheets []string
|
||||||
|
Scripts []string
|
||||||
|
|
||||||
|
Flash flashMessage // Quick message at top of page
|
||||||
|
Menu []menuItem // Top-aligned menu items
|
||||||
|
BottomMenu []menuItem // Bottom-aligned menu items
|
||||||
|
|
||||||
|
// Any other template data
|
||||||
|
TemplateData interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type flashMessage struct {
|
||||||
|
Message string
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
type menuItem struct {
|
||||||
|
Text string
|
||||||
|
Link string
|
||||||
|
Active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var site SiteData
|
||||||
|
|
||||||
|
var sessionStore = sessions.NewCookieStore([]byte("gostatbot secret cookie nobody will guess"))
|
||||||
|
var r *mux.Router
|
||||||
|
|
||||||
|
// This is the main function for the web server
|
||||||
|
func statWebMain(slack *Slack) {
|
||||||
|
site.Title = "stat_bot"
|
||||||
|
site.SubTitle = ""
|
||||||
|
site.Port = 3000
|
||||||
|
site.SessionName = "statbot"
|
||||||
|
|
||||||
|
r = mux.NewRouter()
|
||||||
|
r.StrictSlash(true)
|
||||||
|
|
||||||
|
assetHandler := http.FileServer(http.Dir("./assets/"))
|
||||||
|
http.Handle("/assets/", http.StripPrefix("/assets/", assetHandler))
|
||||||
|
r.HandleFunc("/", handleStats)
|
||||||
|
http.Handle("/", r)
|
||||||
|
go func() {
|
||||||
|
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", site.Port), context.ClearHandler(http.DefaultServeMux)))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initRequest(w http.ResponseWriter, req *http.Request) {
|
||||||
|
site.SubTitle = ""
|
||||||
|
site.Stylesheets = make([]string, 0, 0)
|
||||||
|
site.Stylesheets = append(site.Stylesheets, "/assets/css/pure-min.css")
|
||||||
|
site.Stylesheets = append(site.Stylesheets, "/assets/css/statbot.css")
|
||||||
|
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.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/"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStats(w http.ResponseWriter, req *http.Request) {
|
||||||
|
initRequest(w, req)
|
||||||
|
|
||||||
|
setMenuItemActive("Stats")
|
||||||
|
|
||||||
|
showPage("stats.html", site, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// showPage
|
||||||
|
// Load a template and all of the surrounding templates
|
||||||
|
func showPage(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
|
||||||
|
for _, tmpl := range []string{
|
||||||
|
"htmlheader.html",
|
||||||
|
"menu.html",
|
||||||
|
"header.html",
|
||||||
|
tmplName,
|
||||||
|
"footer.html",
|
||||||
|
"htmlfooter.html",
|
||||||
|
} {
|
||||||
|
if err := outputTemplate(tmpl, tmplData, w); err != nil {
|
||||||
|
writeToLog(fmt.Sprintf("%s\n", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputTemplate
|
||||||
|
// Spit out a template
|
||||||
|
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
|
||||||
|
_, err := os.Stat("templates/" + tmplName)
|
||||||
|
if err == nil {
|
||||||
|
t := template.New(tmplName)
|
||||||
|
t, _ = t.ParseFiles("templates/" + tmplName)
|
||||||
|
return t.Execute(w, tmplData)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("WebServer: Cannot load template (templates/%s): File not found", tmplName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setMenuItemActive
|
||||||
|
// Sets a menu item to active, all others to inactive
|
||||||
|
func setMenuItemActive(which string) {
|
||||||
|
for i := range site.Menu {
|
||||||
|
if site.Menu[i].Text == which {
|
||||||
|
site.Menu[i].Active = true
|
||||||
|
} else {
|
||||||
|
site.Menu[i].Active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSessionStringValue(key string, w http.ResponseWriter, req *http.Request) (string, error) {
|
||||||
|
session, err := sessionStore.Get(req, site.SessionName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
val := session.Values[key]
|
||||||
|
var retVal string
|
||||||
|
var ok bool
|
||||||
|
if retVal, ok = val.(string); !ok {
|
||||||
|
return "", fmt.Errorf("Unable to create string from %s", key)
|
||||||
|
}
|
||||||
|
return retVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertError(err error, w http.ResponseWriter) bool {
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
1
templates/footer.html
Normal file
1
templates/footer.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
</div>
|
8
templates/header.html
Normal file
8
templates/header.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<div class="content">
|
||||||
|
<aside class="center {{ .Flash.Status }}">
|
||||||
|
{{ .Flash.Message }}
|
||||||
|
</aside>
|
||||||
|
<div class="header">
|
||||||
|
<h1>{{.Title}}</h1>
|
||||||
|
<h2>{{.SubTitle}}</h2>
|
||||||
|
</div>
|
7
templates/htmlfooter.html
Normal file
7
templates/htmlfooter.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
</div>
|
||||||
|
<!-- TODO: Add Dynamic Scripts -->
|
||||||
|
{{ range $i, $v := .Scripts }}
|
||||||
|
<script src="{{ $v }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
</body>
|
||||||
|
</html>
|
21
templates/htmlheader.html
Normal file
21
templates/htmlheader.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta http-equiv="Cache-control" content="No-Cache">
|
||||||
|
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="http://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
{{ range $i, $v := .Stylesheets }}
|
||||||
|
<link rel="stylesheet" href="{{ $v }}">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="layout">
|
24
templates/menu.html
Normal file
24
templates/menu.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!-- Menu Toggle -->
|
||||||
|
<a id="menuLink" class="menu-link" href="#menu">
|
||||||
|
<!-- Hamburger icon -->
|
||||||
|
<span></span>
|
||||||
|
</a>
|
||||||
|
<div id="menu">
|
||||||
|
<div class="pure-menu">
|
||||||
|
<a class="pure-menu-heading" href="/">{{.Title}}</a>
|
||||||
|
<ul class="pure-menu-list">
|
||||||
|
{{ range $i, $v := .Menu }}
|
||||||
|
<li class="pure-menu-item {{ if $v.Active }} pure-menu-selected {{ end }}">
|
||||||
|
<a class="pure-menu-link" href="{{ $v.Link }}">{{ $v.Text }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
<ul class="pure-menu-list menu-list-dropped">
|
||||||
|
{{ range $i, $v := .BottomMenu }}
|
||||||
|
<li class="pure-menu-item {{ if $v.Active }} pure-menu-selected {{ end }}">
|
||||||
|
<a class="pure-menu-link" href="{{ $v.Link }}">{{ $v.Text }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
3
templates/stats.html
Normal file
3
templates/stats.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
devICT Slack Statistics!
|
||||||
|
</div>
|
Loading…
Reference in New Issue
Block a user