14a14b7e80
This commit might be a little broken, another is coming to fix things up! The topics list is now paginated. Refactored the error handling system. Added the Trumboyg WYSIWYG editor for Cosora. Moved Delete() out of the TopicStore and into *Topic You can now bulk delete and bulk lock topics on Cosora. h1s are now formatted properly on Firefox. Added more ARIA Labels. SuperModOnly is now a piece of middleware for the Control Panel routes. Refactored and extended the router generator. Improved the SEO for the paginators. Added bits of Microdata to improve SEO further. Wrote benchmarks for users.Get() and users.BypassGet() More errors are caught now. You can now attach pcss files to posts. Improved the error logging for JavaScript. Topic list avatars now link to the associated profiles. Added last poster avatars to the forum list.
440 lines
11 KiB
Go
440 lines
11 KiB
Go
// +build !no_ws
|
|
|
|
/*
|
|
*
|
|
* Gosora WebSocket Subsystem
|
|
* Copyright Azareal 2017 - 2018
|
|
*
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Azareal/gopsutil/cpu"
|
|
"github.com/Azareal/gopsutil/mem"
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type WSUser struct {
|
|
conn *websocket.Conn
|
|
User *User
|
|
}
|
|
|
|
type WSHub struct {
|
|
onlineUsers map[int]*WSUser
|
|
onlineGuests map[*WSUser]bool
|
|
guests sync.RWMutex
|
|
users sync.RWMutex
|
|
}
|
|
|
|
// TODO: Disable WebSockets on high load? Add a Control Panel interface for disabling it?
|
|
var enableWebsockets = true // Put this in caps for consistency with the other constants?
|
|
|
|
var wsHub WSHub
|
|
var wsUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
|
|
var errWsNouser = errors.New("This user isn't connected via WebSockets")
|
|
|
|
func init() {
|
|
adminStatsWatchers = make(map[*WSUser]bool)
|
|
wsHub = WSHub{
|
|
onlineUsers: make(map[int]*WSUser),
|
|
onlineGuests: make(map[*WSUser]bool),
|
|
}
|
|
}
|
|
|
|
func (hub *WSHub) guestCount() int {
|
|
defer hub.guests.RUnlock()
|
|
hub.guests.RLock()
|
|
return len(hub.onlineGuests)
|
|
}
|
|
|
|
func (hub *WSHub) userCount() int {
|
|
defer hub.users.RUnlock()
|
|
hub.users.RLock()
|
|
return len(hub.onlineUsers)
|
|
}
|
|
|
|
func (hub *WSHub) broadcastMessage(msg string) error {
|
|
hub.users.RLock()
|
|
for _, wsUser := range hub.onlineUsers {
|
|
w, err := wsUser.conn.NextWriter(websocket.TextMessage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _ = w.Write([]byte(msg))
|
|
}
|
|
hub.users.RUnlock()
|
|
return nil
|
|
}
|
|
|
|
func (hub *WSHub) pushMessage(targetUser int, msg string) error {
|
|
hub.users.RLock()
|
|
wsUser, ok := hub.onlineUsers[targetUser]
|
|
hub.users.RUnlock()
|
|
if !ok {
|
|
return errWsNouser
|
|
}
|
|
|
|
w, err := wsUser.conn.NextWriter(websocket.TextMessage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.Write([]byte(msg))
|
|
w.Close()
|
|
return nil
|
|
}
|
|
|
|
func (hub *WSHub) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
|
|
//log.Print("In pushAlert")
|
|
hub.users.RLock()
|
|
wsUser, ok := hub.onlineUsers[targetUser]
|
|
hub.users.RUnlock()
|
|
if !ok {
|
|
return errWsNouser
|
|
}
|
|
|
|
//log.Print("Building alert")
|
|
alert, err := buildAlert(asid, event, elementType, actorID, targetUserID, elementID, *wsUser.User)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//log.Print("Getting WS Writer")
|
|
w, err := wsUser.conn.NextWriter(websocket.TextMessage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.Write([]byte(alert))
|
|
_ = w.Close()
|
|
return nil
|
|
}
|
|
|
|
func (hub *WSHub) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
|
|
//log.Print("In pushAlerts")
|
|
var wsUsers []*WSUser
|
|
hub.users.RLock()
|
|
// We don't want to keep a lock on this for too long, so we'll accept some nil pointers
|
|
for _, uid := range users {
|
|
wsUsers = append(wsUsers, hub.onlineUsers[uid])
|
|
}
|
|
hub.users.RUnlock()
|
|
if len(wsUsers) == 0 {
|
|
return errWsNouser
|
|
}
|
|
|
|
var errs []error
|
|
for _, wsUser := range wsUsers {
|
|
if wsUser == nil {
|
|
continue
|
|
}
|
|
|
|
//log.Print("Building alert")
|
|
alert, err := buildAlert(asid, event, elementType, actorID, targetUserID, elementID, *wsUser.User)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
//log.Print("Getting WS Writer")
|
|
w, err := wsUser.conn.NextWriter(websocket.TextMessage)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
w.Write([]byte(alert))
|
|
w.Close()
|
|
}
|
|
|
|
// Return the first error
|
|
if len(errs) != 0 {
|
|
for _, err := range errs {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TODO: How should we handle errors for this?
|
|
func routeWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteError {
|
|
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
userptr, err := users.Get(user.ID)
|
|
if err != nil && err != ErrStoreCapacityOverflow {
|
|
return nil
|
|
}
|
|
|
|
wsUser := &WSUser{conn, userptr}
|
|
if user.ID == 0 {
|
|
wsHub.guests.Lock()
|
|
wsHub.onlineGuests[wsUser] = true
|
|
wsHub.guests.Unlock()
|
|
} else {
|
|
wsHub.users.Lock()
|
|
wsHub.onlineUsers[user.ID] = wsUser
|
|
wsHub.users.Unlock()
|
|
}
|
|
|
|
//conn.SetReadLimit(/* put the max request size from earlier here? */)
|
|
//conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
|
var currentPage []byte
|
|
for {
|
|
_, message, err := conn.ReadMessage()
|
|
if err != nil {
|
|
if user.ID == 0 {
|
|
wsHub.guests.Lock()
|
|
delete(wsHub.onlineGuests, wsUser)
|
|
wsHub.guests.Unlock()
|
|
} else {
|
|
wsHub.users.Lock()
|
|
delete(wsHub.onlineUsers, user.ID)
|
|
wsHub.users.Unlock()
|
|
}
|
|
break
|
|
}
|
|
|
|
//log.Print("Message", message)
|
|
//log.Print("string(Message)", string(message))
|
|
messages := bytes.Split(message, []byte("\r"))
|
|
for _, msg := range messages {
|
|
//log.Print("Submessage", msg)
|
|
//log.Print("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], currentPage) {
|
|
wsLeavePage(wsUser, currentPage)
|
|
currentPage = msgblocks[1]
|
|
//log.Print("Current Page:", currentPage)
|
|
//log.Print("Current Page:", string(currentPage))
|
|
wsPageResponses(wsUser, currentPage)
|
|
}
|
|
}
|
|
/*if bytes.Equal(message,[]byte(`start-view`)) {
|
|
} else if bytes.Equal(message,[]byte(`end-view`)) {
|
|
}*/
|
|
}
|
|
}
|
|
conn.Close()
|
|
return nil
|
|
}
|
|
|
|
func wsPageResponses(wsUser *WSUser, page []byte) {
|
|
switch string(page) {
|
|
case "/panel/":
|
|
//log.Print("/panel/ WS Route")
|
|
/*w, err := wsUser.conn.NextWriter(websocket.TextMessage)
|
|
if err != nil {
|
|
//log.Print(err.Error())
|
|
return
|
|
}
|
|
|
|
log.Print(wsHub.online_users)
|
|
uonline := wsHub.userCount()
|
|
gonline := wsHub.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...
|
|
adminStatsMutex.Lock()
|
|
watchers := len(adminStatsWatchers)
|
|
adminStatsWatchers[wsUser] = true
|
|
if watchers == 0 {
|
|
go adminStatsTicker()
|
|
}
|
|
adminStatsMutex.Unlock()
|
|
}
|
|
}
|
|
|
|
func wsLeavePage(wsUser *WSUser, page []byte) {
|
|
switch string(page) {
|
|
case "/panel/":
|
|
adminStatsMutex.Lock()
|
|
delete(adminStatsWatchers, wsUser)
|
|
adminStatsMutex.Unlock()
|
|
}
|
|
}
|
|
|
|
var adminStatsWatchers map[*WSUser]bool
|
|
var adminStatsMutex sync.RWMutex
|
|
|
|
func adminStatsTicker() {
|
|
time.Sleep(time.Second)
|
|
|
|
var lastUonline = -1
|
|
var lastGonline = -1
|
|
var lastTotonline = -1
|
|
var lastCPUPerc = -1
|
|
var lastAvailableRAM int64 = -1
|
|
var noStatUpdates bool
|
|
var noRAMUpdates bool
|
|
|
|
var onlineColour, onlineGuestsColour, onlineUsersColour, cpustr, cpuColour, ramstr, ramColour string
|
|
var cpuerr, ramerr error
|
|
var memres *mem.VirtualMemoryStat
|
|
var cpuPerc []float64
|
|
|
|
var totunit, uunit, gunit string
|
|
|
|
AdminStatLoop:
|
|
for {
|
|
adminStatsMutex.RLock()
|
|
watchCount := len(adminStatsWatchers)
|
|
adminStatsMutex.RUnlock()
|
|
if watchCount == 0 {
|
|
break AdminStatLoop
|
|
}
|
|
|
|
cpuPerc, cpuerr = cpu.Percent(time.Second, true)
|
|
memres, ramerr = mem.VirtualMemory()
|
|
uonline := wsHub.userCount()
|
|
gonline := wsHub.guestCount()
|
|
totonline := uonline + gonline
|
|
|
|
// It's far more likely that the CPU Usage will change than the other stats, so we'll optimise them separately...
|
|
noStatUpdates = (uonline == lastUonline && gonline == lastGonline && totonline == lastTotonline)
|
|
noRAMUpdates = (lastAvailableRAM == int64(memres.Available))
|
|
if int(cpuPerc[0]) == lastCPUPerc && noStatUpdates && noRAMUpdates {
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
|
|
if !noStatUpdates {
|
|
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"
|
|
}
|
|
|
|
totonline, totunit = convertFriendlyUnit(totonline)
|
|
uonline, uunit = convertFriendlyUnit(uonline)
|
|
gonline, gunit = convertFriendlyUnit(gonline)
|
|
}
|
|
|
|
if cpuerr != nil {
|
|
cpustr = "Unknown"
|
|
} else {
|
|
calcperc := int(cpuPerc[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 !noRAMUpdates {
|
|
if ramerr != nil {
|
|
ramstr = "Unknown"
|
|
} else {
|
|
totalCount, totalUnit := convertByteUnit(float64(memres.Total))
|
|
usedCount := convertByteInUnit(float64(memres.Total-memres.Available), totalUnit)
|
|
|
|
// 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 (totalCount - float64(int(totalCount))) > 0.85 {
|
|
usedCount += 1.0 - (totalCount - float64(int(totalCount)))
|
|
totstr = strconv.Itoa(int(totalCount) + 1)
|
|
} else {
|
|
totstr = fmt.Sprintf("%.1f", totalCount)
|
|
}
|
|
|
|
if usedCount > totalCount {
|
|
usedCount = totalCount
|
|
}
|
|
ramstr = fmt.Sprintf("%.1f", usedCount) + " / " + totstr + totalUnit
|
|
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
|
|
adminStatsMutex.RLock()
|
|
watchers := adminStatsWatchers
|
|
adminStatsMutex.RUnlock()
|
|
|
|
for watcher := range watchers {
|
|
w, err := watcher.conn.NextWriter(websocket.TextMessage)
|
|
if err != nil {
|
|
//log.Print(err.Error())
|
|
adminStatsMutex.Lock()
|
|
delete(adminStatsWatchers, watcher)
|
|
adminStatsMutex.Unlock()
|
|
continue
|
|
}
|
|
|
|
// nolint
|
|
if !noStatUpdates {
|
|
w.Write([]byte("set #dash-totonline " + strconv.Itoa(totonline) + totunit + " online\r"))
|
|
w.Write([]byte("set #dash-gonline " + strconv.Itoa(gonline) + gunit + " guests online\r"))
|
|
w.Write([]byte("set #dash-uonline " + strconv.Itoa(uonline) + uunit + " users online\r"))
|
|
|
|
w.Write([]byte("set-class #dash-totonline grid_item grid_stat " + onlineColour + "\r"))
|
|
w.Write([]byte("set-class #dash-gonline grid_item grid_stat " + onlineGuestsColour + "\r"))
|
|
w.Write([]byte("set-class #dash-uonline grid_item grid_stat " + onlineUsersColour + "\r"))
|
|
}
|
|
|
|
w.Write([]byte("set #dash-cpu CPU: " + cpustr + "%\r"))
|
|
w.Write([]byte("set-class #dash-cpu grid_item grid_istat " + cpuColour + "\r"))
|
|
|
|
if !noRAMUpdates {
|
|
w.Write([]byte("set #dash-ram RAM: " + ramstr + "\r"))
|
|
w.Write([]byte("set-class #dash-ram grid_item grid_istat " + ramColour + "\r"))
|
|
}
|
|
|
|
w.Close()
|
|
}
|
|
|
|
lastUonline = uonline
|
|
lastGonline = gonline
|
|
lastTotonline = totonline
|
|
lastCPUPerc = int(cpuPerc[0])
|
|
lastAvailableRAM = int64(memres.Available)
|
|
|
|
//time.Sleep(time.Second)
|
|
}
|
|
}
|