Uptimeow/api/handler.go
2024-10-17 18:47:37 +08:00

453 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"database/sql"
"encoding/json"
"github.com/MeowLynxSea/Uptimeow/config"
"github.com/MeowLynxSea/Uptimeow/internal/rcon"
_ "github.com/glebarez/sqlite"
"github.com/gorilla/websocket"
"github.com/robfig/cron/v3"
"github.com/wanghuiyt/ding"
"log"
"net/http"
"strconv"
"strings"
"time"
)
var GlobalConfig config.ConfigData
var saveCron = cron.New()
var isOnline bool
var tps, tps5, tps15 float64
var onlinePlayer, maxPlayer int
var playerList []string
var db *sql.DB
var warnLevel int
const (
warnLevelNormal = 0
warnLevelWarning = 1
warnLevelCritical = 2
)
type ServerData struct {
Time time.Time `json:"time"`
IsOnline bool `json:"is_online"`
Tps float64 `json:"tps"`
OnlinePlayer int `json:"online_player"`
MaxPlayer int `json:"max_player"`
}
// Response 是发送给WebSocket客户端的响应结构
type Response struct {
Code int `json:"code"`
Data []ServerData `json:"data"`
}
type ServerInfo struct {
ServerName string `json:"server_name"`
ServerAddress string `json:"server_address"`
ServerWebsite string `json:"server_website"`
ServerDescription string `json:"server_description"`
}
type DetailedInfo struct {
Time time.Time `json:"time"`
IsOnline bool `json:"is_online"`
Tps float64 `json:"tps"`
OnlinePlayer int `json:"online_player"`
MaxPlayer int `json:"max_player"`
PlayerList string `json:"player_list,omitempty"`
}
func pushDingTalkBot(message string, msgtype string) {
if GlobalConfig.Warn.DingTalkBot.Enabled {
dingMsger := ding.Webhook{
AccessToken: GlobalConfig.Warn.DingTalkBot.AccessToken,
Secret: GlobalConfig.Warn.DingTalkBot.Secret,
}
if GlobalConfig.Warn.DingTalkBot.AtMobile != "" {
err := dingMsger.SendMessageText(message, GlobalConfig.Warn.DingTalkBot.AtMobile)
if err != nil {
log.Println("[ERROR] 钉钉机器人推送失败,原因: ", err)
} else {
log.Println("[INFO] 钉钉机器人推送[" + msgtype + "]成功")
}
} else {
err := dingMsger.SendMessageText(message)
if err != nil {
log.Println("[ERROR] 钉钉机器人推送失败,原因: ", err)
} else {
log.Println("[INFO] 钉钉机器人推送[" + msgtype + "]成功")
}
}
}
}
func init() {
GlobalConfig = config.Load()
warnLevel = 0
pushDingTalkBot("【成功】Uptimeow 监控已上线", "成功消息")
db, err := sql.Open("sqlite", "data/history.db")
if err != nil {
log.Fatal(err)
}
// 确保数据库连接是有效的
err = db.Ping()
if err != nil {
log.Fatal(err)
}
// 创建表data
createTableSQL := `
CREATE TABLE IF NOT EXISTS data (
time_index DATETIME NOT NULL PRIMARY KEY,
online BOOLEAN,
tps INTEGER,
online_player INTEGER,
max_player INTEGER,
player_list TEXT
);
`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatal(err)
}
saveCron.AddFunc("@every 10s", func() {
currentTime := time.Now()
// log.Println("[DEBUG] Saving data to database")
err = db.Ping()
if err != nil {
log.Println(err)
}
if !isOnline {
_, err = db.Exec("INSERT INTO data (time_index, online, tps, online_player, max_player, player_list) VALUES (?, ?, ?, ?, ?, ?)", currentTime.Format("2006-01-02 15:04:05"), 0, tps, 0, 0, "")
} else {
if tps != 0 {
_, err = db.Exec("INSERT INTO data (time_index, online, tps, online_player, max_player, player_list) VALUES (?, ?, ?, ?, ?, ?)", currentTime.Format("2006-01-02 15:04:05"), isOnline, tps, onlinePlayer, maxPlayer, strings.Join(playerList, ","))
}
}
if err != nil {
log.Println("[ERROR] Failed to insert data into database:] ", err)
}
switch warnLevel {
case warnLevelNormal:
if !isOnline && GlobalConfig.Warn.EnabledType.Offline {
warnLevel = warnLevelCritical
pushDingTalkBot("【紧急】服务器离线\n经监测服务器已离线请尽快处理\n时间"+currentTime.Format("2006-01-02 15:04:05"), "异常告警")
break
}
if tps < GlobalConfig.Warn.EnabledType.LowTps.Threold && GlobalConfig.Warn.EnabledType.LowTps.Enabled && tps != 0 {
warnLevel = warnLevelWarning
pushDingTalkBot("【警告】TPS过低报警\n服务器TPS低于设定值("+strconv.FormatFloat(GlobalConfig.Warn.EnabledType.LowTps.Threold, 'f', 2, 64)+")\n当前TPS"+strconv.FormatFloat(tps, 'f', 2, 64)+"\n时间"+currentTime.Format("2006-01-02 15:04:05"), "异常告警")
}
case warnLevelWarning:
if !isOnline && GlobalConfig.Warn.EnabledType.Offline {
warnLevel = warnLevelCritical
pushDingTalkBot("【紧急】服务器离线\n经监测服务器已离线请尽快处理\n时间"+currentTime.Format("2006-01-02 15:04:05"), "异常告警")
break
}
if tps >= GlobalConfig.Warn.EnabledType.LowTps.Threold && GlobalConfig.Warn.EnabledType.LowTps.Enabled {
warnLevel = warnLevelNormal
pushDingTalkBot("【恢复】服务器TPS恢复正常\n时间"+currentTime.Format("2006-01-02 15:04:05"), "成功消息")
}
case warnLevelCritical:
if isOnline && GlobalConfig.Warn.EnabledType.Offline {
warnLevel = warnLevelNormal
pushDingTalkBot("【恢复】服务器已恢复在线\n时间"+currentTime.Format("2006-01-02 15:04:05"), "成功消息")
}
}
})
saveCron.Start()
isOnline = false
go rcon.InitRcon(callback)
}
func toInt(value interface{}) int {
switch v := value.(type) {
case int:
return v
case int64:
return int(v)
case float64:
return int(v)
case string:
i, _ := strconv.Atoi(v)
return i
default:
return 0
}
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// 允许所有跨域请求,或者你可以在这里添加更复杂的验证逻辑
return true
},
}
// WebSocketHandler 处理WebSocket连接
func WebSocketHandler(w http.ResponseWriter, r *http.Request) {
localDB, err := sql.Open("sqlite", "data/history.db")
if err != nil {
log.Fatal(err)
}
defer localDB.Close()
// 确保数据库连接是有效的
err = localDB.Ping()
if err != nil {
log.Fatal(err)
}
// 将HTTP连接升级为WebSocket连接
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Error upgrading to WebSocket:", err)
return
}
defer conn.Close()
// 在这里实现WebSocket的消息处理逻辑
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("Error reading message:", err)
break
}
var resp Response
clientTimeFormat := "2006/01/02 15:04:05"
switch {
case string(message)[:13] == "earlier than ":
// 解析时间并获取数据
// log.Println("Received earlier than request")
reqTime, err := time.Parse(clientTimeFormat, strings.TrimPrefix(string(message), "earlier than "))
if err != nil {
log.Println("Error parsing time:", err)
continue
}
resp.Data, err = getEarlierData(localDB, reqTime)
if err != nil {
log.Println("Error getting data from database:", err)
continue
}
case string(message)[:11] == "later than ":
// 解析时间并获取数据
// log.Println("Received later than request")
reqTime, err := time.Parse(clientTimeFormat, strings.TrimPrefix(string(message), "later than "))
if err != nil {
log.Println("Error parsing time:", err)
continue
}
resp.Data, err = getLaterData(localDB, reqTime)
if err != nil {
log.Println("Error getting data from database:", err)
continue
}
default:
log.Println("Received unknown command " + string(message))
continue
}
if err != nil {
log.Println("Error getting data from database:", err)
continue
}
resp.Code = 200
// 序列化数据为JSON
jsonResp, err := json.Marshal(resp)
if err != nil {
log.Println("Error marshaling response:", err)
continue
}
// 发送数据给客户端
if err := conn.WriteMessage(websocket.TextMessage, jsonResp); err != nil {
log.Println("Error writing message:", err)
break
}
}
}
func getLaterData(database *sql.DB, t time.Time) ([]ServerData, error) {
dbTime := t.Format("2006-01-02 15:04:05")
query := `SELECT time_index, online, tps, online_player, max_player
FROM data WHERE time_index > ? ORDER BY time_index ASC`
rows, err := database.Query(query, dbTime)
if err != nil {
return nil, err
}
defer rows.Close()
var data []ServerData
for rows.Next() {
var sd ServerData
if err := rows.Scan(&sd.Time, &sd.IsOnline, &sd.Tps, &sd.OnlinePlayer, &sd.MaxPlayer); err != nil {
return nil, err
}
data = append(data, sd)
}
return data, nil
}
func getEarlierData(database *sql.DB, t time.Time) ([]ServerData, error) {
dbTime := t.Format("2006-01-02 15:04:05")
query := `SELECT time_index, online, tps, online_player, max_player
FROM data WHERE time_index < ? ORDER BY time_index DESC LIMIT 60`
rows, err := database.Query(query, dbTime)
if err != nil {
return nil, err
}
defer rows.Close()
var data []ServerData
for rows.Next() {
var sd ServerData
if err := rows.Scan(&sd.Time, &sd.IsOnline, &sd.Tps, &sd.OnlinePlayer, &sd.MaxPlayer); err != nil {
return nil, err
}
data = append(data, sd)
}
reverse(data)
return data, nil
}
func reverse(slice []ServerData) {
last := len(slice) - 1
for i := 0; i < len(slice)/2; i++ {
slice[i], slice[last-i] = slice[last-i], slice[i]
}
}
func APIHandler(w http.ResponseWriter, r *http.Request) {
// 设置响应内容类型为JSON
w.Header().Set("Content-Type", "application/json")
// 解析请求参数
queryParams := r.URL.Query()
requestType := queryParams.Get("type")
// 检查请求类型是否为 server_info
switch requestType {
case "server_info":
// 准备要返回的数据
serverInfo := ServerInfo{
ServerName: GlobalConfig.ServerInfo.Name,
ServerAddress: GlobalConfig.ServerInfo.Address,
ServerWebsite: GlobalConfig.ServerInfo.Website,
ServerDescription: GlobalConfig.ServerInfo.Description,
}
// 创建响应结构
response := struct {
Code int `json:"code"`
Data ServerInfo `json:"data"`
}{
Code: 200,
Data: serverInfo,
}
// 将响应结构序列化为JSON并写入响应体
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
case "detailed_info":
clientTimeFormat := "2006/01/02 15:04:05"
t, err := time.Parse(clientTimeFormat, queryParams.Get("time"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
dbTime := t.Format("2006-01-02 15:04:05")
localDB, err := sql.Open("sqlite", "data/history.db")
if err != nil {
log.Fatal(err)
}
defer localDB.Close()
query := `SELECT time_index, online, tps, online_player, max_player, player_list
FROM data WHERE time_index > ? ORDER BY time_index ASC`
rows, err := localDB.Query(query, dbTime)
if err != nil {
return
}
defer rows.Close()
var data []DetailedInfo
for rows.Next() {
var sd DetailedInfo
var playerList string
if err := rows.Scan(&sd.Time, &sd.IsOnline, &sd.Tps, &sd.OnlinePlayer, &sd.MaxPlayer, &playerList); err != nil {
return
}
sd.PlayerList = playerList
data = append(data, sd)
}
// 创建响应结构
response := struct {
Code int `json:"code"`
Data DetailedInfo `json:"data"`
}{
Code: 200,
Data: data[0],
}
// 将响应结构序列化为JSON并写入响应体
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
default:
http.Error(w, "Invalid request type", http.StatusBadRequest)
}
}
func callback(data string) {
// log.Println("[DEBUG] Receive callback data: " + data)
var jsonData map[string]interface{}
err := json.Unmarshal([]byte(data), &jsonData)
if err != nil {
log.Fatalln(err)
}
switch toInt(jsonData["type"]) {
case rcon.DataType_connection_success:
isOnline = true
log.Println("[INFO] RCON connection success")
case rcon.DataType_connection_error:
isOnline = false
tps, tps5, tps15, onlinePlayer, maxPlayer, playerList = 0, 0, 0, 0, 0, []string{}
log.Println("[ERROR] RCON connection error")
case rcon.DataType_execution_error:
isOnline = false
tps, tps5, tps15, onlinePlayer, maxPlayer, playerList = 0, 0, 0, 0, 0, []string{}
log.Println("[ERROR] RCON execution error")
case rcon.DataType_data_tps:
log.Println("[DEBUG] TPS: " + strconv.FormatFloat(jsonData["data"].(map[string]interface{})["l1m"].(float64), 'f', -1, 64))
tps = jsonData["data"].(map[string]interface{})["l1m"].(float64)
tps5 = jsonData["data"].(map[string]interface{})["l5m"].(float64)
tps15 = jsonData["data"].(map[string]interface{})["l15m"].(float64)
case rcon.DataType_data_list:
log.Println("[DEBUG] Player online: " + strconv.FormatFloat(jsonData["data"].(map[string]interface{})["online_player"].(float64), 'f', -1, 64) + "/" + strconv.FormatFloat(jsonData["data"].(map[string]interface{})["max_player"].(float64), 'f', -1, 64))
onlinePlayer = int(jsonData["data"].(map[string]interface{})["online_player"].(float64))
maxPlayer = int(jsonData["data"].(map[string]interface{})["max_player"].(float64))
//read player list
playerList = []string{}
for _, player := range jsonData["data"].(map[string]interface{})["player_list"].([]interface{}) {
playerList = append(playerList, player.(string))
}
}
}