gosora/websockets.go
Azareal 14a14b7e80 More work on Cosora, we have a screenshot of it up now, although it's super experimental at the moment.
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.
2017-10-30 09:57:08 +00:00

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)
}
}