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.