Merge pull request #42 from PretendoNetwork/feat/grpc-friends-data
Some checks failed
Build and Publish Docker Image / Build and Publish Docker Image (amd64) (push) Has been cancelled
Build and Publish Docker Image / Build and Publish Docker Image (arm64) (push) Has been cancelled

feat(grpc): implement fetch user data for Wii U & 3DS
This commit is contained in:
Jemma Poffinbarger
2026-05-29 18:47:20 -05:00
committed by GitHub
14 changed files with 780 additions and 5 deletions

View File

@@ -98,4 +98,5 @@ All configuration options are handled via environment variables
| `PN_FRIENDS_CONFIG_ACCOUNT_GRPC_PORT` | Port for your account server gRPC service | Yes |
| `PN_FRIENDS_CONFIG_ACCOUNT_GRPC_API_KEY` | API key for your account server gRPC service | No (Assumed to be an open gRPC API) |
| `PN_FRIENDS_CONFIG_HEALTH_CHECK_PORT` | Port for the basic UDP health check server | No |
| `PN_FRIENDS_CONFIG_ENABLE_BELLA` | Enables a debug user named "Bella" which is always assigned as your friend | No | No
| `PN_FRIENDS_CONFIG_ENABLE_BELLA` | Enables a debug user named "Bella" which is always assigned as your friend | No |
| `PN_FRIENDS_CONFIG_MII_DECRYPT_KEY` | AES key used to decrypt 3DS Mii data (as a hex string) | Yes |

44
database/3ds/get_mii.go Normal file
View File

@@ -0,0 +1,44 @@
package database_3ds
import (
"database/sql"
"github.com/PretendoNetwork/friends/database"
"github.com/PretendoNetwork/nex-go/v2/types"
friends_3ds_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-3ds/types"
)
// GetMii returns the Mii of a specified user
func GetMii(pid types.PID) (friends_3ds_types.FriendMii, error) {
friendMii := friends_3ds_types.NewFriendMii()
rows, err := database.Manager.QueryRow(`
SELECT mii_name, mii_profanity, mii_character_set, mii_data, mii_changed FROM "3ds".user_data WHERE pid=$1`, pid)
if err != nil {
return friendMii, err
}
var miiName string
var miiProfanity bool
var miiCharacterSet uint8
var miiData []byte
var changedTime uint64
err = rows.Scan(&pid, &miiName, &miiProfanity, &miiCharacterSet, &miiData, &changedTime)
if err != nil {
if err == sql.ErrNoRows {
return friendMii, database.ErrPIDNotFound
} else {
return friendMii, err
}
}
friendMii.PID = types.NewPID(uint64(pid))
friendMii.Mii.Name = types.NewString(miiName)
friendMii.Mii.ProfanityFlag = types.NewBool(miiProfanity)
friendMii.Mii.CharacterSet = types.NewUInt8(miiCharacterSet)
friendMii.Mii.MiiData = types.NewBuffer(miiData)
friendMii.ModifiedAt = types.NewDateTime(changedTime)
return friendMii, nil
}

View File

@@ -0,0 +1,72 @@
package database_3ds
import (
"database/sql"
"github.com/PretendoNetwork/friends/database"
"github.com/PretendoNetwork/nex-go/v2/types"
friends_3ds_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-3ds/types"
)
// GetUserData returns a data for a specific user
func GetUserData(pid types.PID) (friends_3ds_types.FriendPersistentInfo, error) {
friendPersistentInfo := friends_3ds_types.NewFriendPersistentInfo()
row, err := database.Manager.QueryRow(`
SELECT pid, region, area,
language, country, favorite_title,
favorite_title_version, comment,
comment_changed, last_online, mii_changed
FROM "3ds".user_data WHERE pid=$1
`, pid)
if err != nil {
if err == sql.ErrNoRows {
return friendPersistentInfo, database.ErrPIDNotFound
} else {
return friendPersistentInfo, err
}
}
var region uint8
var area uint8
var language uint8
var country uint8
var titleID uint64
var titleVersion uint16
var message string
var lastOnlineTime uint64
var msgUpdateTime uint64
var miiModifiedAtTime uint64
err = row.Scan(
&pid,
&region,
&area,
&language,
&country,
&titleID,
&titleVersion,
&message,
&msgUpdateTime,
&lastOnlineTime,
&miiModifiedAtTime,
)
if err != nil {
return friendPersistentInfo, err
}
friendPersistentInfo.PID = types.NewPID(uint64(pid))
friendPersistentInfo.Region = types.NewUInt8(region)
friendPersistentInfo.Country = types.NewUInt8(country)
friendPersistentInfo.Area = types.NewUInt8(area)
friendPersistentInfo.Language = types.NewUInt8(language)
friendPersistentInfo.Platform = types.NewUInt8(2) // * Always 3DS
friendPersistentInfo.GameKey.TitleID = types.NewUInt64(titleID)
friendPersistentInfo.GameKey.TitleVersion = types.NewUInt16(titleVersion)
friendPersistentInfo.Message = types.NewString(message)
friendPersistentInfo.MessageUpdatedAt = types.NewDateTime(msgUpdateTime)
friendPersistentInfo.MiiModifiedAt = types.NewDateTime(miiModifiedAtTime)
friendPersistentInfo.LastOnline = types.NewDateTime(lastOnlineTime)
return friendPersistentInfo, nil
}

View File

@@ -0,0 +1,86 @@
package database_wiiu
import (
"database/sql"
"github.com/PretendoNetwork/friends/database"
"github.com/PretendoNetwork/friends/globals"
"github.com/PretendoNetwork/nex-go/v2/types"
friends_wiiu_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-wiiu/types"
)
// GetUserData returns a data for a specific user
func GetUserData(pid types.PID) (friends_wiiu_types.FriendInfo, error) {
friendInfo := friends_wiiu_types.NewFriendInfo()
row, err := database.Manager.QueryRow(`
SELECT
u.comment, u.comment_changed,
u.last_online,
bi.username, bi.unknown,
ai.unknown1, ai.unknown2,
mii.name, mii.unknown1, mii.unknown2, mii.data, mii.unknown_datetime
FROM wiiu.user_data AS u
INNER JOIN wiiu.principal_basic_info AS bi ON bi.pid = $1
INNER JOIN wiiu.network_account_info AS ai ON ai.pid = $1
INNER JOIN wiiu.mii AS mii ON mii.pid = $1
WHERE u.pid=$1
LIMIT 1
`, pid)
if err != nil {
if err == sql.ErrNoRows {
return friendInfo, database.ErrPIDNotFound
} else {
return friendInfo, err
}
}
var date uint64
var lastOnlineTime uint64
var commentContents string
var commentChanged uint64 = 0
var nnid string
var unknown uint8
var unknown1 uint8
var unknown2 uint8
var miiName string
var miiUnknown1 uint8
var miiUnknown2 uint8
var miiData []byte
var miiDatetime uint64
err = row.Scan(&commentContents, &commentChanged, &lastOnlineTime, &nnid, &unknown, &unknown1, &unknown2, &miiName, &miiUnknown1, &miiUnknown2, &miiData, &miiDatetime)
if err != nil {
return friendInfo, err
}
friendInfo.NNAInfo.Unknown1 = types.NewUInt8(unknown1)
friendInfo.NNAInfo.Unknown2 = types.NewUInt8(unknown2)
friendInfo.NNAInfo.PrincipalBasicInfo.PID = types.NewPID(uint64(pid))
friendInfo.NNAInfo.PrincipalBasicInfo.NNID = types.NewString(nnid)
friendInfo.NNAInfo.PrincipalBasicInfo.Unknown = types.NewUInt8(unknown)
friendInfo.NNAInfo.PrincipalBasicInfo.Mii.Name = types.NewString(miiName)
friendInfo.NNAInfo.PrincipalBasicInfo.Mii.Unknown1 = types.NewUInt8(miiUnknown1)
friendInfo.NNAInfo.PrincipalBasicInfo.Mii.Unknown2 = types.NewUInt8(miiUnknown2)
friendInfo.NNAInfo.PrincipalBasicInfo.Mii.MiiData = types.NewBuffer(miiData)
friendInfo.NNAInfo.PrincipalBasicInfo.Mii.Datetime = types.NewDateTime(miiDatetime)
lastOnline := types.NewDateTime(0).Now()
connectedUser, ok := globals.ConnectedUsers.Get(uint32(pid))
if ok && connectedUser != nil {
// * Online
friendInfo.Presence = connectedUser.PresenceV2.Copy().(friends_wiiu_types.NintendoPresenceV2)
} else {
// * Offline
lastOnline = types.NewDateTime(lastOnlineTime) // TODO - Change this
}
friendInfo.Status.Unknown = types.NewUInt8(0)
friendInfo.Status.Contents = types.NewString(commentContents)
friendInfo.Status.LastChanged = types.NewDateTime(commentChanged)
friendInfo.BecameFriend = types.NewDateTime(date)
friendInfo.LastOnline = lastOnline
friendInfo.Unknown = types.NewUInt64(0)
return friendInfo, nil
}

View File

@@ -14,6 +14,7 @@ type config struct {
AccountGRPCAPIKey string `envconf:"optional"`
HealthCheckPort uint16 `envconf:"optional"`
EnableBella bool `envconf:"optional"`
MiiDecryptKey string
}
var Config *config = &config{}

4
go.mod
View File

@@ -5,7 +5,7 @@ go 1.24.0
toolchain go1.24.3
require (
github.com/PretendoNetwork/grpc/go v0.0.0-20260114221322-0631a1e0c840
github.com/PretendoNetwork/grpc/go v0.0.0-20260501210425-981c793afb28
github.com/PretendoNetwork/nex-go/v2 v2.3.0
github.com/PretendoNetwork/nex-protocols-common-go/v2 v2.4.0
github.com/PretendoNetwork/nex-protocols-go/v2 v2.2.2
@@ -15,6 +15,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
)
require (
@@ -43,5 +44,4 @@ require (
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

4
go.sum
View File

@@ -1,5 +1,5 @@
github.com/PretendoNetwork/grpc/go v0.0.0-20260114221322-0631a1e0c840 h1:IkflRrU2XT/8voysYxZTxcQYPyMBCt7yBHWy8U6Q/tU=
github.com/PretendoNetwork/grpc/go v0.0.0-20260114221322-0631a1e0c840/go.mod h1:L6We4KkcQeiQVPrF7iu8Zax0B1Bm0v4nssR1JOAiRFQ=
github.com/PretendoNetwork/grpc/go v0.0.0-20260501210425-981c793afb28 h1:BHRf3HF4Wyavo1+GAzaem+dqsox/V2asr91W3YY8GbI=
github.com/PretendoNetwork/grpc/go v0.0.0-20260501210425-981c793afb28/go.mod h1:L6We4KkcQeiQVPrF7iu8Zax0B1Bm0v4nssR1JOAiRFQ=
github.com/PretendoNetwork/nex-go/v2 v2.3.0 h1:CQNm/DzYhXvyzD/5l+Dxfp0/AbObuCfyfhLAeY6BejI=
github.com/PretendoNetwork/nex-go/v2 v2.3.0/go.mod h1:2xKxiTtNxGliQ80xeicc6w3D53hmunOndoB3XJxUn/8=
github.com/PretendoNetwork/nex-protocols-common-go/v2 v2.4.0 h1:EhXj1EDbNgdg40BPx/7n1HHsAy/DayGIWthu81UNyvI=

97
grpc/get_user_data_3ds.go Normal file
View File

@@ -0,0 +1,97 @@
package grpc
import (
"context"
"time"
"github.com/PretendoNetwork/friends/database"
database_3ds "github.com/PretendoNetwork/friends/database/3ds"
"github.com/PretendoNetwork/friends/globals"
"github.com/PretendoNetwork/friends/utility"
pb "github.com/PretendoNetwork/grpc/go/friends/v2"
"github.com/PretendoNetwork/nex-go/v2/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUserData3DSRequest) (*pb.GetUserData3DSResponse, error) {
user, err := database_3ds.GetUserData(types.PID(in.GetPid()))
if err != nil {
if err == database.ErrPIDNotFound {
return nil, status.Errorf(codes.NotFound, "PID was not found")
} else {
globals.Logger.Critical(err.Error())
return nil, status.Errorf(codes.Internal, "Internal error")
}
}
miiData, err := database_3ds.GetMii(types.PID(in.GetPid()))
if err != nil {
if err == database.ErrPIDNotFound {
return nil, status.Errorf(codes.NotFound, "PID was not found")
} else {
globals.Logger.Critical(err.Error())
return nil, status.Errorf(codes.Internal, "Internal error")
}
}
gameKey := &pb.GameKey{
TitleId: uint64(user.GameKey.TitleID),
TitleVersion: uint32(user.GameKey.TitleVersion),
}
mii := &pb.Mii{
Name: string(miiData.Mii.Name),
ProfanityFlag: bool(miiData.Mii.ProfanityFlag),
CharacterSet: uint32(miiData.Mii.CharacterSet),
MiiDataEncrypted: miiData.Mii.MiiData,
}
mii_data, err := utility.DecryptMiiData(miiData.Mii.MiiData)
if err == nil {
mii.MiiData = mii_data
}
friendMii := &pb.FriendMii{
Pid: uint32(miiData.PID),
Mii: mii,
ModifiedAt: timestamppb.New(time.Unix(int64(miiData.ModifiedAt.Second()), 0)),
}
presence := &pb.NintendoPresence{}
connectedUser, ok := globals.ConnectedUsers.Get(uint32(user.PID))
if ok && connectedUser != nil {
presence.ChangedFlags = uint32(connectedUser.Presence.ChangedFlags)
presence.GameKey = &pb.GameKey{
TitleId: uint64(connectedUser.Presence.GameKey.TitleID),
TitleVersion: uint32(connectedUser.Presence.GameKey.TitleVersion),
}
presence.Message = string(connectedUser.Presence.Message)
presence.JoinAvailableFlag = uint32(connectedUser.Presence.JoinAvailableFlag)
presence.MatchmakeType = uint32(connectedUser.Presence.MatchmakeType)
presence.JoinGameId = uint32(connectedUser.Presence.JoinGameID)
presence.JoinGameMode = uint32(connectedUser.Presence.JoinGameMode)
presence.OwnerPid = uint32(connectedUser.Presence.OwnerPID)
presence.JoinGroupId = uint32(connectedUser.Presence.JoinGroupID)
presence.ApplicationArg = connectedUser.Presence.ApplicationArg
}
info := &pb.FriendInfo3DS{
Pid: uint32(user.PID),
Region: uint32(user.Region),
Country: uint32(user.Country),
Area: uint32(user.Area),
Language: uint32(user.Language),
Platform: uint32(user.Platform),
Presence: presence,
GameKey: gameKey,
Message: string(user.Message),
MessageUpdatedAt: timestamppb.New(user.MessageUpdatedAt.Standard()),
MiiModifiedAt: timestamppb.New(user.MiiModifiedAt.Standard()),
LastOnline: timestamppb.New(user.LastOnline.Standard()),
Mii: friendMii,
}
return &pb.GetUserData3DSResponse{
User: info,
}, nil
}

View File

@@ -0,0 +1,75 @@
package grpc
import (
"context"
"time"
"github.com/PretendoNetwork/friends/database"
database_wiiu "github.com/PretendoNetwork/friends/database/wiiu"
"github.com/PretendoNetwork/friends/globals"
pb "github.com/PretendoNetwork/grpc/go/friends/v2"
"github.com/PretendoNetwork/nex-go/v2/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *gRPCFriendsV2Server) GetUserDataWiiU(ctx context.Context, in *pb.GetUserDataWiiURequest) (*pb.GetUserDataWiiUResponse, error) {
user, err := database_wiiu.GetUserData(types.PID(in.GetPid()))
if err != nil {
if err == database.ErrPIDNotFound {
return nil, status.Errorf(codes.NotFound, "PID was not found")
} else {
globals.Logger.Critical(err.Error())
return nil, status.Errorf(codes.Internal, "Internal error")
}
}
comment := &pb.Comment{
Contents: string(user.Status.Contents),
LastChanged: timestamppb.New(time.Unix(int64(user.Status.LastChanged.Second()), 0)),
}
mii := &pb.MiiV2{
Name: string(user.NNAInfo.PrincipalBasicInfo.Mii.Name),
MiiData: user.NNAInfo.PrincipalBasicInfo.Mii.MiiData,
Datetime: timestamppb.New(time.Unix(int64(user.NNAInfo.PrincipalBasicInfo.Mii.Datetime.Second()), 0)),
}
principal := &pb.PrincipalBasicInfo{
Pid: uint32(user.NNAInfo.PrincipalBasicInfo.PID),
Nnid: string(user.NNAInfo.PrincipalBasicInfo.NNID),
Mii: mii,
}
nnaInfo := &pb.NNAInfo{
PrincipalBasicInfo: principal,
}
gameKey := &pb.GameKey{
TitleId: uint64(user.Presence.GameKey.TitleID),
TitleVersion: uint32(user.Presence.GameKey.TitleVersion),
}
presence := &pb.NintendoPresenceV2{
ChangedFlags: uint32(user.Presence.ChangedFlags),
Online: bool(user.Presence.Online),
GameKey: gameKey,
Message: string(user.Presence.Message),
GameServerId: uint32(user.Presence.GameServerID),
Pid: uint32(user.Presence.PID),
GatheringId: uint32(user.Presence.GatheringID),
ApplicationData: user.Presence.ApplicationData,
}
info := &pb.FriendInfoWiiU{
NnaInfo: nnaInfo,
Presence: presence,
Status: comment,
BecameFriend: timestamppb.New(user.BecameFriend.Standard()),
LastOnline: timestamppb.New(user.LastOnline.Standard()),
}
return &pb.GetUserDataWiiUResponse{
User: info,
}, nil
}

View File

@@ -0,0 +1,174 @@
package grpc
import (
"context"
"database/sql"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/PretendoNetwork/friends/database"
database_3ds "github.com/PretendoNetwork/friends/database/3ds"
"github.com/PretendoNetwork/friends/globals"
"github.com/PretendoNetwork/friends/utility"
pb "github.com/PretendoNetwork/grpc/go/friends/v2"
"github.com/PretendoNetwork/nex-go/v2/types"
friends_3ds_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-3ds/types"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb.GetUserFriendsData3DSRequest) (*pb.GetUserFriendsData3DSResponse, error) {
var friends []*pb.FriendInfo3DS
friendList, err := database_3ds.GetUserFriends(in.Pid)
if err != nil && err != database.ErrEmptyList {
globals.Logger.Critical(err.Error())
return &pb.GetUserFriendsData3DSResponse{
Friends: friends,
}, status.Errorf(codes.Internal, "internal server error")
}
friendPIDs := make([]uint32, len(friendList))
for _, friend := range friendList {
friendPIDs = append(friendPIDs, uint32(friend.PID))
}
friendInfoList, err := database_3ds.GetFriendPersistentInfos(uint32(in.Pid), friendPIDs)
if err != nil && err != sql.ErrNoRows {
globals.Logger.Critical(err.Error())
return &pb.GetUserFriendsData3DSResponse{
Friends: friends,
}, status.Errorf(codes.Internal, "internal server error")
}
miiList, err := database_3ds.GetFriendMiis(friendPIDs)
if err != nil && err != sql.ErrNoRows {
globals.Logger.Critical(err.Error())
return &pb.GetUserFriendsData3DSResponse{
Friends: friends,
}, status.Errorf(codes.Internal, "internal server error")
}
if globals.Config.EnableBella {
bella := friends_3ds_types.NewFriendPersistentInfo()
bella.PID = types.NewPID(1743126339)
bella.Region = types.NewUInt8(0)
bella.Country = types.NewUInt8(0)
bella.Area = types.NewUInt8(0)
bella.Language = types.NewUInt8(0)
bella.Platform = types.NewUInt8(0)
bella.GameKey.TitleID = 0x0005000010176900
bella.GameKey.TitleVersion = types.NewUInt16(0)
bella.Message = "Howdy Hey!"
bella.MessageUpdatedAt = types.NewDateTime(0)
bella.MiiModifiedAt = types.NewDateTime(0)
bella.LastOnline = types.NewDateTime(0)
mii := friends_3ds_types.NewMii()
mii.Name = types.NewString("Bandwidth")
mii.ProfanityFlag = types.NewBool(false)
mii.CharacterSet = types.NewUInt8(0)
mii.MiiData = types.NewBuffer([]byte{
0x03, 0x00, 0x00, 0x40, 0xE9, 0x55, 0xA2, 0x09,
0xE7, 0xC7, 0x41, 0x82, 0xD9, 0x7D, 0x0B, 0x2D,
0x03, 0xB3, 0xB8, 0x8D, 0x27, 0xD9, 0x00, 0x00,
0x01, 0x40, 0x62, 0x00, 0x65, 0x00, 0x6C, 0x00,
0x6C, 0x00, 0x61, 0x00, 0x00, 0x00, 0x45, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40,
0x12, 0x00, 0x81, 0x01, 0x04, 0x68, 0x43, 0x18,
0x20, 0x34, 0x46, 0x14, 0x81, 0x12, 0x17, 0x68,
0x0D, 0x00, 0x00, 0x29, 0x03, 0x52, 0x48, 0x50,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x86,
})
friendMii := friends_3ds_types.NewFriendMii()
friendMii.PID = types.NewPID(uint64(bella.PID))
friendMii.Mii = mii
friendMii.ModifiedAt = types.NewDateTime(0)
friendInfoList = append(friendInfoList, bella)
miiList = append(miiList, friendMii)
}
for _, friend := range friendInfoList {
gameKey := &pb.GameKey{
TitleId: uint64(friend.GameKey.TitleID),
TitleVersion: uint32(friend.GameKey.TitleVersion),
}
miiIndex := -1
for index, mii := range miiList {
if mii.PID == friend.PID {
miiIndex = index
break
}
}
if miiIndex == -1 {
continue
}
miiData := miiList[miiIndex]
mii := &pb.Mii{
Name: string(miiData.Mii.Name),
ProfanityFlag: bool(miiData.Mii.ProfanityFlag),
CharacterSet: uint32(miiData.Mii.CharacterSet),
MiiDataEncrypted: miiData.Mii.MiiData,
}
mii_data, err := utility.DecryptMiiData(miiData.Mii.MiiData)
if err == nil {
mii.MiiData = mii_data
}
friendMii := &pb.FriendMii{
Pid: uint32(miiData.PID),
Mii: mii,
ModifiedAt: timestamppb.New(time.Unix(int64(miiData.ModifiedAt.Second()), 0)),
}
presence := &pb.NintendoPresence{}
connectedUser, ok := globals.ConnectedUsers.Get(uint32(friend.PID))
if ok && connectedUser != nil {
presence.ChangedFlags = uint32(connectedUser.Presence.ChangedFlags)
presence.GameKey = &pb.GameKey{
TitleId: uint64(connectedUser.Presence.GameKey.TitleID),
TitleVersion: uint32(connectedUser.Presence.GameKey.TitleVersion),
}
presence.Message = string(connectedUser.Presence.Message)
presence.JoinAvailableFlag = uint32(connectedUser.Presence.JoinAvailableFlag)
presence.MatchmakeType = uint32(connectedUser.Presence.MatchmakeType)
presence.JoinGameId = uint32(connectedUser.Presence.JoinGameID)
presence.JoinGameMode = uint32(connectedUser.Presence.JoinGameMode)
presence.OwnerPid = uint32(connectedUser.Presence.OwnerPID)
presence.JoinGroupId = uint32(connectedUser.Presence.JoinGroupID)
presence.ApplicationArg = connectedUser.Presence.ApplicationArg
}
info := &pb.FriendInfo3DS{
Pid: uint32(friend.PID),
Region: uint32(friend.Region),
Country: uint32(friend.Country),
Area: uint32(friend.Area),
Language: uint32(friend.Language),
Platform: uint32(friend.Platform),
Presence: presence,
GameKey: gameKey,
Message: string(friend.Message),
MessageUpdatedAt: timestamppb.New(friend.MessageUpdatedAt.Standard()),
MiiModifiedAt: timestamppb.New(friend.MiiModifiedAt.Standard()),
LastOnline: timestamppb.New(friend.LastOnline.Standard()),
Mii: friendMii,
}
friends = append(friends, info)
}
return &pb.GetUserFriendsData3DSResponse{
Friends: friends,
}, nil
}

View File

@@ -0,0 +1,145 @@
package grpc
import (
"context"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/PretendoNetwork/friends/database"
database_wiiu "github.com/PretendoNetwork/friends/database/wiiu"
"github.com/PretendoNetwork/friends/globals"
pb "github.com/PretendoNetwork/grpc/go/friends/v2"
"github.com/PretendoNetwork/nex-go/v2/types"
friends_wiiu_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-wiiu/types"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb.GetUserFriendsDataWiiURequest) (*pb.GetUserFriendsDataWiiUResponse, error) {
var friends []*pb.FriendInfoWiiU
friendList, err := database_wiiu.GetUserFriendList(in.Pid)
if err != nil && err != database.ErrEmptyList {
globals.Logger.Critical(err.Error())
return &pb.GetUserFriendsDataWiiUResponse{
Friends: friends,
}, status.Errorf(codes.Internal, "internal server error")
}
if globals.Config.EnableBella {
bella := friends_wiiu_types.NewFriendInfo()
bella.NNAInfo = friends_wiiu_types.NewNNAInfo()
bella.Presence = friends_wiiu_types.NewNintendoPresenceV2()
bella.Status = friends_wiiu_types.NewComment()
bella.BecameFriend = types.NewDateTime(0)
bella.LastOnline = types.NewDateTime(0)
bella.Unknown = types.NewUInt64(0)
bella.NNAInfo.PrincipalBasicInfo = friends_wiiu_types.NewPrincipalBasicInfo()
bella.NNAInfo.Unknown1 = types.NewUInt8(0)
bella.NNAInfo.Unknown2 = types.NewUInt8(0)
bella.NNAInfo.PrincipalBasicInfo.PID = types.NewPID(1743126339)
bella.NNAInfo.PrincipalBasicInfo.NNID = types.NewString("PN_Testing")
bella.NNAInfo.PrincipalBasicInfo.Mii = friends_wiiu_types.NewMiiV2()
bella.NNAInfo.PrincipalBasicInfo.Unknown = types.NewUInt8(0)
bella.NNAInfo.PrincipalBasicInfo.Mii.Name = types.NewString("Bandwidth")
bella.NNAInfo.PrincipalBasicInfo.Mii.Unknown1 = types.NewUInt8(0)
bella.NNAInfo.PrincipalBasicInfo.Mii.Unknown2 = types.NewUInt8(0)
bella.NNAInfo.PrincipalBasicInfo.Mii.MiiData = types.NewBuffer([]byte{
0x03, 0x00, 0x00, 0x40, 0xE9, 0x55, 0xA2, 0x09,
0xE7, 0xC7, 0x41, 0x82, 0xD9, 0x7D, 0x0B, 0x2D,
0x03, 0xB3, 0xB8, 0x8D, 0x27, 0xD9, 0x00, 0x00,
0x01, 0x40, 0x62, 0x00, 0x65, 0x00, 0x6C, 0x00,
0x6C, 0x00, 0x61, 0x00, 0x00, 0x00, 0x45, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40,
0x12, 0x00, 0x81, 0x01, 0x04, 0x68, 0x43, 0x18,
0x20, 0x34, 0x46, 0x14, 0x81, 0x12, 0x17, 0x68,
0x0D, 0x00, 0x00, 0x29, 0x03, 0x52, 0x48, 0x50,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x86,
})
bella.NNAInfo.PrincipalBasicInfo.Mii.Datetime = types.NewDateTime(0)
bella.Presence.ChangedFlags = types.NewUInt32(0x1EE)
bella.Presence.Online = types.NewBool(true)
bella.Presence.GameKey = friends_wiiu_types.NewGameKey()
bella.Presence.Unknown1 = types.NewUInt8(0)
bella.Presence.Message = types.NewString("Testing")
bella.Presence.Unknown2 = types.NewUInt32(0)
bella.Presence.Unknown3 = types.NewUInt8(0)
bella.Presence.GameServerID = types.NewUInt32(0)
bella.Presence.Unknown4 = types.NewUInt32(0)
bella.Presence.PID = types.NewPID(1743126339)
bella.Presence.GatheringID = types.NewUInt32(0)
bella.Presence.ApplicationData = types.NewBuffer([]byte{0x0})
bella.Presence.Unknown5 = types.NewUInt8(0)
bella.Presence.Unknown6 = types.NewUInt8(0)
bella.Presence.Unknown7 = types.NewUInt8(0)
bella.Presence.GameKey.TitleID = 0x0005000010176900
bella.Presence.GameKey.TitleVersion = types.NewUInt16(0)
bella.Status.Unknown = types.NewUInt8(0)
bella.Status.Contents = types.NewString("Greetings programs!")
bella.Status.LastChanged = types.NewDateTime(0)
friendList = append(friendList, bella)
}
for _, friend := range friendList {
comment := &pb.Comment{
Contents: string(friend.Status.Contents),
LastChanged: timestamppb.New(time.Unix(int64(friend.Status.LastChanged.Second()), 0)),
}
mii := &pb.MiiV2{
Name: string(friend.NNAInfo.PrincipalBasicInfo.Mii.Name),
MiiData: friend.NNAInfo.PrincipalBasicInfo.Mii.MiiData,
Datetime: timestamppb.New(time.Unix(int64(friend.NNAInfo.PrincipalBasicInfo.Mii.Datetime.Second()), 0)),
}
principal := &pb.PrincipalBasicInfo{
Pid: uint32(friend.NNAInfo.PrincipalBasicInfo.PID),
Nnid: string(friend.NNAInfo.PrincipalBasicInfo.NNID),
Mii: mii,
}
nnaInfo := &pb.NNAInfo{
PrincipalBasicInfo: principal,
}
gameKey := &pb.GameKey{
TitleId: uint64(friend.Presence.GameKey.TitleID),
TitleVersion: uint32(friend.Presence.GameKey.TitleVersion),
}
presence := &pb.NintendoPresenceV2{
ChangedFlags: uint32(friend.Presence.ChangedFlags),
Online: bool(friend.Presence.Online),
GameKey: gameKey,
Message: string(friend.Presence.Message),
GameServerId: uint32(friend.Presence.GameServerID),
Pid: uint32(friend.Presence.PID),
GatheringId: uint32(friend.Presence.GatheringID),
ApplicationData: friend.Presence.ApplicationData,
}
info := &pb.FriendInfoWiiU{
NnaInfo: nnaInfo,
Presence: presence,
Status: comment,
BecameFriend: timestamppb.New(friend.BecameFriend.Standard()),
LastOnline: timestamppb.New(friend.LastOnline.Standard()),
}
friends = append(friends, info)
}
return &pb.GetUserFriendsDataWiiUResponse{
Friends: friends,
}, nil
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/PretendoNetwork/friends/globals"
pb "github.com/PretendoNetwork/grpc/go/friends"
pbv2 "github.com/PretendoNetwork/grpc/go/friends/v2"
"google.golang.org/grpc"
)
@@ -14,6 +15,10 @@ type gRPCFriendsServer struct {
pb.UnimplementedFriendsServer
}
type gRPCFriendsV2Server struct {
pbv2.UnimplementedFriendsServiceServer
}
func StartGRPCServer() {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", globals.Config.GRPCServerPort))
if err != nil {
@@ -25,6 +30,7 @@ func StartGRPCServer() {
)
pb.RegisterFriendsServer(server, &gRPCFriendsServer{})
pbv2.RegisterFriendsServiceServer(server, &gRPCFriendsV2Server{})
log.Printf("server listening at %v", listener.Addr())

17
init.go
View File

@@ -1,6 +1,7 @@
package main
import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
"fmt"
@@ -71,5 +72,21 @@ func init() {
"X-API-Key", globals.Config.AccountGRPCAPIKey,
)
if strings.TrimSpace(globals.Config.MiiDecryptKey) == "" {
globals.Logger.Warning("PN_FRIENDS_CONFIG_MII_DECRYPT_KEY environment variable not set. 3DS Mii data cannot be decrypted")
}
miiKeyBytes, err := hex.DecodeString(globals.Config.MiiDecryptKey)
if err != nil {
globals.Logger.Criticalf("Failed to decode PN_FRIENDS_CONFIG_MII_DECRYPT_KEY %v", err)
os.Exit(0)
}
miiMD5Hash := md5.Sum(miiKeyBytes)
if hex.EncodeToString(miiMD5Hash[:]) != "aeb707b225ec0fcd8a503e26e3dcd596" {
globals.Logger.Criticalf("PN_FRIENDS_CONFIG_MII_DECRYPT_KEY is incorrect! md5: %s", hex.EncodeToString(miiMD5Hash[:]))
os.Exit(0)
}
database.ConnectPostgres()
}

57
utility/decrypt_mii.go Normal file
View File

@@ -0,0 +1,57 @@
package utility
import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"fmt"
"github.com/PretendoNetwork/friends/globals"
"github.com/PretendoNetwork/nex-go/v2/types"
)
func ccmDecrypt(key, nonce, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
L := 15 - len(nonce)
counter := make([]byte, aes.BlockSize)
counter[0] = byte(L - 1)
copy(counter[1:1+len(nonce)], nonce)
stream := cipher.NewCTR(block, counter)
padded := make([]byte, aes.BlockSize+len(ciphertext))
copy(padded[aes.BlockSize:], ciphertext)
out := make([]byte, len(padded))
stream.XORKeyStream(out, padded)
return out[aes.BlockSize:], nil
}
func DecryptMiiData(miiData types.Buffer) ([]byte, error) {
if len(miiData) < 0x70 {
return nil, fmt.Errorf("Mii data length is incorrect: %d", len(miiData))
}
nonce := miiData[:8]
ciphertext := miiData[8 : 8+0x58]
key, err := hex.DecodeString(globals.Config.MiiDecryptKey)
if err != nil {
return nil, err
}
content, err := ccmDecrypt(key, append(append([]byte{}, nonce...), 0, 0, 0, 0), ciphertext)
if err != nil {
return nil, err
}
result := make([]byte, 0, len(content)+len(nonce))
result = append(result, content[:12]...)
result = append(result, nonce...)
result = append(result, content[12:]...)
return result, nil
}