mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-08-28 11:34:16 -05:00
Add event logging via database and webhook
This commit is contained in:
@@ -48,6 +48,20 @@ type Config struct {
|
||||
AllowConnectWithoutDeviceID bool `xml:"allowConnectWithoutDeviceID"`
|
||||
|
||||
ServerName string `xml:"serverName,omitempty"`
|
||||
|
||||
EventReporting EventReportingConfig `xml:"eventReporting"`
|
||||
}
|
||||
|
||||
type EventReportingConfig struct {
|
||||
LogToDatabase bool `xml:"logToDatabase"`
|
||||
Webhooks []WebhookConfig `xml:"discord>webhook"`
|
||||
}
|
||||
|
||||
type WebhookConfig struct {
|
||||
Enabled bool `xml:"enabled"`
|
||||
URL string `xml:"url"`
|
||||
Author string `xml:"author,omitempty"`
|
||||
EventTypes []string `xml:"eventTypes>event"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -140,3 +154,9 @@ func GetConfig() Config {
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (c Config) RegisterWebhooks() {
|
||||
for _, webhook := range c.EventReporting.Webhooks {
|
||||
webhook.RegisterEventReporting()
|
||||
}
|
||||
}
|
||||
|
||||
97
common/webhook.go
Normal file
97
common/webhook.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"wwfc/logging"
|
||||
)
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, v := range slice {
|
||||
if v == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type webhookAuthor struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type webhookEmbed struct {
|
||||
Author webhookAuthor `json:"author,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type webhookPayload struct {
|
||||
Embeds []webhookEmbed `json:"embeds"`
|
||||
}
|
||||
|
||||
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:
|
||||
var sb strings.Builder
|
||||
for i, item := range v {
|
||||
sb.WriteString(encodeWebhookValue(item))
|
||||
if i < len(v)-1 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
default:
|
||||
return " - (unknown)"
|
||||
}
|
||||
}
|
||||
|
||||
func (w WebhookConfig) ReportEvent(eventType string, eventData map[string]any) {
|
||||
embed := webhookEmbed{
|
||||
Title: "> **" + eventType + "**",
|
||||
}
|
||||
|
||||
if w.Author != "" {
|
||||
embed.Author = webhookAuthor{Name: w.Author}
|
||||
}
|
||||
|
||||
for key, value := range eventData {
|
||||
embed.Description += "- " + key + "\n" + encodeWebhookValue(value) + "\n"
|
||||
}
|
||||
|
||||
// Send HTTP POST request
|
||||
jsonData, err := json.Marshal(webhookPayload{Embeds: []webhookEmbed{embed}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
resp, err := http.Post(w.URL, "application/json", strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
panic("received non-2xx response: " + resp.Status)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func (w WebhookConfig) RegisterEventReporting() {
|
||||
if !w.Enabled {
|
||||
return
|
||||
}
|
||||
logging.RegisterEventCallback(w.EventTypes, func(eventType string, eventData map[string]any) {
|
||||
w.ReportEvent(eventType, eventData)
|
||||
})
|
||||
}
|
||||
@@ -1,73 +1,118 @@
|
||||
<Config>
|
||||
<!-- The address the GameSpy services will bind to -->
|
||||
<gsAddress>127.0.0.1</gsAddress>
|
||||
<!-- The address the GameSpy services will bind to -->
|
||||
<gsAddress>127.0.0.1</gsAddress>
|
||||
|
||||
<!-- The address the frontend RPC server will bind to -->
|
||||
<frontendAddress>127.0.0.1:29998</frontendAddress>
|
||||
<!-- The address the NAS HTTP server will bind to -->
|
||||
<nasAddress>127.0.0.1</nasAddress>
|
||||
<nasPort>80</nasPort>
|
||||
|
||||
<!-- The address the frontend can reach the backend from -->
|
||||
<frontendBackendAddress>127.0.0.1:29999</frontendBackendAddress>
|
||||
<!-- The address the NAS HTTPS proxy server will bind to -->
|
||||
<nasAddressHttps>127.0.0.1</nasAddressHttps>
|
||||
<nasPortHttps>443</nasPortHttps>
|
||||
<enableHttps>false</enableHttps>
|
||||
|
||||
<!-- The address the backend RPC server will bind to -->
|
||||
<backendAddress>127.0.0.1:29999</backendAddress>
|
||||
<!--
|
||||
The address the payload server will be located at,
|
||||
leave blank to use the deprecated integrated server
|
||||
-->
|
||||
<payloadServerAddress>127.0.0.1:29997</payloadServerAddress>
|
||||
|
||||
<!-- The address the backend can reach the frontend from -->
|
||||
<backendFrontendAddress>127.0.0.1:29998</backendFrontendAddress>
|
||||
<!--
|
||||
The address the frontend RPC server will bind to.
|
||||
This is a local channel for internal server communication,
|
||||
that _MUST NOT_ be exposed to a public network or the internet.
|
||||
Do NOT change this address unless you are running the frontend
|
||||
and backend on separate servers or containers.
|
||||
-->
|
||||
<frontendAddress>127.0.0.1:29998</frontendAddress>
|
||||
|
||||
<!-- The address the NAS HTTP server will bind to -->
|
||||
<nasAddress>127.0.0.1</nasAddress>
|
||||
<nasPort>80</nasPort>
|
||||
<!-- The address the frontend can reach the backend from -->
|
||||
<frontendBackendAddress>127.0.0.1:29999</frontendBackendAddress>
|
||||
|
||||
<!-- The address the NAS HTTPS proxy server will bind to -->
|
||||
<nasAddressHttps>127.0.0.1</nasAddressHttps>
|
||||
<nasPortHttps>443</nasPortHttps>
|
||||
<enableHttps>false</enableHttps>
|
||||
<enableHttpsExploitWii>false</enableHttpsExploitWii>
|
||||
<enableHttpsExploitDS>false</enableHttpsExploitDS>
|
||||
<!--
|
||||
The address the backend RPC server will bind to.
|
||||
This is a local channel for internal server communication,
|
||||
please note the above warning on frontendAddress.
|
||||
-->
|
||||
<backendAddress>127.0.0.1:29999</backendAddress>
|
||||
|
||||
<!-- The address the payload server will be located at, leave blank to use legacy integrated payload server -->
|
||||
<payloadServerAddress>127.0.0.1:29997</payloadServerAddress>
|
||||
<!-- The address the backend can reach the frontend from -->
|
||||
<backendFrontendAddress>127.0.0.1:29998</backendFrontendAddress>
|
||||
|
||||
<!-- Path to the certificate and key used for modern web browser requests -->
|
||||
<certPath>fullchain.pem</certPath>
|
||||
<keyPath>privkey.pem</keyPath>
|
||||
<!-- Path to the certificate and key used for modern web browser requests -->
|
||||
<certPath>fullchain.pem</certPath>
|
||||
<keyPath>privkey.pem</keyPath>
|
||||
|
||||
<!-- Path to the certificate and key used for the Wii DNS exploit -->
|
||||
<certDerPathWii>naswii-cert.der</certDerPathWii>
|
||||
<keyPathWii>naswii-key.pem</keyPathWii>
|
||||
<!-- SSL/TLS exploit configuration -->
|
||||
<enableHttpsExploitWii>false</enableHttpsExploitWii>
|
||||
<enableHttpsExploitDS>false</enableHttpsExploitDS>
|
||||
|
||||
<!-- Path to the certificate, Wii client certificate and key used for the DS DNS exploit -->
|
||||
<certDerPathDS>nas-cert.der</certDerPathDS>
|
||||
<wiiCertDerPathDS>nwc.der</wiiCertDerPathDS>
|
||||
<keyPathDS>nas-key.pem</keyPathDS>
|
||||
<!-- Path to the certificate and key used for the Wii DNS exploit -->
|
||||
<certDerPathWii>naswii-cert.der</certDerPathWii>
|
||||
<keyPathWii>naswii-key.pem</keyPathWii>
|
||||
|
||||
<!-- Allow default Dolphin device keys to be used -->
|
||||
<allowDefaultDolphinKeys>true</allowDefaultDolphinKeys>
|
||||
<!-- Path to the certificate, Wii client certificate and key used for the DS DNS exploit -->
|
||||
<certDerPathDS>nas-cert.der</certDerPathDS>
|
||||
<wiiCertDerPathDS>nwc.der</wiiCertDerPathDS>
|
||||
<keyPathDS>nas-key.pem</keyPathDS>
|
||||
|
||||
<!-- Database Credentials -->
|
||||
<username>username</username>
|
||||
<password>password</password>
|
||||
<!-- Allow default Dolphin device keys to be used -->
|
||||
<allowDefaultDolphinKeys>true</allowDefaultDolphinKeys>
|
||||
|
||||
<!-- Database information -->
|
||||
<databaseAddress>127.0.0.1</databaseAddress>
|
||||
<databaseName>wwfc</databaseName>
|
||||
|
||||
<!-- Logging configuration -->
|
||||
<!-- Log verbosity
|
||||
0: No messages are logged.
|
||||
1: General messages are logged.
|
||||
2: General and error messages are logged.
|
||||
3: General, error, and warning messages are logged.
|
||||
4: General, error, warning, and informational messages are logged.
|
||||
-->
|
||||
<logLevel>4</logLevel>
|
||||
<!-- Log output
|
||||
None : Messages are discarded.
|
||||
StdOut : Messages are written to standard output.
|
||||
StdOutAndFile: Messages are written to both standard output and a file.
|
||||
-->
|
||||
<logOutput>StdOutAndFile</logOutput>
|
||||
<!-- Database Credentials -->
|
||||
<username>username</username>
|
||||
<password>password</password>
|
||||
|
||||
<!-- API secret -->
|
||||
<apiSecret>hQ3f57b3tW2WnjJH3v</apiSecret>
|
||||
<!-- Database information -->
|
||||
<databaseAddress>127.0.0.1</databaseAddress>
|
||||
<databaseName>wwfc</databaseName>
|
||||
|
||||
<!-- Logging configuration -->
|
||||
<!-- Log verbosity
|
||||
0: No messages are logged.
|
||||
1: General messages are logged.
|
||||
2: General and error messages are logged.
|
||||
3: General, error, and warning messages are logged.
|
||||
4: General, error, warning, and informational messages are logged.
|
||||
-->
|
||||
<logLevel>4</logLevel>
|
||||
<!-- Log output
|
||||
None : Messages are discarded.
|
||||
StdOut : Messages are written to standard output.
|
||||
StdOutAndFile: Messages are written to both standard output and a file.
|
||||
-->
|
||||
<logOutput>StdOutAndFile</logOutput>
|
||||
|
||||
<!-- API secret.
|
||||
This is used for API authentication and should only be shared with
|
||||
trusted administrators.
|
||||
-->
|
||||
<apiSecret>hQ3f57b3tW2WnjJH3v</apiSecret>
|
||||
|
||||
<eventReporting>
|
||||
<!-- Enable to log events to the "events" table in the database -->
|
||||
<logToDatabase>true</logToDatabase>
|
||||
<!-- Discord logging via webhook -->
|
||||
<discord>
|
||||
<!-- Multiple <webhook> elements can be added here -->
|
||||
<webhook>
|
||||
<!-- Enable sending events to this webhook -->
|
||||
<enabled>true</enabled>
|
||||
<!-- The URL of the webhook to send events to -->
|
||||
<url>https://discord.com/api/webhooks/...</url>
|
||||
<!-- The author tag to be displayed in the webhook embed, can be omitted -->
|
||||
<author>wfc-server event reporting</author>
|
||||
<!--
|
||||
The list of events that will be reported on this webhook.
|
||||
Hint: Use the event type "all" to report all events to this webhook.
|
||||
The following list contains all possible event types that can be reported:
|
||||
-->
|
||||
<eventTypes>
|
||||
<!-- <event>all</event> -->
|
||||
<event>backend_started</event>
|
||||
<event>backend_stopped</event>
|
||||
</eventTypes>
|
||||
</webhook>
|
||||
</discord>
|
||||
</eventReporting>
|
||||
</Config>
|
||||
|
||||
34
database/events.go
Normal file
34
database/events.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"wwfc/common"
|
||||
"wwfc/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
insertEventQuery = `
|
||||
INSERT INTO events (event_type, event_data)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id`
|
||||
)
|
||||
|
||||
func (c *Connection) InsertEvent(eventType string, eventData map[string]any) (int, error) {
|
||||
var eventId int
|
||||
err := c.pool.QueryRow(c.ctx, insertEventQuery, eventType, eventData).Scan(&eventId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return eventId, nil
|
||||
}
|
||||
|
||||
func (c *Connection) RegisterEventLogging(config common.Config, eventTypes []string) {
|
||||
if !config.EventReporting.LogToDatabase {
|
||||
return
|
||||
}
|
||||
logging.RegisterEventCallback(eventTypes, func(eventType string, eventData map[string]any) {
|
||||
_, err := c.InsertEvent(eventType, eventData)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
47
logging/event.go
Normal file
47
logging/event.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package logging
|
||||
|
||||
import "sync"
|
||||
|
||||
type eventCallbackConfig struct {
|
||||
Function func(eventType string, eventData map[string]any)
|
||||
EventTypes map[string]struct{}
|
||||
AllEvents bool
|
||||
}
|
||||
|
||||
var (
|
||||
eventCallbacks []eventCallbackConfig
|
||||
mutex sync.RWMutex
|
||||
)
|
||||
|
||||
func Event(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
|
||||
for _, eventType := range eventTypes {
|
||||
if eventType == "all" {
|
||||
allEvents = true
|
||||
eventTypeSet = make(map[string]struct{})
|
||||
break
|
||||
}
|
||||
eventTypeSet[eventType] = struct{}{}
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
eventCallbacks = append(eventCallbacks, eventCallbackConfig{
|
||||
Function: callback,
|
||||
EventTypes: eventTypeSet,
|
||||
AllEvents: allEvents,
|
||||
})
|
||||
}
|
||||
23
main.go
23
main.go
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
"wwfc/api"
|
||||
"wwfc/common"
|
||||
"wwfc/database"
|
||||
"wwfc/gamestats"
|
||||
"wwfc/gpcm"
|
||||
"wwfc/gpsp"
|
||||
@@ -70,12 +71,27 @@ type RPCPacket struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func connectAndLogEvent(eventType string) {
|
||||
var db *database.Connection
|
||||
defer func() {
|
||||
logging.Event(eventType, map[string]any{})
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
}()
|
||||
dbObject := database.Start(config)
|
||||
db = &dbObject
|
||||
dbObject.RegisterEventLogging(config, []string{eventType})
|
||||
}
|
||||
|
||||
// backendMain starts all the servers and creates an RPC server to communicate with the frontend
|
||||
func backendMain(noSignal, noReload bool) {
|
||||
config.RegisterWebhooks()
|
||||
defer connectAndLogEvent("backend_stopped")
|
||||
|
||||
err := os.Mkdir("state", 0755)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
logging.Error("BACKEN", err)
|
||||
os.Exit(1)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sigExit := make(chan os.Signal, 1)
|
||||
@@ -119,6 +135,9 @@ func backendMain(noSignal, noReload bool) {
|
||||
// Wait for all servers to start
|
||||
wg.Wait()
|
||||
|
||||
// Log via event that the backend has started
|
||||
go connectAndLogEvent("backend_started")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
|
||||
10
schema.sql
10
schema.sql
@@ -149,6 +149,16 @@ ALTER TABLE ONLY public.users ALTER COLUMN profile_id SET DEFAULT nextval('publi
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT users_pkey PRIMARY KEY (profile_id);
|
||||
|
||||
--
|
||||
-- Name: events; Type: TABLE; Schema: public; Owner: wiilink
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.events (
|
||||
id serial PRIMARY KEY,
|
||||
event_type character varying NOT NULL,
|
||||
event_data jsonb NOT NULL,
|
||||
event_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
|
||||
Reference in New Issue
Block a user