From 3b5f48b5a2baa4b37677b1e3bc4142347a10593c Mon Sep 17 00:00:00 2001 From: Azareal Date: Thu, 11 May 2017 14:04:43 +0100 Subject: [PATCH] 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. --- README.md | 8 +- build-gosora-linux-nowebsockets | 4 + build-nowebsockets.bat | 30 +++ images/panel-dashboard.png | Bin 0 -> 50344 bytes install-gosora-linux | 2 + install.bat | 7 +- main.go | 2 + mod_routes.go | 37 ++- no_websockets.go | 22 ++ pages.go | 1 + panel_routes.go | 87 ++++--- public/global.js | 60 +++++ routes.go | 31 +-- run-gosora-linux-nowebsockets | 6 + run-nowebsockets.bat | 27 +++ templates/panel-dashboard.html | 2 +- themes/cosmo-conflux/public/main.css | 7 +- themes/cosmo/public/main.css | 13 +- themes/tempra-conflux/public/main.css | 3 + themes/tempra-cursive/public/main.css | 3 + themes/tempra-simple/public/main.css | 3 + update-deps-linux | 2 + update-deps.bat | 7 + user.go | 24 +- websockets.go | 319 ++++++++++++++++++++++++++ 25 files changed, 625 insertions(+), 82 deletions(-) create mode 100644 build-gosora-linux-nowebsockets create mode 100644 build-nowebsockets.bat create mode 100644 images/panel-dashboard.png create mode 100644 no_websockets.go create mode 100644 run-gosora-linux-nowebsockets create mode 100644 run-nowebsockets.bat create mode 100644 websockets.go diff --git a/README.md b/README.md index 3f497a68..6bade74c 100644 --- a/README.md +++ b/README.md @@ -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! +Other modern features like alerts, advanced dashboard, etc. + # 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. @@ -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/gorilla/websocket Needed for Gosora's Optional WebSockets Module. + # 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). diff --git a/build-gosora-linux-nowebsockets b/build-gosora-linux-nowebsockets new file mode 100644 index 00000000..32cb9ac1 --- /dev/null +++ b/build-gosora-linux-nowebsockets @@ -0,0 +1,4 @@ +echo "Building Gosora" +go build -o Gosora -tags no_ws +echo "Building the installer" +go build ./install diff --git a/build-nowebsockets.bat b/build-nowebsockets.bat new file mode 100644 index 00000000..c8299f2b --- /dev/null +++ b/build-nowebsockets.bat @@ -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 \ No newline at end of file diff --git a/images/panel-dashboard.png b/images/panel-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd11c874f88a93b59a4ff731b665a6f7b0cc878 GIT binary patch literal 50344 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 V4sqJ>fd-?i+Sm23<*Qpi`yXr6-!cFI literal 0 HcmV?d00001 diff --git a/install-gosora-linux b/install-gosora-linux index 9da702d0..82438ade 100644 --- a/install-gosora-linux +++ b/install-gosora-linux @@ -4,6 +4,8 @@ echo "Installing bcrypt" go get -u golang.org/x/crypto/bcrypt echo "Installing gopsutil" go get -u github.com/shirou/gopsutil +echo "Installing Gorilla WebSockets" +go get -u github.com/gorilla/websocket echo "Preparing the installer" go generate diff --git a/install.bat b/install.bat index 8b6bcbc2..83c69b14 100644 --- a/install.bat +++ b/install.bat @@ -20,6 +20,11 @@ if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) +go get -u github.com/gorilla/websocket +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) echo Preparing the installer go generate @@ -27,7 +32,7 @@ if %errorlevel% neq 0 ( pause exit /b %errorlevel% ) -go build +go build -o gosora.exe if %errorlevel% neq 0 ( pause exit /b %errorlevel% diff --git a/main.go b/main.go index bca4c986..7e4eb621 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ const gigabyte int = megabyte * 1024 const terabyte int = gigabyte * 1024 const saltLength int = 32 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 no_css_tmpl = template.CSS("") @@ -273,6 +274,7 @@ func main(){ ///router.HandleFunc("/api/", route_api) //router.HandleFunc("/exit/", route_exit) ///router.HandleFunc("/", default_route) + router.HandleFunc("/ws/", route_websockets) defer db.Close() //if profiling { diff --git a/mod_routes.go b/mod_routes.go index 27c31799..f168baeb 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -1,13 +1,15 @@ package main -import "log" -import "fmt" -import "strconv" -import "net" -import "net/http" -import "html" -import "database/sql" -import _ "github.com/go-sql-driver/mysql" +import ( + "log" +// "fmt" + "strconv" + "net" + "net/http" + "html" + "database/sql" + _ "github.com/go-sql-driver/mysql" +) func route_edit_topic(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() @@ -20,8 +22,7 @@ func route_edit_topic(w http.ResponseWriter, r *http.Request) { is_js = "0" } - var tid int - var fid int + var tid, fid int tid, err = strconv.Atoi(r.URL.Path[len("/topic/edit/submit/"):]) if err != nil { 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" { http.Redirect(w,r,"/topic/" + strconv.Itoa(tid),http.StatusSeeOther) } 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 createdBy int - var fid int + var createdBy, fid int err = db.QueryRow("select content, createdBy, parentID from topics where tid = ?", tid).Scan(&content, &createdBy, &fid) if err == sql.ErrNoRows { 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" { http.Redirect(w,r, "/topic/" + strconv.Itoa(tid) + "#reply-" + strconv.Itoa(rid), http.StatusSeeOther) } 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 } - var tid int + var tid, createdBy int var content string - var createdBy int err = db.QueryRow("select tid, content, createdBy from replies where rid = ?", rid).Scan(&tid, &content, &createdBy) if err == sql.ErrNoRows { 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" { //http.Redirect(w,r, "/topic/" + strconv.Itoa(tid), http.StatusSeeOther) } else { - fmt.Fprintf(w,`{"success":"1"}`) + w.Write(success_json_bytes) } wcount := word_count(content) @@ -482,7 +481,7 @@ func route_profile_reply_edit_submit(w http.ResponseWriter, r *http.Request) { if is_js == "0" { http.Redirect(w,r,"/user/" + strconv.Itoa(uid) + "#reply-" + strconv.Itoa(rid), http.StatusSeeOther) } 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" { //http.Redirect(w,r, "/user/" + strconv.Itoa(uid), http.StatusSeeOther) } else { - fmt.Fprintf(w,`{"success":"1"}`) + w.Write(success_json_bytes) } } diff --git a/no_websockets.go b/no_websockets.go new file mode 100644 index 00000000..ea76e359 --- /dev/null +++ b/no_websockets.go @@ -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) { +} diff --git a/pages.go b/pages.go index a70f6db2..3576c8dc 100644 --- a/pages.go +++ b/pages.go @@ -79,6 +79,7 @@ type CreateTopicPage struct type GridElement struct { + ID string Body string Order int // For future use Class string diff --git a/panel_routes.go b/panel_routes.go index 557390e9..61a4cd49 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -34,19 +34,9 @@ func route_panel(w http.ResponseWriter, r *http.Request){ if err != nil { cpustr = "Unknown" } 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() cpustr = strconv.Itoa(calcperc) - if calcperc < 25 { + if calcperc < 30 { cpuColour = "stat_green" } else if calcperc < 75 { cpuColour = "stat_orange" @@ -99,9 +89,9 @@ func route_panel(w http.ResponseWriter, r *http.Request){ var postInterval string = "day" var postColour string - if postCount > 10 { + if postCount > 25 { postColour = "stat_green" - } else if postCount > 0 { + } else if postCount > 5 { postColour = "stat_orange" } else { postColour = "stat_red" @@ -116,7 +106,7 @@ func route_panel(w http.ResponseWriter, r *http.Request){ var topicInterval string = "day" var topicColour string - if topicCount > 10 { + if topicCount > 8 { topicColour = "stat_green" } else if topicCount > 0 { topicColour = "stat_orange" @@ -141,23 +131,60 @@ func route_panel(w http.ResponseWriter, r *http.Request){ var newUserInterval string = "week" var gridElements []GridElement = []GridElement{ - GridElement{"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{"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"*/}, + GridElement{"dash-version","v" + version.String(),0,"grid_istat stat_green","","","Gosora is up-to-date :)"}, + GridElement{"dash-cpu","CPU: " + cpustr + "%",1,"grid_istat " + cpuColour,"","","The global CPU usage of this server"}, + GridElement{"dash-ram","RAM: " + ramstr,2,"grid_istat " + ramColour,"","","The global RAM usage of this server"}, } + 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} 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" { http.Redirect(w,r,"/panel/forums/",http.StatusSeeOther) } else { - fmt.Fprintf(w,`{"success":"1"}`) + w.Write(success_json_bytes) } } diff --git a/public/global.js b/public/global.js index 42b76138..2e60e0a2 100644 --- a/public/global.js +++ b/public/global.js @@ -73,6 +73,66 @@ function load_alerts(menu_alerts) } $(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){ //console.log("Clicked on edit"); event.preventDefault(); diff --git a/routes.go b/routes.go index 17166957..8ab4b1de 100644 --- a/routes.go +++ b/routes.go @@ -1,20 +1,22 @@ /* Copyright Azareal 2016 - 2017 */ package main -import "log" -//import "fmt" -import "strconv" -import "bytes" -import "regexp" -import "strings" -import "time" -import "io" -import "os" -import "net" -import "net/http" -import "html" -import "html/template" -import "database/sql" +import ( + "log" +// "fmt" + "strconv" + "bytes" + "regexp" + "strings" + "time" + "io" + "os" + "net" + "net/http" + "html" + "html/template" + "database/sql" +) import _ "github.com/go-sql-driver/mysql" 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 var tList []interface{} var nList []string +var success_json_bytes []byte = []byte(`{"success":"1"}`) // GET functions func route_static(w http.ResponseWriter, r *http.Request){ diff --git a/run-gosora-linux-nowebsockets b/run-gosora-linux-nowebsockets new file mode 100644 index 00000000..0360c35d --- /dev/null +++ b/run-gosora-linux-nowebsockets @@ -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 diff --git a/run-nowebsockets.bat b/run-nowebsockets.bat new file mode 100644 index 00000000..db6c13ff --- /dev/null +++ b/run-nowebsockets.bat @@ -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 \ No newline at end of file diff --git a/templates/panel-dashboard.html b/templates/panel-dashboard.html index 58ddfa20..557598d8 100644 --- a/templates/panel-dashboard.html +++ b/templates/panel-dashboard.html @@ -3,7 +3,7 @@
{{range .GridItems}} -
{{.Body}}
{{end}}
diff --git a/themes/cosmo-conflux/public/main.css b/themes/cosmo-conflux/public/main.css index d2f91c77..f2f621d8 100644 --- a/themes/cosmo-conflux/public/main.css +++ b/themes/cosmo-conflux/public/main.css @@ -899,9 +899,7 @@ blockquote p .notice:first-child { display: inline-block; } .getTopics { display: none; } - .userinfo { - width: 70px; - } + .userinfo { width: 70px; } .userinfo .avatar_item { background-size: 64px; width: 64px; @@ -913,6 +911,9 @@ blockquote p } .user_content { min-height: 80px !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) { diff --git a/themes/cosmo/public/main.css b/themes/cosmo/public/main.css index 67b8f48b..c7971cc3 100644 --- a/themes/cosmo/public/main.css +++ b/themes/cosmo/public/main.css @@ -940,9 +940,7 @@ blockquote p .forumLastposter img { display: none; } .getTopics { display: none; } - .userinfo { - width: 70px; - } + .userinfo { width: 70px; } .userinfo .avatar_item { background-size: 64px; width: 64px; @@ -954,6 +952,9 @@ blockquote p } .user_content { min-height: 97.5px !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) { @@ -1113,11 +1114,7 @@ blockquote p } #main { width: 1690px; } - .index_category - { - float: left; - width: 835px; - } + .index_category { float: left; width: 835px; } .index_category:nth-child(even) { margin-left: 10px; } .index_category:nth-child(odd) { overflow: hidden; } .index_category:only-child { width: 100%; } diff --git a/themes/tempra-conflux/public/main.css b/themes/tempra-conflux/public/main.css index 7384eef8..ab0f1916 100644 --- a/themes/tempra-conflux/public/main.css +++ b/themes/tempra-conflux/public/main.css @@ -620,8 +620,11 @@ button.username .menu_right { padding-right: 5px; } .menu_create_topic { display: none; } .menu_alerts { padding-left: 4px; padding-right: 4px; } + .hide_on_mobile { display: none; } .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) { diff --git a/themes/tempra-cursive/public/main.css b/themes/tempra-cursive/public/main.css index 1fdcbdf9..52131fd6 100644 --- a/themes/tempra-cursive/public/main.css +++ b/themes/tempra-cursive/public/main.css @@ -515,8 +515,11 @@ button.username .menu_right { padding-right: 5px; } .menu_create_topic { display: none;} .menu_alerts { padding-left: 4px; padding-right: 4px; } + .hide_on_mobile { display: none; } .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) { diff --git a/themes/tempra-simple/public/main.css b/themes/tempra-simple/public/main.css index be93abfe..fcf1283c 100644 --- a/themes/tempra-simple/public/main.css +++ b/themes/tempra-simple/public/main.css @@ -507,8 +507,11 @@ button.username .menu_right { padding-right: 5px; } .menu_create_topic { display: none;} .menu_alerts { padding-left: 4px; padding-right: 4px; } + .hide_on_mobile { display: none !important; } .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) { diff --git a/update-deps-linux b/update-deps-linux index a24e4160..e2006620 100644 --- a/update-deps-linux +++ b/update-deps-linux @@ -4,3 +4,5 @@ echo "Updating bcrypt" go get -u golang.org/x/crypto/bcrypt echo "Updating gopsutil" go get -u github.com/shirou/gopsutil +echo "Updating Gorilla WebSockets" +go get -u github.com/gorilla/websocket diff --git a/update-deps.bat b/update-deps.bat index 38ef0adb..22997898 100644 --- a/update-deps.bat +++ b/update-deps.bat @@ -27,5 +27,12 @@ if %errorlevel% neq 0 ( 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 pause \ No newline at end of file diff --git a/user.go b/user.go index 54b6e717..626ad031 100644 --- a/user.go +++ b/user.go @@ -1,4 +1,5 @@ package main + //import "fmt" import "sync" import "strings" @@ -34,6 +35,7 @@ type User struct Level int Score int Last_IP string + //WS_Conn interface{} } type Email struct @@ -53,6 +55,8 @@ type UserStore interface { Set(item *User) error Add(item *User) error AddUnsafe(item *User) error + //SetConn(conn interface{}) error + //GetConn() interface{} Remove(id int) error RemoveUnsafe(id int) error GetLength() int @@ -109,7 +113,7 @@ func (sts *StaticUserStore) CascadeGet(id int) (*User, error) { user.Tag = groups[user.Group].Tag init_user_perms(user) if err == nil { - sts.Add(user) + sts.Set(user) } return user, err } @@ -137,17 +141,18 @@ func (sts *StaticUserStore) Load(id int) error { func (sts *StaticUserStore) Set(item *User) error { sts.Lock() - _, ok := sts.items[item.ID] + user, ok := sts.items[item.ID] if ok { - sts.items[item.ID] = item + sts.Unlock() + *user = *item } else if sts.length >= sts.capacity { sts.Unlock() return ErrStoreCapacityOverflow } else { sts.items[item.ID] = item + sts.Unlock() sts.length++ } - sts.Unlock() return nil } @@ -171,6 +176,17 @@ func (sts *StaticUserStore) AddUnsafe(item *User) error { 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 { sts.Lock() delete(sts.items,id) diff --git a/websockets.go b/websockets.go new file mode 100644 index 00000000..1027b7aa --- /dev/null +++ b/websockets.go @@ -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) + } +} \ No newline at end of file