Added support for Websockets.

The Control Panel Dashboard now updates every second.
You can now see how many guests and users are online via the Control Panel Dashboard.
The Control Panel Dashboard is now a little more mobile friendly.
This commit is contained in:
Azareal 2017-05-11 14:04:43 +01:00
parent fab2db0936
commit 3b5f48b5a2
25 changed files with 625 additions and 82 deletions

View File

@ -28,12 +28,14 @@ A plugin system. More on this to come.
A responsive design. Looks great on mobile phones, tablets, laptops, desktops and more! A responsive design. Looks great on mobile phones, tablets, laptops, desktops and more!
Other modern features like alerts, advanced dashboard, etc.
# Dependencies # Dependencies
Go 1.7. You will need to install this. Pick the .msi, if you want everything sorted out for you rather than having to go around updating the environment settings. https://golang.org/doc/install Go 1.8 - You will need to install this. Pick the .msi, if you want everything sorted out for you rather than having to go around updating the environment settings. https://golang.org/doc/install
MySQL Database. You will need to setup a MySQL Database somewhere. A MariaDB Database works equally well and is much faster than MySQL. You could use something like WNMP / XAMPP which have a little PHP script called PhpMyAdmin for managing MySQL databases or you could install MariaDB directly. MySQL Database - You will need to setup a MySQL Database somewhere. A MariaDB Database works equally well and is much faster than MySQL. You could use something like WNMP / XAMPP which have a little PHP script called PhpMyAdmin for managing MySQL databases or you could install MariaDB directly.
Download the .msi installer from [MariaDB](https://mariadb.com/downloads) and run that. You may want to set it up as a service to avoid running it every-time the computer starts up. Download the .msi installer from [MariaDB](https://mariadb.com/downloads) and run that. You may want to set it up as a service to avoid running it every-time the computer starts up.
@ -122,6 +124,8 @@ We're looking for ways to clean-up the plugin system so that all of them (except
* github.com/StackExchange/wmi Dependency for gopsutil on Windows. * github.com/StackExchange/wmi Dependency for gopsutil on Windows.
* github.com/gorilla/websocket Needed for Gosora's Optional WebSockets Module.
# Bundled Plugins # Bundled Plugins
There are several plugins which are bundled with the software by default. These cover various common tasks which aren't common enough to clutter the core with or which have competing implementation methods (E.g. plugin_markdown vs plugin_bbcode for post mark-up). There are several plugins which are bundled with the software by default. These cover various common tasks which aren't common enough to clutter the core with or which have competing implementation methods (E.g. plugin_markdown vs plugin_bbcode for post mark-up).

View File

@ -0,0 +1,4 @@
echo "Building Gosora"
go build -o Gosora -tags no_ws
echo "Building the installer"
go build ./install

30
build-nowebsockets.bat Normal file
View File

@ -0,0 +1,30 @@
@echo off
echo Generating the dynamic code
go generate
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Building the executable
go build -o gosora.exe -tags no_ws
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Building the installer
go build ./install
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Building the router generator
go build ./router_gen
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Gosora was successfully built
pause

BIN
images/panel-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -4,6 +4,8 @@ echo "Installing bcrypt"
go get -u golang.org/x/crypto/bcrypt go get -u golang.org/x/crypto/bcrypt
echo "Installing gopsutil" echo "Installing gopsutil"
go get -u github.com/shirou/gopsutil go get -u github.com/shirou/gopsutil
echo "Installing Gorilla WebSockets"
go get -u github.com/gorilla/websocket
echo "Preparing the installer" echo "Preparing the installer"
go generate go generate

View File

@ -20,6 +20,11 @@ if %errorlevel% neq 0 (
pause pause
exit /b %errorlevel% exit /b %errorlevel%
) )
go get -u github.com/gorilla/websocket
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Preparing the installer echo Preparing the installer
go generate go generate
@ -27,7 +32,7 @@ if %errorlevel% neq 0 (
pause pause
exit /b %errorlevel% exit /b %errorlevel%
) )
go build go build -o gosora.exe
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
pause pause
exit /b %errorlevel% exit /b %errorlevel%

View File

@ -27,6 +27,7 @@ const gigabyte int = megabyte * 1024
const terabyte int = gigabyte * 1024 const terabyte int = gigabyte * 1024
const saltLength int = 32 const saltLength int = 32
const sessionLength int = 80 const sessionLength int = 80
var enable_websockets bool = false // Don't change this, the value is overwritten by an initialiser
var templates = template.New("") var templates = template.New("")
var no_css_tmpl = template.CSS("") var no_css_tmpl = template.CSS("")
@ -273,6 +274,7 @@ func main(){
///router.HandleFunc("/api/", route_api) ///router.HandleFunc("/api/", route_api)
//router.HandleFunc("/exit/", route_exit) //router.HandleFunc("/exit/", route_exit)
///router.HandleFunc("/", default_route) ///router.HandleFunc("/", default_route)
router.HandleFunc("/ws/", route_websockets)
defer db.Close() defer db.Close()
//if profiling { //if profiling {

View File

@ -1,13 +1,15 @@
package main package main
import "log" import (
import "fmt" "log"
import "strconv" // "fmt"
import "net" "strconv"
import "net/http" "net"
import "html" "net/http"
import "database/sql" "html"
import _ "github.com/go-sql-driver/mysql" "database/sql"
_ "github.com/go-sql-driver/mysql"
)
func route_edit_topic(w http.ResponseWriter, r *http.Request) { func route_edit_topic(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm() err := r.ParseForm()
@ -20,8 +22,7 @@ func route_edit_topic(w http.ResponseWriter, r *http.Request) {
is_js = "0" is_js = "0"
} }
var tid int var tid, fid int
var fid int
tid, err = strconv.Atoi(r.URL.Path[len("/topic/edit/submit/"):]) tid, err = strconv.Atoi(r.URL.Path[len("/topic/edit/submit/"):])
if err != nil { if err != nil {
PreErrorJSQ("The provided TopicID is not a valid number.",w,r,is_js) PreErrorJSQ("The provided TopicID is not a valid number.",w,r,is_js)
@ -104,7 +105,7 @@ func route_edit_topic(w http.ResponseWriter, r *http.Request) {
if is_js == "0" { if is_js == "0" {
http.Redirect(w,r,"/topic/" + strconv.Itoa(tid),http.StatusSeeOther) http.Redirect(w,r,"/topic/" + strconv.Itoa(tid),http.StatusSeeOther)
} else { } else {
fmt.Fprintf(w,`{"success":"1"}`) w.Write(success_json_bytes)
} }
} }
@ -116,8 +117,7 @@ func route_delete_topic(w http.ResponseWriter, r *http.Request) {
} }
var content string var content string
var createdBy int var createdBy, fid int
var fid int
err = db.QueryRow("select content, createdBy, parentID from topics where tid = ?", tid).Scan(&content, &createdBy, &fid) err = db.QueryRow("select content, createdBy, parentID from topics where tid = ?", tid).Scan(&content, &createdBy, &fid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
PreError("The topic you tried to delete doesn't exist.",w,r) PreError("The topic you tried to delete doesn't exist.",w,r)
@ -344,7 +344,7 @@ func route_reply_edit_submit(w http.ResponseWriter, r *http.Request) {
if is_js == "0" { if is_js == "0" {
http.Redirect(w,r, "/topic/" + strconv.Itoa(tid) + "#reply-" + strconv.Itoa(rid), http.StatusSeeOther) http.Redirect(w,r, "/topic/" + strconv.Itoa(tid) + "#reply-" + strconv.Itoa(rid), http.StatusSeeOther)
} else { } else {
fmt.Fprintf(w,`{"success":"1"}`) w.Write(success_json_bytes)
} }
} }
@ -365,9 +365,8 @@ func route_reply_delete_submit(w http.ResponseWriter, r *http.Request) {
return return
} }
var tid int var tid, createdBy int
var content string var content string
var createdBy int
err = db.QueryRow("select tid, content, createdBy from replies where rid = ?", rid).Scan(&tid, &content, &createdBy) err = db.QueryRow("select tid, content, createdBy from replies where rid = ?", rid).Scan(&tid, &content, &createdBy)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
PreErrorJSQ("The reply you tried to delete doesn't exist.",w,r,is_js) PreErrorJSQ("The reply you tried to delete doesn't exist.",w,r,is_js)
@ -405,7 +404,7 @@ func route_reply_delete_submit(w http.ResponseWriter, r *http.Request) {
if is_js == "0" { if is_js == "0" {
//http.Redirect(w,r, "/topic/" + strconv.Itoa(tid), http.StatusSeeOther) //http.Redirect(w,r, "/topic/" + strconv.Itoa(tid), http.StatusSeeOther)
} else { } else {
fmt.Fprintf(w,`{"success":"1"}`) w.Write(success_json_bytes)
} }
wcount := word_count(content) wcount := word_count(content)
@ -482,7 +481,7 @@ func route_profile_reply_edit_submit(w http.ResponseWriter, r *http.Request) {
if is_js == "0" { if is_js == "0" {
http.Redirect(w,r,"/user/" + strconv.Itoa(uid) + "#reply-" + strconv.Itoa(rid), http.StatusSeeOther) http.Redirect(w,r,"/user/" + strconv.Itoa(uid) + "#reply-" + strconv.Itoa(rid), http.StatusSeeOther)
} else { } else {
fmt.Fprintf(w,`{"success":"1"}`) w.Write(success_json_bytes)
} }
} }
@ -533,7 +532,7 @@ func route_profile_reply_delete_submit(w http.ResponseWriter, r *http.Request) {
if is_js == "0" { if is_js == "0" {
//http.Redirect(w,r, "/user/" + strconv.Itoa(uid), http.StatusSeeOther) //http.Redirect(w,r, "/user/" + strconv.Itoa(uid), http.StatusSeeOther)
} else { } else {
fmt.Fprintf(w,`{"success":"1"}`) w.Write(success_json_bytes)
} }
} }

22
no_websockets.go Normal file
View File

@ -0,0 +1,22 @@
// +build no_ws
package main
import "net/http"
var ws_hub WS_Hub
type WS_Hub struct
{
}
func (_ *WS_Hub) GuestCount() int {
return 0
}
func (_ *WS_Hub) UserCount() int {
return 0
}
func route_websockets(_ http.ResponseWriter, _ *http.Request) {
}

View File

@ -79,6 +79,7 @@ type CreateTopicPage struct
type GridElement struct type GridElement struct
{ {
ID string
Body string Body string
Order int // For future use Order int // For future use
Class string Class string

View File

@ -34,19 +34,9 @@ func route_panel(w http.ResponseWriter, r *http.Request){
if err != nil { if err != nil {
cpustr = "Unknown" cpustr = "Unknown"
} else { } else {
/*cpures, _ := cpu.Times(true)
totcpu := cpures[0].Idle + cpures[0].System + cpures[0].User
fmt.Println("System",cpures[0].System)
fmt.Println("User",cpures[0].User)
fmt.Println("Usage",cpures[0].System + cpures[0].User)
fmt.Println("Idle",cpures[0].Idle)
fmt.Println("Gap",totcpu - (cpures[0].System + cpures[0].User))
perc := ((cpures[0].System + + cpures[0].User) * 100) / totcpu
fmt.Println("Perc",perc)
fmt.Println("Perc2",perc2)*/
calcperc := int(perc2[0]) / runtime.NumCPU() calcperc := int(perc2[0]) / runtime.NumCPU()
cpustr = strconv.Itoa(calcperc) cpustr = strconv.Itoa(calcperc)
if calcperc < 25 { if calcperc < 30 {
cpuColour = "stat_green" cpuColour = "stat_green"
} else if calcperc < 75 { } else if calcperc < 75 {
cpuColour = "stat_orange" cpuColour = "stat_orange"
@ -99,9 +89,9 @@ func route_panel(w http.ResponseWriter, r *http.Request){
var postInterval string = "day" var postInterval string = "day"
var postColour string var postColour string
if postCount > 10 { if postCount > 25 {
postColour = "stat_green" postColour = "stat_green"
} else if postCount > 0 { } else if postCount > 5 {
postColour = "stat_orange" postColour = "stat_orange"
} else { } else {
postColour = "stat_red" postColour = "stat_red"
@ -116,7 +106,7 @@ func route_panel(w http.ResponseWriter, r *http.Request){
var topicInterval string = "day" var topicInterval string = "day"
var topicColour string var topicColour string
if topicCount > 10 { if topicCount > 8 {
topicColour = "stat_green" topicColour = "stat_green"
} else if topicCount > 0 { } else if topicCount > 0 {
topicColour = "stat_orange" topicColour = "stat_orange"
@ -141,23 +131,60 @@ func route_panel(w http.ResponseWriter, r *http.Request){
var newUserInterval string = "week" var newUserInterval string = "week"
var gridElements []GridElement = []GridElement{ var gridElements []GridElement = []GridElement{
GridElement{"v" + version.String(),0,"grid_istat stat_green","","","Gosora is up-to-date :)"}, GridElement{"dash-version","v" + version.String(),0,"grid_istat stat_green","","","Gosora is up-to-date :)"},
GridElement{"CPU: " + cpustr + "%",1,"grid_istat " + cpuColour,"","","The global CPU usage of this server"}, GridElement{"dash-cpu","CPU: " + cpustr + "%",1,"grid_istat " + cpuColour,"","","The global CPU usage of this server"},
GridElement{"RAM: " + ramstr,2,"grid_istat " + ramColour,"","","The global RAM usage of this server"}, GridElement{"dash-ram","RAM: " + ramstr,2,"grid_istat " + ramColour,"","","The global RAM usage of this server"},
GridElement{strconv.Itoa(postCount) + " posts / " + postInterval,3,"grid_stat " + postColour,"","","The number of new posts over the last 24 hours"},
GridElement{strconv.Itoa(topicCount) + " topics / " + topicInterval,4,"grid_stat " + topicColour,"","","The number of new topics over the last 24 hours"},
GridElement{"20 online / day",5,"grid_stat stat_disabled","","","Coming Soon!"/*"The people online over the last 24 hours"*/},
GridElement{"8 searches / week",6,"grid_stat stat_disabled","","","Coming Soon!"/*"The number of searches over the last 7 days"*/},
GridElement{strconv.Itoa(newUserCount) + " new users / " + newUserInterval,7,"grid_stat","","","The number of new users over the last 7 days"},
GridElement{strconv.Itoa(reportCount) + " reports / " + reportInterval,8,"grid_stat","","","The number of reports over the last 7 days"},
GridElement{"2 minutes / user / week",9,"grid_stat stat_disabled","","","Coming Soon!"/*"The average number of number of minutes spent by each active user over the last 7 days"*/},
GridElement{"2 visitors / week",10,"grid_stat stat_disabled","","","Coming Soon!"/*"The number of unique visitors we've had over the last 7 days"*/},
GridElement{"5 posts / user / week",11,"grid_stat stat_disabled","","","Coming Soon!"/*"The average number of posts made by each active user over the past week"*/},
} }
if enable_websockets {
uonline := ws_hub.UserCount()
gonline := ws_hub.GuestCount()
totonline := uonline + gonline
var onlineColour string
if totonline > 10 {
onlineColour = "stat_green"
} else if totonline > 3 {
onlineColour = "stat_orange"
} else {
onlineColour = "stat_red"
}
var onlineGuestsColour string
if gonline > 10 {
onlineGuestsColour = "stat_green"
} else if gonline > 1 {
onlineGuestsColour = "stat_orange"
} else {
onlineGuestsColour = "stat_red"
}
var onlineUsersColour string
if uonline > 5 {
onlineUsersColour = "stat_green"
} else if uonline > 1 {
onlineUsersColour = "stat_orange"
} else {
onlineUsersColour = "stat_red"
}
gridElements = append(gridElements, GridElement{"dash-totonline",strconv.Itoa(totonline) + " online",3,"grid_stat " + onlineColour,"","","The number of people who are currently online"})
gridElements = append(gridElements, GridElement{"dash-gonline",strconv.Itoa(gonline) + " guests online",4,"grid_stat " + onlineGuestsColour,"","","The number of guests who are currently online"})
gridElements = append(gridElements, GridElement{"dash-uonline",strconv.Itoa(uonline) + " users online",5,"grid_stat " + onlineUsersColour,"","","The number of logged-in users who are currently online"})
}
gridElements = append(gridElements, GridElement{"dash-postsperday",strconv.Itoa(postCount) + " posts / " + postInterval,6,"grid_stat " + postColour,"","","The number of new posts over the last 24 hours"})
gridElements = append(gridElements, GridElement{"dash-topicsperday",strconv.Itoa(topicCount) + " topics / " + topicInterval,7,"grid_stat " + topicColour,"","","The number of new topics over the last 24 hours"})
gridElements = append(gridElements, GridElement{"dash-totonlineperday","20 online / day",8,"grid_stat stat_disabled","","","Coming Soon!"/*"The people online over the last 24 hours"*/})
gridElements = append(gridElements, GridElement{"dash-searches","8 searches / week",9,"grid_stat stat_disabled","","","Coming Soon!"/*"The number of searches over the last 7 days"*/})
gridElements = append(gridElements, GridElement{"dash-newusers",strconv.Itoa(newUserCount) + " new users / " + newUserInterval,10,"grid_stat","","","The number of new users over the last 7 days"})
gridElements = append(gridElements, GridElement{"dash-reports",strconv.Itoa(reportCount) + " reports / " + reportInterval,11,"grid_stat","","","The number of reports over the last 7 days"})
gridElements = append(gridElements, GridElement{"dash-minperuser","2 minutes / user / week",12,"grid_stat stat_disabled","","","Coming Soon!"/*"The average number of number of minutes spent by each active user over the last 7 days"*/})
gridElements = append(gridElements, GridElement{"dash-visitorsperweek","2 visitors / week",13,"grid_stat stat_disabled","","","Coming Soon!"/*"The number of unique visitors we've had over the last 7 days"*/})
gridElements = append(gridElements, GridElement{"dash-postsperuser","5 posts / user / week",14,"grid_stat stat_disabled","","","Coming Soon!"/*"The average number of posts made by each active user over the past week"*/})
pi := PanelDashboardPage{"Control Panel Dashboard",user,noticeList,gridElements,nil} pi := PanelDashboardPage{"Control Panel Dashboard",user,noticeList,gridElements,nil}
templates.ExecuteTemplate(w,"panel-dashboard.html",pi) templates.ExecuteTemplate(w,"panel-dashboard.html",pi)
} }
@ -388,7 +415,7 @@ func route_panel_forums_edit_submit(w http.ResponseWriter, r *http.Request, sfid
if is_js == "0" { if is_js == "0" {
http.Redirect(w,r,"/panel/forums/",http.StatusSeeOther) http.Redirect(w,r,"/panel/forums/",http.StatusSeeOther)
} else { } else {
fmt.Fprintf(w,`{"success":"1"}`) w.Write(success_json_bytes)
} }
} }

View File

@ -73,6 +73,66 @@ function load_alerts(menu_alerts)
} }
$(document).ready(function(){ $(document).ready(function(){
function SplitN(data,ch,n) {
var out = []
if(data.length == 0) {
return out
}
var lastIndex = 0
var j = 0
var lastN = 1
for(var i = 0; i < data.length; i++) {
if(data[i] == ch) {
out[j++] = data.substring(lastIndex,i)
lastIndex = i
if(lastN == n) {
break
}
lastN++
}
}
if(data.length > lastIndex) {
out[out.length - 1] += data.substring(lastIndex)
}
return out
}
if(window["WebSocket"]) {
conn = new WebSocket("ws://" + document.location.host + "/ws/")
conn.onopen = function() {
conn.send("page " + document.location.pathname + '\r')
}
conn.onclose = function() {
conn = false
}
conn.onmessage = function(event) {
//console.log("WS_Message:")
//console.log(event.data)
var messages = event.data.split('\r')
for(var i = 0; i < messages.length; i++) {
//console.log("Message:")
//console.log(messages[i])
if(messages[i].startsWith("set ")) {
//msgblocks = messages[i].split(' ',3)
msgblocks = SplitN(messages[i]," ",3)
if(msgblocks.length < 3) {
continue
}
document.querySelector(msgblocks[1]).innerHTML = msgblocks[2]
} else if(messages[i].startsWith("set-class ")) {
msgblocks = SplitN(messages[i]," ",3)
if(msgblocks.length < 3) {
continue
}
document.querySelector(msgblocks[1]).className = msgblocks[2]
}
}
}
} else {
conn = false
}
$(".open_edit").click(function(event){ $(".open_edit").click(function(event){
//console.log("Clicked on edit"); //console.log("Clicked on edit");
event.preventDefault(); event.preventDefault();

View File

@ -1,20 +1,22 @@
/* Copyright Azareal 2016 - 2017 */ /* Copyright Azareal 2016 - 2017 */
package main package main
import "log" import (
//import "fmt" "log"
import "strconv" // "fmt"
import "bytes" "strconv"
import "regexp" "bytes"
import "strings" "regexp"
import "time" "strings"
import "io" "time"
import "os" "io"
import "net" "os"
import "net/http" "net"
import "html" "net/http"
import "html/template" "html"
import "database/sql" "html/template"
"database/sql"
)
import _ "github.com/go-sql-driver/mysql" import _ "github.com/go-sql-driver/mysql"
import "golang.org/x/crypto/bcrypt" import "golang.org/x/crypto/bcrypt"
@ -22,6 +24,7 @@ import "golang.org/x/crypto/bcrypt"
// A blank list to fill out that parameter in Page for routes which don't use it // A blank list to fill out that parameter in Page for routes which don't use it
var tList []interface{} var tList []interface{}
var nList []string var nList []string
var success_json_bytes []byte = []byte(`{"success":"1"}`)
// GET functions // GET functions
func route_static(w http.ResponseWriter, r *http.Request){ func route_static(w http.ResponseWriter, r *http.Request){

View File

@ -0,0 +1,6 @@
echo "Generating the dynamic code"
go generate
echo "Building Gosora"
go build -o Gosora -tags no_ws
echo "Running Gosora"
./Gosora

27
run-nowebsockets.bat Normal file
View File

@ -0,0 +1,27 @@
@echo off
echo Generating the dynamic code
go generate
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Building the router generator
go build ./router_gen
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Running the router generator
router_gen.exe
echo Building the executable
go build -o gosora.exe -tags no_ws
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo Running Gosora
gosora.exe
pause

View File

@ -3,7 +3,7 @@
<div class="colstack_right"> <div class="colstack_right">
<div class="colstack_grid"> <div class="colstack_grid">
{{range .GridItems}} {{range .GridItems}}
<div class="grid_item {{.Class}}" title="{{.Note}}" style="{{if .TextColour}}color: {{.TextColour}};{{end}} <div id="{{.ID}}" class="grid_item {{.Class}}" title="{{.Note}}" style="{{if .TextColour}}color: {{.TextColour}};{{end}}
{{if .Background}}background-color: {{.Background}};{{end}}">{{.Body}}</div> {{if .Background}}background-color: {{.Background}};{{end}}">{{.Body}}</div>
{{end}} {{end}}
</div> </div>

View File

@ -899,9 +899,7 @@ blockquote p
.notice:first-child { display: inline-block; } .notice:first-child { display: inline-block; }
.getTopics { display: none; } .getTopics { display: none; }
.userinfo { .userinfo { width: 70px; }
width: 70px;
}
.userinfo .avatar_item { .userinfo .avatar_item {
background-size: 64px; background-size: 64px;
width: 64px; width: 64px;
@ -913,6 +911,9 @@ blockquote p
} }
.user_content { min-height: 80px !important; } .user_content { min-height: 80px !important; }
.user_content.nobuttons { min-height: 103px !important; } .user_content.nobuttons { min-height: 103px !important; }
.colstack_grid { grid-template-columns: none; grid-gap: 8px; }
.grid_istat { margin-bottom: 0px; }
} }
@media (min-width: 800px) @media (min-width: 800px)
{ {

View File

@ -940,9 +940,7 @@ blockquote p
.forumLastposter img { display: none; } .forumLastposter img { display: none; }
.getTopics { display: none; } .getTopics { display: none; }
.userinfo { .userinfo { width: 70px; }
width: 70px;
}
.userinfo .avatar_item { .userinfo .avatar_item {
background-size: 64px; background-size: 64px;
width: 64px; width: 64px;
@ -954,6 +952,9 @@ blockquote p
} }
.user_content { min-height: 97.5px !important; } .user_content { min-height: 97.5px !important; }
.user_content.nobuttons { min-height: 121px !important; } .user_content.nobuttons { min-height: 121px !important; }
.colstack_grid { grid-template-columns: none; grid-gap: 8px; }
.grid_istat { margin-bottom: 0px; }
} }
@media (min-width: 800px) @media (min-width: 800px)
{ {
@ -1113,11 +1114,7 @@ blockquote p
} }
#main { width: 1690px; } #main { width: 1690px; }
.index_category .index_category { float: left; width: 835px; }
{
float: left;
width: 835px;
}
.index_category:nth-child(even) { margin-left: 10px; } .index_category:nth-child(even) { margin-left: 10px; }
.index_category:nth-child(odd) { overflow: hidden; } .index_category:nth-child(odd) { overflow: hidden; }
.index_category:only-child { width: 100%; } .index_category:only-child { width: 100%; }

View File

@ -620,8 +620,11 @@ button.username
.menu_right { padding-right: 5px; } .menu_right { padding-right: 5px; }
.menu_create_topic { display: none; } .menu_create_topic { display: none; }
.menu_alerts { padding-left: 4px; padding-right: 4px; } .menu_alerts { padding-left: 4px; padding-right: 4px; }
.hide_on_mobile { display: none; } .hide_on_mobile { display: none; }
.prev_button, .next_button { top: auto;bottom: 5px; } .prev_button, .next_button { top: auto;bottom: 5px; }
.colstack_grid { grid-template-columns: none; grid-gap: 8px; }
.grid_istat { margin-bottom: 0px; }
} }
@media (max-width: 470px) { @media (max-width: 470px) {

View File

@ -515,8 +515,11 @@ button.username
.menu_right { padding-right: 5px; } .menu_right { padding-right: 5px; }
.menu_create_topic { display: none;} .menu_create_topic { display: none;}
.menu_alerts { padding-left: 4px; padding-right: 4px; } .menu_alerts { padding-left: 4px; padding-right: 4px; }
.hide_on_mobile { display: none; } .hide_on_mobile { display: none; }
.prev_button, .next_button { top: auto; bottom: 5px; } .prev_button, .next_button { top: auto; bottom: 5px; }
.colstack_grid { grid-template-columns: none; grid-gap: 8px; }
.grid_istat { margin-bottom: 0px; }
} }
@media (max-width: 470px) { @media (max-width: 470px) {

View File

@ -507,8 +507,11 @@ button.username
.menu_right { padding-right: 5px; } .menu_right { padding-right: 5px; }
.menu_create_topic { display: none;} .menu_create_topic { display: none;}
.menu_alerts { padding-left: 4px; padding-right: 4px; } .menu_alerts { padding-left: 4px; padding-right: 4px; }
.hide_on_mobile { display: none !important; } .hide_on_mobile { display: none !important; }
.prev_button, .next_button { top: auto; bottom: 5px; } .prev_button, .next_button { top: auto; bottom: 5px; }
.colstack_grid { grid-template-columns: none; grid-gap: 8px; }
.grid_istat { margin-bottom: 0px; }
} }
@media (max-width: 470px) { @media (max-width: 470px) {

View File

@ -4,3 +4,5 @@ echo "Updating bcrypt"
go get -u golang.org/x/crypto/bcrypt go get -u golang.org/x/crypto/bcrypt
echo "Updating gopsutil" echo "Updating gopsutil"
go get -u github.com/shirou/gopsutil go get -u github.com/shirou/gopsutil
echo "Updating Gorilla WebSockets"
go get -u github.com/gorilla/websocket

View File

@ -27,5 +27,12 @@ if %errorlevel% neq 0 (
exit /b %errorlevel% exit /b %errorlevel%
) )
echo Updating Gorilla Websockets
go get -u github.com/gorilla/websocket
if %errorlevel% neq 0 (
pause
exit /b %errorlevel%
)
echo The dependencies were successfully updated echo The dependencies were successfully updated
pause pause

24
user.go
View File

@ -1,4 +1,5 @@
package main package main
//import "fmt" //import "fmt"
import "sync" import "sync"
import "strings" import "strings"
@ -34,6 +35,7 @@ type User struct
Level int Level int
Score int Score int
Last_IP string Last_IP string
//WS_Conn interface{}
} }
type Email struct type Email struct
@ -53,6 +55,8 @@ type UserStore interface {
Set(item *User) error Set(item *User) error
Add(item *User) error Add(item *User) error
AddUnsafe(item *User) error AddUnsafe(item *User) error
//SetConn(conn interface{}) error
//GetConn() interface{}
Remove(id int) error Remove(id int) error
RemoveUnsafe(id int) error RemoveUnsafe(id int) error
GetLength() int GetLength() int
@ -109,7 +113,7 @@ func (sts *StaticUserStore) CascadeGet(id int) (*User, error) {
user.Tag = groups[user.Group].Tag user.Tag = groups[user.Group].Tag
init_user_perms(user) init_user_perms(user)
if err == nil { if err == nil {
sts.Add(user) sts.Set(user)
} }
return user, err return user, err
} }
@ -137,17 +141,18 @@ func (sts *StaticUserStore) Load(id int) error {
func (sts *StaticUserStore) Set(item *User) error { func (sts *StaticUserStore) Set(item *User) error {
sts.Lock() sts.Lock()
_, ok := sts.items[item.ID] user, ok := sts.items[item.ID]
if ok { if ok {
sts.items[item.ID] = item sts.Unlock()
*user = *item
} else if sts.length >= sts.capacity { } else if sts.length >= sts.capacity {
sts.Unlock() sts.Unlock()
return ErrStoreCapacityOverflow return ErrStoreCapacityOverflow
} else { } else {
sts.items[item.ID] = item sts.items[item.ID] = item
sts.Unlock()
sts.length++ sts.length++
} }
sts.Unlock()
return nil return nil
} }
@ -171,6 +176,17 @@ func (sts *StaticUserStore) AddUnsafe(item *User) error {
return nil return nil
} }
/*func (sts *StaticUserStore) SetConn(id int, conn interface{}) *User, error {
sts.Lock()
user, err := sts.CascadeGet(id)
sts.Unlock()
if err != nil {
return nil, err
}
user.WS_Conn = conn
return user, nil
}*/
func (sts *StaticUserStore) Remove(id int) error { func (sts *StaticUserStore) Remove(id int) error {
sts.Lock() sts.Lock()
delete(sts.items,id) delete(sts.items,id)

319
websockets.go Normal file
View File

@ -0,0 +1,319 @@
// +build !no_ws
package main
import "fmt"
import "sync"
import "time"
import "bytes"
import "strconv"
import "runtime"
import "net/http"
import "github.com/gorilla/websocket"
import "github.com/shirou/gopsutil/cpu"
import "github.com/shirou/gopsutil/mem"
type WS_User struct
{
conn *websocket.Conn
User *User
}
type WS_Hub struct
{
online_users map[int]*WS_User
online_guests map[*WS_User]bool
guests sync.RWMutex
users sync.RWMutex
}
var ws_hub WS_Hub
var ws_upgrader = websocket.Upgrader{ReadBufferSize:1024,WriteBufferSize:1024}
func init() {
enable_websockets = true
admin_stats_watchers = make(map[*WS_User]bool)
ws_hub = WS_Hub{
online_users: make(map[int]*WS_User),
online_guests: make(map[*WS_User]bool),
}
}
func (hub *WS_Hub) GuestCount() int {
defer hub.guests.RUnlock()
hub.guests.RLock()
return len(hub.online_guests)
}
func (hub *WS_Hub) UserCount() int {
defer hub.users.RUnlock()
hub.users.RLock()
return len(hub.online_users)
}
func route_websockets(w http.ResponseWriter, r *http.Request) {
user, ok := SimpleSessionCheck(w,r)
if !ok {
return
}
conn, err := ws_upgrader.Upgrade(w,r,nil)
if err != nil {
return
}
userptr, err := users.CascadeGet(user.ID)
if err != nil && err != ErrStoreCapacityOverflow {
return
}
ws_user := &WS_User{conn,userptr}
if user.ID == 0 {
ws_hub.guests.Lock()
ws_hub.online_guests[ws_user] = true
ws_hub.guests.Unlock()
} else {
ws_hub.users.Lock()
ws_hub.online_users[user.ID] = ws_user
ws_hub.users.Unlock()
}
//conn.SetReadLimit(/* put the max request size from earlier here? */)
//conn.SetReadDeadline(time.Now().Add(60 * time.Second))
var current_page []byte
for {
_, message, err := conn.ReadMessage()
if err != nil {
if user.ID == 0 {
ws_hub.guests.Lock()
delete(ws_hub.online_guests,ws_user)
ws_hub.guests.Unlock()
} else {
ws_hub.users.Lock()
delete(ws_hub.online_users,user.ID)
ws_hub.users.Unlock()
}
break
}
//fmt.Println("Message",message)
//fmt.Println("Message",string(message))
messages := bytes.Split(message,[]byte("\r"))
for _, msg := range messages {
//fmt.Println("Submessage",msg)
//fmt.Println("Submessage",string(msg))
if bytes.HasPrefix(msg,[]byte("page ")) {
msgblocks := bytes.SplitN(msg,[]byte(" "),2)
if len(msgblocks) < 2 {
continue
}
if !bytes.Equal(msgblocks[1],current_page) {
ws_leave_page(ws_user, current_page)
current_page = msgblocks[1]
//fmt.Println("Current Page: ",current_page)
//fmt.Println("Current Page: ",string(current_page))
ws_page_responses(ws_user, current_page)
}
}
/*if bytes.Equal(message,[]byte(`start-view`)) {
} else if bytes.Equal(message,[]byte(`end-view`)) {
}*/
}
}
conn.Close()
}
func ws_page_responses(ws_user *WS_User, page []byte) {
switch(string(page)) {
case "/panel/":
//fmt.Println("/panel/ WS Route")
w, err := ws_user.conn.NextWriter(websocket.TextMessage)
if err != nil {
//fmt.Println(err.Error())
return
}
fmt.Println(ws_hub.online_users)
uonline := ws_hub.UserCount()
gonline := ws_hub.GuestCount()
totonline := uonline + gonline
w.Write([]byte("set #dash-totonline " + strconv.Itoa(totonline) + " online\r"))
w.Write([]byte("set #dash-gonline " + strconv.Itoa(gonline) + " guests online\r"))
w.Write([]byte("set #dash-uonline " + strconv.Itoa(uonline) + " users online\r"))
w.Close()
// Listen for changes and inform the admins...
admin_stats_mutex.Lock()
watchers := len(admin_stats_watchers)
admin_stats_watchers[ws_user] = true
if watchers == 0 {
go admin_stats_ticker()
}
admin_stats_mutex.Unlock()
}
}
func ws_leave_page(ws_user *WS_User, page []byte) {
switch(string(page)) {
case "/panel/":
admin_stats_mutex.Lock()
delete(admin_stats_watchers,ws_user)
admin_stats_mutex.Unlock()
}
}
var admin_stats_watchers map[*WS_User]bool
var admin_stats_mutex sync.RWMutex
func admin_stats_ticker() {
time.Sleep(time.Second)
var last_uonline int = -1
var last_gonline int = -1
var last_totonline int = -1
var last_cpu_perc int = -1
var last_available_ram int64 = -1
var no_stat_updates bool = false
var onlineColour, onlineGuestsColour, onlineUsersColour, cpustr, cpuColour, ramstr, ramColour string
var cpuerr, ramerr error
var memres *mem.VirtualMemoryStat
var cpu_perc []float64
AdminStatLoop:
for {
//fmt.Println("tick tock")
admin_stats_mutex.RLock()
watch_count := len(admin_stats_watchers)
admin_stats_mutex.RUnlock()
if watch_count == 0 {
break AdminStatLoop
}
cpu_perc, cpuerr = cpu.Percent(time.Duration(time.Second),true)
memres, ramerr = mem.VirtualMemory()
uonline := ws_hub.UserCount()
gonline := ws_hub.GuestCount()
totonline := uonline + gonline
// It's far more likely that the CPU Usage will change than the other stats, so we'll optimise them seperately...
no_stat_updates = (uonline == last_uonline && gonline == last_gonline && totonline == last_totonline)
if no_stat_updates && int(cpu_perc[0]) == last_cpu_perc && last_available_ram == int64(memres.Available) {
time.Sleep(time.Second)
continue
}
if !no_stat_updates {
if totonline > 10 {
onlineColour = "stat_green"
} else if totonline > 3 {
onlineColour = "stat_orange"
} else {
onlineColour = "stat_red"
}
if gonline > 10 {
onlineGuestsColour = "stat_green"
} else if gonline > 1 {
onlineGuestsColour = "stat_orange"
} else {
onlineGuestsColour = "stat_red"
}
if uonline > 5 {
onlineUsersColour = "stat_green"
} else if uonline > 1 {
onlineUsersColour = "stat_orange"
} else {
onlineUsersColour = "stat_red"
}
}
if cpuerr != nil {
cpustr = "Unknown"
} else {
calcperc := int(cpu_perc[0]) / runtime.NumCPU()
cpustr = strconv.Itoa(calcperc)
if calcperc < 30 {
cpuColour = "stat_green"
} else if calcperc < 75 {
cpuColour = "stat_orange"
} else {
cpuColour = "stat_red"
}
}
if ramerr != nil {
ramstr = "Unknown"
} else {
total_count, total_unit := convert_byte_unit(float64(memres.Total))
used_count := convert_byte_in_unit(float64(memres.Total - memres.Available),total_unit)
// Round totals with .9s up, it's how most people see it anyway. Floats are notoriously imprecise, so do it off 0.85
var totstr string
if (total_count - float64(int(total_count))) > 0.85 {
used_count += 1.0 - (total_count - float64(int(total_count)))
totstr = strconv.Itoa(int(total_count) + 1)
} else {
totstr = fmt.Sprintf("%.1f",total_count)
}
if used_count > total_count {
used_count = total_count
}
ramstr = fmt.Sprintf("%.1f",used_count) + " / " + totstr + total_unit
ramperc := ((memres.Total - memres.Available) * 100) / memres.Total
if ramperc < 50 {
ramColour = "stat_green"
} else if ramperc < 75 {
ramColour = "stat_orange"
} else {
ramColour = "stat_red"
}
}
admin_stats_mutex.RLock()
watchers := admin_stats_watchers
admin_stats_mutex.RUnlock()
for watcher, _ := range watchers {
w, err := watcher.conn.NextWriter(websocket.TextMessage)
if err != nil {
fmt.Println(err.Error())
admin_stats_mutex.Lock()
delete(admin_stats_watchers,watcher)
admin_stats_mutex.Unlock()
continue
}
if !no_stat_updates {
w.Write([]byte("set #dash-totonline " + strconv.Itoa(totonline) + " online\r"))
w.Write([]byte("set #dash-gonline " + strconv.Itoa(gonline) + " guests online\r"))
w.Write([]byte("set #dash-uonline " + strconv.Itoa(uonline) + " users online\r"))
w.Write([]byte("set-class #dash-totonline grid_stat " + onlineColour + "\r"))
w.Write([]byte("set-class #dash-gonline grid_stat " + onlineGuestsColour + "\r"))
w.Write([]byte("set-class #dash-uonline grid_stat " + onlineUsersColour + "\r"))
}
w.Write([]byte("set #dash-cpu CPU: " + cpustr + "%\r"))
w.Write([]byte("set-class #dash-cpu grid_istat " + cpuColour + "\r"))
w.Write([]byte("set #dash-ram RAM: " + ramstr + "\r"))
w.Write([]byte("set-class #dash-ram grid_istat " + ramColour + "\r"))
w.Close()
}
last_uonline = uonline
last_gonline = gonline
last_totonline = totonline
last_cpu_perc = int(cpu_perc[0])
last_available_ram = int64(memres.Available)
//time.Sleep(time.Second)
}
}