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 00000000..8dd11c87 Binary files /dev/null and b/images/panel-dashboard.png differ 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