Skip to content

Commit

Permalink
feat(matchmake-extension): Implement NotificationData methods
Browse files Browse the repository at this point in the history
The NotificationData methods are used by games to send notifications to
friends about user activity, among others. These notifications are
created or updated using `UpdateNotificationData`, which the server will
register and send to the connected friends (as seen on Mario Tennis Open).

The lifetime of these notifications is the same as the connection of the
user who sends them. That is, when a user sends a notification and then
disconnects, the notifications will be discarded.

All notifications sent over `UpdateNotificationData` are logged inside
the `tracking.notification_data` table to prevent abuse. The type of
these notifications is also constrained to a range of specific values
reserved for game-specific purposes (from 101 to 108).
  • Loading branch information
DaniElectra committed Jan 27, 2025
1 parent bcc53de commit 21b019a
Show file tree
Hide file tree
Showing 8 changed files with 395 additions and 1 deletion.
53 changes: 53 additions & 0 deletions matchmake-extension/database/get_notification_datas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package database

import (
"github.com/PretendoNetwork/nex-go/v2"
"github.com/PretendoNetwork/nex-go/v2/types"
common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals"
notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types"
pqextended "github.com/PretendoNetwork/pq-extended"
)

// GetNotificationDatas gets the notification datas that belong to friends of the user and match with any of the given types
func GetNotificationDatas(manager *common_globals.MatchmakingManager, sourcePID types.PID, notificationTypes []uint32) ([]notifications_types.NotificationEvent, *nex.Error) {
dataList := make([]notifications_types.NotificationEvent, 0)

var friendList []uint32
if manager.GetUserFriendPIDs != nil {
friendList = manager.GetUserFriendPIDs(uint32(sourcePID))
}

rows, err := manager.Database.Query(`SELECT
source_pid,
type,
param_1,
param_2,
param_str
FROM matchmaking.notifications WHERE active=true AND source_pid=ANY($1) AND type=ANY($2)
`, pqextended.Array(friendList), pqextended.Array(notificationTypes))
if err != nil {
return nil, nex.NewError(nex.ResultCodes.Core.Unknown, err.Error())
}

for rows.Next() {
notificationData := notifications_types.NewNotificationEvent()

err = rows.Scan(
&notificationData.PIDSource,
&notificationData.Type,
&notificationData.Param1,
&notificationData.Param2,
&notificationData.StrParam,
)
if err != nil {
common_globals.Logger.Critical(err.Error())
continue
}

dataList = append(dataList, notificationData)
}

rows.Close()

return dataList, nil
}
17 changes: 17 additions & 0 deletions matchmake-extension/database/inactivate_notification_datas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package database

import (
"github.com/PretendoNetwork/nex-go/v2"
"github.com/PretendoNetwork/nex-go/v2/types"
common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals"
)

// InactivateNotificationDatas marks the notifications of a given user as inactive
func InactivateNotificationDatas(manager *common_globals.MatchmakingManager, sourcePID types.PID) *nex.Error {
_, err := manager.Database.Exec(`UPDATE matchmaking.notifications SET active=false WHERE source_pid=$1`, sourcePID)
if err != nil {
return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error())
}

return nil
}
36 changes: 36 additions & 0 deletions matchmake-extension/database/update_notification_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package database

import (
"github.com/PretendoNetwork/nex-go/v2"
notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types"
common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals"
)

// UpdateNotificationData updates the notification data of the specified user and type
func UpdateNotificationData(manager *common_globals.MatchmakingManager, notificationData notifications_types.NotificationEvent) *nex.Error {
_, err := manager.Database.Exec(`INSERT INTO matchmaking.notifications (
source_pid,
type,
param_1,
param_2,
param_str
) VALUES (
$1,
$2,
$3,
$4,
$5
) ON CONFLICT (source_pid, type) DO UPDATE SET
param_1=$3, param_2=$4, param_str=$5, active=true WHERE source_pid=$1 AND type=$2`,
notificationData.PIDSource,
notificationData.Type,
notificationData.Param1,
notificationData.Param2,
notificationData.StrParam,
)
if err != nil {
return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error())
}

return nil
}
55 changes: 55 additions & 0 deletions matchmake-extension/get_friend_notification_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package matchmake_extension

import (
"github.com/PretendoNetwork/nex-go/v2"
"github.com/PretendoNetwork/nex-go/v2/types"
common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals"
"github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database"
matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension"
notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types"
)

func (commonProtocol *CommonProtocol) getFriendNotificationData(err error, packet nex.PacketInterface, callID uint32, uiType types.Int32) (*nex.RMCMessage, *nex.Error) {
if err != nil {
common_globals.Logger.Error(err.Error())
return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, err.Error())
}

// * This method can only receive notifications within the range 101-108, which are reserved for game-specific notifications
if uiType < 101 || uiType > 108 {
return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error")
}

connection := packet.Sender().(*nex.PRUDPConnection)
endpoint := connection.Endpoint().(*nex.PRUDPEndPoint)

commonProtocol.manager.Mutex.RLock()

notificationDatas, nexError := database.GetNotificationDatas(commonProtocol.manager, connection.PID(), []uint32{uint32(uiType)})
if nexError != nil {
commonProtocol.manager.Mutex.RUnlock()
return nil, nexError
}

commonProtocol.manager.Mutex.RUnlock()

dataList := types.NewList[notifications_types.NotificationEvent]()
dataList = notificationDatas

rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings())

dataList.WriteTo(rmcResponseStream)

rmcResponseBody := rmcResponseStream.Bytes()

rmcResponse := nex.NewRMCSuccess(endpoint, rmcResponseBody)
rmcResponse.ProtocolID = matchmake_extension.ProtocolID
rmcResponse.MethodID = matchmake_extension.MethodGetFriendNotificationData
rmcResponse.CallID = callID

if commonProtocol.OnAfterUpdateNotificationData != nil {
go commonProtocol.OnAfterGetFriendNotificationData(packet, uiType)
}

return rmcResponse, nil
}
60 changes: 60 additions & 0 deletions matchmake-extension/get_lst_friend_notification_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package matchmake_extension

import (
"github.com/PretendoNetwork/nex-go/v2"
"github.com/PretendoNetwork/nex-go/v2/types"
matchmake_extension "github.com/PretendoNetwork/nex-protocols-go/v2/matchmake-extension"
notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types"
common_globals "github.com/PretendoNetwork/nex-protocols-common-go/v2/globals"
"github.com/PretendoNetwork/nex-protocols-common-go/v2/matchmake-extension/database"
)

func (commonProtocol *CommonProtocol) getlstFriendNotificationData(err error, packet nex.PacketInterface, callID uint32, lstTypes types.List[types.UInt32]) (*nex.RMCMessage, *nex.Error) {
if err != nil {
common_globals.Logger.Error(err.Error())
return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, err.Error())
}

connection := packet.Sender().(*nex.PRUDPConnection)
endpoint := connection.Endpoint().(*nex.PRUDPEndPoint)

notificationTypes := make([]uint32, len(lstTypes))
for i, notificationType := range lstTypes {
// * This method can only receive notifications within the range 101-108, which are reserved for game-specific notifications
if notificationType < 101 || notificationType > 108 {
return nil, nex.NewError(nex.ResultCodes.Core.InvalidArgument, "change_error")
}

notificationTypes[i] = uint32(notificationType)
}

commonProtocol.manager.Mutex.RLock()

notificationDatas, nexError := database.GetNotificationDatas(commonProtocol.manager, connection.PID(), notificationTypes)
if nexError != nil {
commonProtocol.manager.Mutex.RUnlock()
return nil, nexError
}

commonProtocol.manager.Mutex.RUnlock()

dataList := types.NewList[notifications_types.NotificationEvent]()
dataList = notificationDatas

rmcResponseStream := nex.NewByteStreamOut(endpoint.LibraryVersions(), endpoint.ByteStreamSettings())

dataList.WriteTo(rmcResponseStream)

rmcResponseBody := rmcResponseStream.Bytes()

rmcResponse := nex.NewRMCSuccess(endpoint, rmcResponseBody)
rmcResponse.ProtocolID = matchmake_extension.ProtocolID
rmcResponse.MethodID = matchmake_extension.MethodGetlstFriendNotificationData
rmcResponse.CallID = callID

if commonProtocol.OnAfterUpdateNotificationData != nil {
go commonProtocol.OnAfterGetlstFriendNotificationData(packet, lstTypes)
}

return rmcResponse, nil
}
52 changes: 51 additions & 1 deletion matchmake-extension/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type CommonProtocol struct {
OnAfterModifyCurrentGameAttribute func(packet nex.PacketInterface, gid types.UInt32, attribIndex types.UInt32, newValue types.UInt32)
OnAfterBrowseMatchmakeSession func(packet nex.PacketInterface, searchCriteria match_making_types.MatchmakeSessionSearchCriteria, resultRange types.ResultRange)
OnAfterJoinMatchmakeSessionEx func(packet nex.PacketInterface, gid types.UInt32, strMessage types.String, dontCareMyBlockList types.Bool, participationCount types.UInt16)
OnAfterUpdateNotificationData func(packet nex.PacketInterface, uiType types.UInt32, uiParam1 types.UInt32, uiParam2 types.UInt32, strParam types.String)
OnAfterGetFriendNotificationData func(packet nex.PacketInterface, uiType types.Int32)
OnAfterGetlstFriendNotificationData func(packet nex.PacketInterface, lstTypes types.List[types.UInt32])
}

// SetDatabase defines the matchmaking manager to be used by the common protocol
Expand Down Expand Up @@ -81,18 +84,56 @@ func (commonProtocol *CommonProtocol) SetManager(manager *common_globals.Matchma
return
}

_, err = manager.Database.Exec(`CREATE TABLE IF NOT EXISTS matchmaking.notifications (
id bigserial PRIMARY KEY,
source_pid numeric(10),
type bigint,
param_1 bigint,
param_2 bigint,
param_str text,
active boolean NOT NULL DEFAULT true,
UNIQUE (source_pid, type)
)`)
if err != nil {
common_globals.Logger.Error(err.Error())
return
}

_, err = manager.Database.Exec(`CREATE TABLE IF NOT EXISTS tracking.notification_data (
id bigserial PRIMARY KEY,
date timestamp,
source_pid numeric(10),
type bigint,
param_1 bigint,
param_2 bigint,
param_str text
)`)
if err != nil {
common_globals.Logger.Error(err.Error())
return
}

// * In case the server is restarted, unregister any previous matchmake sessions
_, err = manager.Database.Exec(`UPDATE matchmaking.gatherings SET registered=false WHERE type='MatchmakeSession'`)
if err != nil {
common_globals.Logger.Error(err.Error())
return
}

// * Mark all notifications as inactive
_, err = manager.Database.Exec(`UPDATE matchmaking.notifications SET active=false`)
if err != nil {
common_globals.Logger.Error(err.Error())
return
}
}

// NewCommonProtocol returns a new CommonProtocol
func NewCommonProtocol(protocol matchmake_extension.Interface) *CommonProtocol {
endpoint := protocol.Endpoint().(*nex.PRUDPEndPoint)

commonProtocol := &CommonProtocol{
endpoint: protocol.Endpoint(),
endpoint: endpoint,
protocol: protocol,
}

Expand All @@ -111,6 +152,15 @@ func NewCommonProtocol(protocol matchmake_extension.Interface) *CommonProtocol {
protocol.SetHandlerModifyCurrentGameAttribute(commonProtocol.modifyCurrentGameAttribute)
protocol.SetHandlerBrowseMatchmakeSession(commonProtocol.browseMatchmakeSession)
protocol.SetHandlerJoinMatchmakeSessionEx(commonProtocol.joinMatchmakeSessionEx)
protocol.SetHandlerUpdateNotificationData(commonProtocol.updateNotificationData)
protocol.SetHandlerGetFriendNotificationData(commonProtocol.getFriendNotificationData)
protocol.SetHandlerGetlstFriendNotificationData(commonProtocol.getlstFriendNotificationData)

endpoint.OnConnectionEnded(func(connection *nex.PRUDPConnection) {
commonProtocol.manager.Mutex.Lock()
database.InactivateNotificationDatas(commonProtocol.manager, connection.PID())
commonProtocol.manager.Mutex.Unlock()
})

return commonProtocol
}
42 changes: 42 additions & 0 deletions matchmake-extension/tracking/log_notification_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package tracking

import (
"database/sql"
"time"

"github.com/PretendoNetwork/nex-go/v2"
notifications_types "github.com/PretendoNetwork/nex-protocols-go/v2/notifications/types"
)

// LogNotificationData logs the update of the notification data of a user with UpdateNotificationData
func LogNotificationData(db *sql.DB, notificationData notifications_types.NotificationEvent) *nex.Error {
eventTime := time.Now().UTC()

_, err := db.Exec(`INSERT INTO tracking.notification_data (
date,
source_pid,
type,
param_1,
param_2,
param_str
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
)`,
eventTime,
notificationData.PIDSource,
notificationData.Type,
notificationData.Param1,
notificationData.Param2,
notificationData.StrParam,
)
if err != nil {
return nex.NewError(nex.ResultCodes.Core.Unknown, err.Error())
}

return nil
}
Loading

0 comments on commit 21b019a

Please sign in to comment.