From d5947f745faafeeea32a00e0c0e417f406ba9d0c Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 29 Oct 2015 11:16:25 -0500 Subject: [PATCH] Starting Web Server work Fixed some issues with stat adding. --- assets/css/pure-min.css | 11 ++ assets/css/statbot.css | 231 +++++++++++++++++++++++++++++++++ processor_general.go | 110 ++++++++++++++++ processor_levelup.go | 35 +++++ slack.go | 34 ++++- statbot.go | 126 ++++++++++++++---- statbot_model.go | 261 +++++++++++++++++++++++++++++--------- statbotweb.go | 155 ++++++++++++++++++++++ templates/footer.html | 1 + templates/header.html | 8 ++ templates/htmlfooter.html | 7 + templates/htmlheader.html | 21 +++ templates/menu.html | 24 ++++ templates/stats.html | 3 + 14 files changed, 936 insertions(+), 91 deletions(-) create mode 100644 assets/css/pure-min.css create mode 100644 assets/css/statbot.css create mode 100644 processor_general.go create mode 100644 processor_levelup.go create mode 100644 statbotweb.go create mode 100644 templates/footer.html create mode 100644 templates/header.html create mode 100644 templates/htmlfooter.html create mode 100644 templates/htmlheader.html create mode 100644 templates/menu.html create mode 100644 templates/stats.html diff --git a/assets/css/pure-min.css b/assets/css/pure-min.css new file mode 100644 index 0000000..f0aa374 --- /dev/null +++ b/assets/css/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v0.6.0 +Copyright 2014 Yahoo! Inc. All rights reserved. +Licensed under the BSD License. +https://github.com/yahoo/pure/blob/master/LICENSE.md +*/ +/*! +normalize.css v^3.0 | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-link,.pure-menu-heading{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-disabled,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/assets/css/statbot.css b/assets/css/statbot.css new file mode 100644 index 0000000..381f806 --- /dev/null +++ b/assets/css/statbot.css @@ -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 `
` 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 `
` 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` `
` is the parent `
` 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 `
  • `. */ +#menu .pure-menu-selected { + background: #0b4992; +} +#menu .pure-menu-heading { + background: #1f8dd6; +} + +/* This styles a link within a selected menu item `
  • `. */ +#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; + } +} diff --git a/processor_general.go b/processor_general.go new file mode 100644 index 0000000..90910b0 --- /dev/null +++ b/processor_general.go @@ -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")) +} diff --git a/processor_levelup.go b/processor_levelup.go new file mode 100644 index 0000000..bcc0c77 --- /dev/null +++ b/processor_levelup.go @@ -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) + } + } +} diff --git a/slack.go b/slack.go index 10326bb..2e3b798 100644 --- a/slack.go +++ b/slack.go @@ -112,9 +112,6 @@ func (s *Slack) getUserInfo(uid string) (*User, error) { } 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 } @@ -143,9 +140,6 @@ func (s *Slack) getChannelInfo(cid string) (*Channel, error) { } 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 } @@ -162,6 +156,34 @@ 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 { diff --git a/statbot.go b/statbot.go index 3aa4c54..8dd708f 100644 --- a/statbot.go +++ b/statbot.go @@ -3,23 +3,44 @@ package main import ( "encoding/json" "fmt" - "log" "os" + "strings" "time" ) 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() { 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 + var slack *Slack if err = initDatabase(); err != nil { panic(err) @@ -29,16 +50,30 @@ func main() { 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") + writeToLog("== " + time.Now().Format(time.RFC3339) + " - Bot Started ==\n") for { // read each incoming message m, err := slack.getMessage() - if err != nil { - log.Fatal(err) + if err == nil { + processMessage(slack, &m) } - processMessage(slack, &m) } + writeToLog("== " + time.Now().Format(time.RFC3339) + " - Bot Stopped ==\n\n") } func processMessage(slack *Slack, m *Message) { @@ -50,53 +85,88 @@ func processMessage(slack *Slack, m *Message) { // TODO: Handle reaction_removed messages 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 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 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 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) { // 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) + for _, proc := range statProcessors { + proc.ProcessUserMessage(m) + } + for _, proc := range messageProcessors { + if isAdmin(m.User) { + proc.ProcessAdminUserMessage(slack, m) + } + proc.ProcessUserMessage(slack, m) + } } else { // Save channel info saveChannelInfo(c) // And save the channel message 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) { f, err := os.OpenFile("statbot.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0664) if err != nil { diff --git a/statbot_model.go b/statbot_model.go index c79300b..cc176ab 100644 --- a/statbot_model.go +++ b/statbot_model.go @@ -12,17 +12,25 @@ 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 - } +var dbOpened bool +func openDatabase() error { + if !dbOpened { + var err error + db, err = bolt.Open(databaseFile, 0600, nil) + if err != nil { + return err + } + dbOpened = true + } return nil } func closeDatabase() error { + if !dbOpened { + return nil + } + dbOpened = false return db.Close() } @@ -33,7 +41,11 @@ func initDatabase() error { err := db.Update(func(tx *bolt.Tx) error { // 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 { return err } @@ -57,6 +69,91 @@ func initDatabase() error { 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 type Channel struct { ID string `json:"id"` @@ -174,7 +271,7 @@ func getChannelInfo(chnl string) (*Channel, error) { func saveChannelInfo(chnl *Channel) error { openDatabase() err := db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("channel")) + b := tx.Bucket([]byte("channels")) var err error var chB, chIB *bolt.Bucket 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 } -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) @@ -353,17 +412,42 @@ func getChannelStat(channel string, key string) (string, error) { 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 +func saveChannelMessage(channel string, message *Message) error { + openDatabase() + err := db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("channels")) + var err error + var chB, chMB, msgBkt *bolt.Bucket + if chB, err = b.CreateBucketIfNotExists([]byte(channel)); err != nil { + return err + } + if chMB, err = chB.CreateBucketIfNotExists([]byte("messages")); err != nil { + return err + } + idx, _ := chMB.NextSequence() + idxKey := []byte(strconv.FormatUint(idx, 10)) + if msgBkt, err = chMB.CreateBucketIfNotExists(idxKey); err != nil { + return err + } + if err = bktPutString(msgBkt, "type", message.Type); err != nil { + return err + } + if err = bktPutString(msgBkt, "user", message.User); err != nil { + return err + } + if err = bktPutString(msgBkt, "text", message.Text); err != nil { + return err + } + if err = bktPutString(msgBkt, "name", message.Name); err != nil { + return err + } + if err = bktPutTime(msgBkt, "time", message.Time); err != nil { + return err + } + return nil + }) + closeDatabase() + return err } // User object @@ -554,6 +638,69 @@ func saveUserMessage(user string, message *Message) error { 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) { bkt := b.Bucket([]byte(key)) if bkt != nil { diff --git a/statbotweb.go b/statbotweb.go new file mode 100644 index 0000000..9deb500 --- /dev/null +++ b/statbotweb.go @@ -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 +} diff --git a/templates/footer.html b/templates/footer.html new file mode 100644 index 0000000..04f5b84 --- /dev/null +++ b/templates/footer.html @@ -0,0 +1 @@ +
  • diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..ea77f6f --- /dev/null +++ b/templates/header.html @@ -0,0 +1,8 @@ +
    + +
    +

    {{.Title}}

    +

    {{.SubTitle}}

    +
    diff --git a/templates/htmlfooter.html b/templates/htmlfooter.html new file mode 100644 index 0000000..bb84825 --- /dev/null +++ b/templates/htmlfooter.html @@ -0,0 +1,7 @@ +
    + + {{ range $i, $v := .Scripts }} + + {{ end }} + + diff --git a/templates/htmlheader.html b/templates/htmlheader.html new file mode 100644 index 0000000..3ecf362 --- /dev/null +++ b/templates/htmlheader.html @@ -0,0 +1,21 @@ + + + + + + + + + {{.Title}} + + + + {{ range $i, $v := .Stylesheets }} + + {{ end }} + + + +
    diff --git a/templates/menu.html b/templates/menu.html new file mode 100644 index 0000000..8464808 --- /dev/null +++ b/templates/menu.html @@ -0,0 +1,24 @@ + + + + + + diff --git a/templates/stats.html b/templates/stats.html new file mode 100644 index 0000000..12b916e --- /dev/null +++ b/templates/stats.html @@ -0,0 +1,3 @@ +
    + devICT Slack Statistics! +