-
Notifications
You must be signed in to change notification settings - Fork 3
/
getstats.go
268 lines (230 loc) · 9.32 KB
/
getstats.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
// Getting stats from OpenSimulator — viewer version, online users, regions, and map
package main
import (
"database/sql"
// "encoding/json"
"fmt"
"github.com/gin-contrib/location"
// "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
// jsoniter "github.com/json-iterator/go"
"html/template"
"log"
"net"
"net/http"
"strings"
"time"
)
// We need to pass JSON to templates, because it won't work otherwise.
// var json = jsoniter.ConfigCompatibleWithStandardLibrary
// SimpleRegion is a very simple struct just to get a region's name. location and size.
// In the future, it might have extra fields for linking to the grid map.
// Added sizeX/Y for 'official' statistics (gwyneth 20200816).
type SimpleRegion struct {
RegionName string `form:"regionName" json:"regionName"` // we'll JSONify this later
LocX int `form:"locX" json:"locX,string"`
LocY int `form:"locY" json:"locY,string"`
SizeX uint `form:"sizeX" json:"sizeX,string"` // Note that the current gridmap does assume sizeX/Y == 256 (gwyneth 20200816)
SizeY uint `form:"sizeY" json:"sizeY,string"`
}
// Apparently this is what we get with /welcome — some information from the viewer! (gwyneth 20200612)
type Viewer struct {
ViewerName string `form:"channel" json:"channel"`
Grid string `form:"grid" json:"grid"`
Language string `form:"lang" json:"lang"`
LoginContentVersion string `form:"login_content_version" json:"login_content_version"`
OS string `form:"os" json:"os"`
SourceID string `form:"sourceid" json:"sourceid"`
Version string `form:"version" json:"version"`
}
type SimpleUser struct {
AvatarName string `form:"Avatar Name" json:"Avatar Name"`
}
// See comments on auth.go for token... the same applies here
type ResponseFormatType struct {
Payload string `uri:"ResponseFormatType"`
}
// GetStats will be used on the Welcome template (and possibly elsewhere) to display some in-world stats.
func GetStats(c *gin.Context) {
// Declare some variables used to JSONify everything. (gwyneth 20200612)
var (
oneViewer Viewer
viewerInfo []Viewer
simpleRegion SimpleRegion
regionsTable []SimpleRegion
userTable []SimpleUser
err error
)
// Deal with what comes from the SL viewer, e.g. something like channel=Firestorm-Releasex64&grid=btgrid&lang=en&login_content_version=2&os=Mac%20OS%20X%2010.15.6&sourceid=&version=6.3.9%20%2858205%29"
if c.Bind(&oneViewer) == nil { // nil means no errors
if oneViewer.ViewerName != "" { // apparently, it binds even if there is nothing to bind to; so we check this first before appending to the table; it means the table will be nil, and commented out on the template (gwyneth 20200616)
viewerInfo = append(viewerInfo, oneViewer)
}
} else {
checkErr(err)
}
if *config["ginMode"] == "debug" {
log.Println("[DEBUG] Data from viewer:", viewerInfo)
}
// open database connection
if *config["dsn"] == "" {
log.Fatal("Please configure the DSN for accessing your OpenSimulator database; this application won't work without that")
}
db, err := sql.Open("mysql", *config["dsn"]) // presumes mysql for now
checkErrFatal(err)
defer db.Close()
rows, err := db.Query("SELECT regionName, locX, locY FROM regions WHERE owner_uuid <> '00000000-0000-0000-0000-000000000000' ORDER BY regionName ASC")
checkErr(err)
defer rows.Close()
var regionName template.HTML
for rows.Next() {
err = rows.Scan(
®ionName,
&simpleRegion.LocX,
&simpleRegion.LocY,
)
simpleRegion.LocX /= 256
simpleRegion.LocY /= 256
simpleRegion.RegionName = fmt.Sprintf(`<a class="class-link text-secondary" href="secondlife://%s/127/127/24/" onclick="goInWorld('secondlife://%s/127/127/24/');">%s</a>`, regionName, regionName, regionName)
regionsTable = append(regionsTable, simpleRegion)
}
checkErr(err)
// log.Println("[DEBUG] Data from regionsTable:", regionsTable)
rows, err = db.Query("SELECT PrincipalID, FirstName, LastName FROM UserAccounts WHERE PrincipalID IN (SELECT UserID FROM Presence)")
checkErr(err)
var principalID, firstName, lastName string // temporary to get replies from the database
for rows.Next() {
err = rows.Scan(&principalID, &firstName, &lastName)
userTable = append(userTable, SimpleUser{AvatarName: firstName + " " + lastName})
}
checkErr(err)
if *config["ginMode"] == "debug" {
log.Println("[DEBUG] Data from userTable:", userTable)
}
c.HTML(http.StatusOK, "welcome.tpl", environment(c,
gin.H{
"needsTables": true,
"needsMap": true,
"slideshow": slideshow,
"viewerInfo": viewerInfo,
"regionsTable": regionsTable,
"usersOnline": userTable,
"Debug": false, // we will probably need two versions of 'debug mode'... (gwyneth 20200622)
"titleCommon": *config["titleCommon"] + "Welcome!",
}))
}
// Implementation of OpenSimulator statistics according to https://github.com/BillBlight/OS_Simple_Stats/blob/master/stats.php (gwyneth 20200816)
var cachedArr gin.H // this will store the last retrieval from the database, with a timestamp, to avoid
// OSSimpleStats is the router handler which will query the database and return in one of many formats.
func OSSimpleStats(c *gin.Context) {
var (
arr gin.H
currentTime = time.Now()
cachedTime = currentTime.Add(-15 * time.Minute) // somewhen in the past
format ResponseFormatType
)
// first handle formats by type; e.g. .../stats?format=json replies with JSON
if err := c.ShouldBindUri(&format); err != nil {
checkErr(err)
}
if *config["ginMode"] == "debug" {
log.Printf("[DEBUG] OSSimpleStats(): Format for stats is: %v; current time is %v\n", format, currentTime)
}
url := location.Get(c) // get info about hostname
if cachedArr != nil && cachedArr["timestamp"] != nil { // first make sure that this is valid
cachedTime = cachedArr["timestamp"].(time.Time)
}
if currentTime.Sub(cachedTime).Minutes() > 5 {
var gStatus = "ONLINE"
var server = *config["ROBUSTserver"]
i := strings.Index(server, "//")
if i != -1 {
server = server[i+2:]
}
if *config["ginMode"] == "debug" {
log.Println("[INFO] OSSimpleStats(): Cache expired; retrieving new set of data")
log.Printf("[DEBUG] OSSimpleStats(): ROBUST server is at %q\n", server)
}
conn, err := net.Dial("tcp", server)
// TODO(gwyneth): I'll probably put a timeout here somewhere (gwyneth 20200817).
if err != nil {
log.Printf("[ERROR] OSSimpleStats(): ROBUST server %q unavailable; error was: %q", server, err)
gStatus = "OFFLINE"
}
conn.Close()
// TODO(gwyneth): for the rest of the things, we will limit this to 1 query every X minutes, or else everything blows up; we might return a cached result (gwyneth 20200817).
// open database connection
if *config["dsn"] == "" {
log.Fatal("Please configure the DSN for accessing your OpenSimulator database; this application won't work without that")
}
db, err := sql.Open("mysql", *config["dsn"]+"?parseTime=true")
checkErrFatal(err)
defer db.Close()
preshguser := 0
checkErr(db.QueryRow("SELECT COUNT(*) FROM GridUser WHERE UserID LIKE '%htt%' AND BINARY Login > UNIX_TIMESTAMP(NOW()) - 2592000").Scan(&preshguser)) // 2592000 = 1 month
nowonlinescounter := 0
checkErr(db.QueryRow("SELECT COUNT(*) FROM Presence").Scan(&nowonlinescounter))
pastmonth := 0
checkErr(db.QueryRow("SELECT DISTINCT COUNT(*) FROM GridUser WHERE BINARY Logout > UNIX_TIMESTAMP(NOW()) - 2592000").Scan(&pastmonth))
totalaccounts := 0
checkErr(db.QueryRow("SELECT COUNT(*) FROM UserAccounts").Scan(&totalaccounts))
totalregions := 0
totalvarregions := 0
totalsingleregions := 0
var totalsize uint = 0
var simpleRegion SimpleRegion
rows, err := db.Query("SELECT sizeX, sizeY FROM regions WHERE owner_uuid <> '00000000-0000-0000-0000-000000000000'")
checkErr(err)
defer rows.Close()
for rows.Next() {
err = rows.Scan(
&simpleRegion.SizeX,
&simpleRegion.SizeY,
)
totalregions++
if simpleRegion.SizeX == 256 {
totalsingleregions++
} else {
totalvarregions++
}
totalsize += simpleRegion.SizeX * simpleRegion.SizeY
}
checkErr(err)
totalsize /= 1000 // we were accumulating rounding errors (gwyneth 20200810)
// Create object to send to templating system
arr = gin.H{
"GridStatus": gStatus,
"Online_Now": nowonlinescounter,
"HG_Visitors_Last_30_Days": preshguser,
"Local_Users_Last_30_Days": pastmonth,
"Total_Active_Last_30_Days": pastmonth + preshguser,
"Registered_Users": totalaccounts,
"Regions": totalregions,
"Var_Regions": totalvarregions,
"Single_Regions": totalsingleregions,
"Total_LandSize": totalsize,
"Login_URL": *config["assetServer"],
"Website": url.Scheme + "://" + url.Host,
"Login_Screen": url.Scheme + "://" + url.Host + "/welcome",
"timestamp": currentTime,
}
// save it
cachedArr = arr
} else {
if *config["ginMode"] == "debug" {
log.Println("[INFO] OSSimpleStats(): Cache still valid, retrieving last stored value")
}
arr = cachedArr // retrieve it
}
switch format.Payload {
case "json":
c.JSON(http.StatusOK, arr)
case "xml":
c.XML(http.StatusOK, arr)
case "yaml":
c.YAML(http.StatusOK, arr)
default:
c.HTML(http.StatusOK, "stats.tpl", environment(c, arr))
}
}