From 18140bebe97c2bad52d7d90f9cd8e6abcf5aba92 Mon Sep 17 00:00:00 2001 From: Palapeli <26661008+mkwcat@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:55:33 -0400 Subject: [PATCH] QR2: Add event logging --- config_example.xml | 18 ++++++++++++ gpcm/login.go | 2 +- logging/event.go | 12 ++++++++ logging/webhook.go | 29 ++++++------------- main.go | 2 +- qr2/group.go | 72 +++++++++++++++++++++++++++++++++++++++++++++- qr2/main.go | 16 +++++++++++ qr2/session.go | 16 +++++++++++ 8 files changed, 144 insertions(+), 23 deletions(-) diff --git a/config_example.xml b/config_example.xml index 9f70f5f..4c531d3 100644 --- a/config_example.xml +++ b/config_example.xml @@ -111,6 +111,24 @@ backend_started backend_stopped + profile_created + logged_in + logged_out + received_login_info + device_authenticated + reported_bad_packet + reported_stall + gpcm_returned_error + group_created + group_deleted + group_joined + group_left + group_host_changed + natneg_succeeded + natneg_failed + profile_kicked + profile_banned + profile_unbanned diff --git a/gpcm/login.go b/gpcm/login.go index 4faa68e..f690752 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -487,7 +487,7 @@ func (g *GameSpySession) verifyExLoginInfo(command common.GameSpyCommand, authTo "device_authenticated", map[string]any{ "profile_id": g.User.ProfileId, - "ng_device_id": g.User.NgDeviceId, + "ng_device_id": g.DeviceId, "payload_version": payloadVer, }, ) diff --git a/logging/event.go b/logging/event.go index 726080e..d517b30 100644 --- a/logging/event.go +++ b/logging/event.go @@ -25,6 +25,18 @@ func Event(eventType string, eventData map[string]any) { } } +func EventSynced(eventType string, eventData map[string]any) { + mutex.RLock() + defer mutex.RUnlock() + for _, callback := range eventCallbacks { + if callback.AllEvents { + callback.Function(eventType, eventData) + } else if _, ok := callback.EventTypes[eventType]; ok { + callback.Function(eventType, eventData) + } + } +} + func RegisterEventCallback(eventTypes []string, callback func(eventType string, eventData map[string]any)) { eventTypeSet := make(map[string]struct{}) allEvents := false diff --git a/logging/webhook.go b/logging/webhook.go index 380ad9f..e6ed6c7 100644 --- a/logging/webhook.go +++ b/logging/webhook.go @@ -2,8 +2,9 @@ package logging import ( "encoding/json" + "fmt" "net/http" - "strconv" + "reflect" "strings" ) @@ -38,31 +39,19 @@ type webhookPayload struct { } func encodeWebhookValue(value any) string { - switch v := value.(type) { - case string: - return " - ``" + strings.ReplaceAll(v, "``", "` `") + "``" - case int: - return " - " + strconv.Itoa(v) - case int32: - return " - " + strconv.Itoa(int(v)) - case int64: - return " - " + strconv.FormatInt(v, 10) - case float32: - return " - " + strconv.FormatFloat(float64(v), 'f', -1, 32) - case float64: - return " - " + strconv.FormatFloat(v, 'f', -1, 64) - case []any: + if reflect.TypeOf(value).Kind() == reflect.Array { var sb strings.Builder - for i, item := range v { - sb.WriteString(encodeWebhookValue(item)) - if i < len(v)-1 { + s := reflect.ValueOf(value) + for i := 0; i < s.Len(); i++ { + sb.WriteString(encodeWebhookValue(s.Index(i).Interface())) + if i < s.Len()-1 { sb.WriteString("\n") } } return sb.String() - default: - return " - (unknown)" } + + return " - ``" + strings.ReplaceAll(fmt.Sprintf("%v", value), "``", "` `") + "``" } func (w WebhookConfig) ReportEvent(eventType string, eventData map[string]any) { diff --git a/main.go b/main.go index 52a3879..4522d75 100644 --- a/main.go +++ b/main.go @@ -74,7 +74,7 @@ type RPCPacket struct { func connectAndLogEvent(eventType string) { var db database.Connection defer db.Close() - defer logging.Event(eventType, map[string]any{}) + defer logging.EventSynced(eventType, map[string]any{}) db = database.Start(config) db.RegisterEvents(config, []string{eventType}) } diff --git a/qr2/group.go b/qr2/group.go index 68d29e0..06943c3 100644 --- a/qr2/group.go +++ b/qr2/group.go @@ -85,6 +85,17 @@ func processResvOK(moduleName string, matchVersion int, reservation common.Match groups[group.GroupName] = group logging.Notice(moduleName, "Created new group", aurora.Cyan(group.GroupName)) + eventData := map[string]any{ + "group_id": group.GroupID, + "group_name": group.GroupName, + "game_name": group.GameName, + "match_type": group.MatchType, + "host_id": sender.Data["dwc_pid"], + } + if group.GameName == "mariokartwii" { + eventData["mario_kart_wii_region"] = group.MKWRegion + } + logging.Event("group_created", eventData) } // Keep group ID updated @@ -101,6 +112,15 @@ func processResvOK(moduleName string, matchVersion int, reservation common.Match logging.Notice(moduleName, "New player", aurora.BrightCyan(destination.Data["dwc_pid"]), "in group", aurora.Cyan(group.GroupName)) + logging.Event( + "group_joined", + map[string]any{ + "group_id": group.GroupID, + "group_name": group.GroupName, + "profile_id": destination.Data["dwc_pid"], + }, + ) + group.LastJoinIndex++ destination.Data["+joinindex"] = strconv.Itoa(group.LastJoinIndex) if matchVersion == 90 { @@ -332,7 +352,7 @@ func ProcessNATNEGReport(result byte, ip1 string, ip2 string) { } if session1.groupPointer == nil || session1.groupPointer != session2.groupPointer { - logging.Warn(moduleName, "Received NATNEG report for two IPs in different groups") + logging.Error(moduleName, "Received NATNEG report for two IPs in different groups") return } @@ -353,6 +373,43 @@ func ProcessNATNEGReport(result byte, ip1 string, ip2 string) { connFail2++ session1.Data["+conn_fail"] = strconv.Itoa(connFail1) session2.Data["+conn_fail"] = strconv.Itoa(connFail2) + logging.Event( + "natneg_failed", + map[string]any{ + "group_id": session1.groupPointer.GroupID, + "group_name": session1.groupPointer.GroupName, + "peers": []map[string]string{ + { + "profile_id": session1.Data["dwc_pid"], + "failed_count": strconv.Itoa(connFail1), + "ip_address": ip1, + }, + { + "profile_id": session2.Data["dwc_pid"], + "failed_count": strconv.Itoa(connFail2), + "ip_address": ip2, + }, + }, + }, + ) + } else { + logging.Event( + "natneg_succeeded", + map[string]any{ + "group_id": session1.groupPointer.GroupID, + "group_name": session1.groupPointer.GroupName, + "peers": []map[string]string{ + { + "profile_id": session1.Data["dwc_pid"], + "ip_address": ip1, + }, + { + "profile_id": session2.Data["dwc_pid"], + "ip_address": ip2, + }, + }, + }, + ) } } @@ -449,8 +506,21 @@ func (g *Group) findNewServer() { } } + eventData := map[string]any{ + "group_id": g.GroupID, + "group_name": g.GroupName, + } + if server != nil { + eventData["new_host_id"] = server.Data["dwc_pid"] + } + if g.server != nil { + eventData["old_host_id"] = g.server.Data["dwc_pid"] + } + g.server = server g.updateMatchType() + + logging.Event("group_host_changed", eventData) } // updateMatchType updates the match type of the group based on the host's dwc_mtype value. diff --git a/qr2/main.go b/qr2/main.go index 0ddd8be..f8ebc8a 100644 --- a/qr2/main.go +++ b/qr2/main.go @@ -6,6 +6,7 @@ import ( "sync" "time" "wwfc/common" + "wwfc/database" "wwfc/logging" "wwfc/nhttp" @@ -25,6 +26,7 @@ const ( AvailableRequest = 0x09 ClientRegisteredReply = 0x0A + // Added by WiiLink ClientExploitReply = 0x10 ) @@ -44,6 +46,20 @@ func StartServer(reload bool) { panic(err) } + // Connect to database for event logging + if config.EventReporting.LogToDatabase { + db := database.Start(config) + db.RegisterEvents(config, []string{ + "group_created", + "group_deleted", + "group_joined", + "group_left", + "group_host_changed", + "natneg_succeeded", + "natneg_failed", + }) + } + masterConn = conn inShutdown.SetFalse() diff --git a/qr2/session.go b/qr2/session.go index 1d906a9..3b359a7 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -83,6 +83,13 @@ func (session *Session) removeFromGroup() { if len(session.groupPointer.players) == 0 { logging.Notice("QR2", "Deleting group", aurora.Cyan(session.groupPointer.GroupName)) delete(groups, session.groupPointer.GroupName) + logging.Event( + "group_deleted", + map[string]any{ + "group_id": session.groupPointer.GroupID, + "group_name": session.groupPointer.GroupName, + }, + ) } else if session.groupPointer.server == session { logging.Notice("QR2", "Server down in group", aurora.Cyan(session.groupPointer.GroupName)) session.groupPointer.server = nil @@ -101,6 +108,15 @@ func (session *Session) removeFromGroup() { session.groupPointer = nil session.GroupName = "" + + logging.Event( + "group_left", + map[string]any{ + "group_id": session.groupPointer.GroupID, + "group_name": session.groupPointer.GroupName, + "profile_id": session.Data["dwc_pid"], + }, + ) } // Update session data, creating the session if it doesn't exist. Returns a copy of the session data.