From eb6bb06c7256f170bbfd21f112318307fa34c014 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Sat, 25 Apr 2026 22:31:06 -0500 Subject: [PATCH 01/18] feat(grpc): implement fetch user data for WIi U & 3DS --- grpc/get_user_friends_data.go | 255 ++++++++++++++++++++++++++++++++++ grpc/grpc_server.go | 6 + 2 files changed, 261 insertions(+) create mode 100644 grpc/get_user_friends_data.go diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data.go new file mode 100644 index 0000000..ad219f9 --- /dev/null +++ b/grpc/get_user_friends_data.go @@ -0,0 +1,255 @@ +package grpc + +import ( + "context" + "database/sql" + + "github.com/PretendoNetwork/friends/database" + database_3ds "github.com/PretendoNetwork/friends/database/3ds" + 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_3ds_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-3ds/types" + friends_wiiu_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-wiiu/types" +) + +func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb.GetUserFriendsDataRequest) (*pb.GetUserFriendsDataWiiUResponse, error) { + var friends []*pb.FriendInfoWiiU + friendList, err := database_wiiu.GetUserFriendList(in.Pid) + if err != nil && err != database.ErrEmptyList { + return &pb.GetUserFriendsDataWiiUResponse{ + Friends: friends, + }, nil + } + + 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("Howdy hey!") + bella.Status.LastChanged = types.NewDateTime(0) + + friendList = append(friendList, bella) + } + + for _, friend := range friendList { + // TODO: Is there a better way to do this? I really don't know what I'm doing here + var comment = &pb.Comment{ + Contents: string(friend.Status.Contents), + LastChanged: uint64(friend.Status.LastChanged), + } + var mii = &pb.MiiV2{ + Name: string(friend.NNAInfo.PrincipalBasicInfo.Mii.Name), + MiiData: friend.NNAInfo.PrincipalBasicInfo.Mii.MiiData, + Datetime: uint64(friend.NNAInfo.PrincipalBasicInfo.Mii.Datetime), + } + var principal = &pb.PrincipalBasicInfo{ + Pid: uint32(friend.NNAInfo.PrincipalBasicInfo.PID), + Nnid: string(friend.NNAInfo.PrincipalBasicInfo.NNID), + Mii: mii, + } + var nnaInfo = &pb.NNAInfo{ + PrincipalBasicInfo: principal, + } + var gameKey = &pb.GameKey{ + TitleId: uint64(friend.Presence.GameKey.TitleID), + TitleVersion: uint32(friend.Presence.GameKey.TitleVersion), + } + var 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, + } + var info = &pb.FriendInfoWiiU{ + NnaInfo: nnaInfo, + Presence: presence, + Status: comment, + BecameFriend: uint64(friend.BecameFriend), + LastOnline: uint64(friend.LastOnline), + } + friends = append(friends, info) + } + + return &pb.GetUserFriendsDataWiiUResponse{ + Friends: friends, + }, nil +} + +func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb.GetUserFriendsDataRequest) (*pb.GetUserFriendsData3DSResponse, error) { + var friends []*pb.FriendInfo3DS + friendList, err := database_3ds.GetUserFriends(in.Pid) + if err != nil && err != database.ErrEmptyList { + return &pb.GetUserFriendsData3DSResponse{ + Friends: friends, + }, nil + } + + 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, + }, nil + } + + miiList, err := database_3ds.GetFriendMiis(friendPIDs) + if err != nil && err != sql.ErrNoRows { + globals.Logger.Critical(err.Error()) + return &pb.GetUserFriendsData3DSResponse{ + Friends: friends, + }, nil + } + + 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 { + // TODO: Is there a better way to do this? I really don't know what I'm doing here + var gameKey = &pb.GameKey{ + TitleId: uint64(friend.GameKey.TitleID), + TitleVersion: uint32(friend.GameKey.TitleVersion), + } + var miiIndex = -1 + for index, mii := range miiList { + if mii.PID == friend.PID { + miiIndex = index + break + } + } + if miiIndex == -1 { + continue + } + var miiData = miiList[miiIndex] + var mii = &pb.MiiV2{ + Name: string(miiData.Mii.Name), + MiiData: miiData.Mii.MiiData, + Datetime: uint64(miiData.ModifiedAt), + } + var 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), + GameKey: gameKey, + Message: string(friend.Message), + MessageUpdatedAt: uint64(friend.MessageUpdatedAt), + MiiModifiedAt: uint64(friend.MiiModifiedAt), + LastOnline: uint64(friend.LastOnline), + Mii: mii, + } + friends = append(friends, info) + } + + return &pb.GetUserFriendsData3DSResponse{ + Friends: friends, + }, nil +} diff --git a/grpc/grpc_server.go b/grpc/grpc_server.go index 6a97e77..7ceb99e 100644 --- a/grpc/grpc_server.go +++ b/grpc/grpc_server.go @@ -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()) From 98c707df115cc5c1b23b75da354ecec10f72f9b6 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Sat, 25 Apr 2026 22:40:14 -0500 Subject: [PATCH 02/18] fix(grpc): split out rpc requests --- grpc/get_user_friends_data.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data.go index ad219f9..b5c8238 100644 --- a/grpc/get_user_friends_data.go +++ b/grpc/get_user_friends_data.go @@ -14,7 +14,7 @@ import ( friends_wiiu_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-wiiu/types" ) -func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb.GetUserFriendsDataRequest) (*pb.GetUserFriendsDataWiiUResponse, error) { +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 { @@ -135,7 +135,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb }, nil } -func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb.GetUserFriendsDataRequest) (*pb.GetUserFriendsData3DSResponse, error) { +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 { From 9a6edb48ab4542a2d35c20baa8785e6c2085653f Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Sun, 26 Apr 2026 12:12:57 -0500 Subject: [PATCH 03/18] fix(grpc): switch to timestamps per updated proto --- grpc/get_user_friends_data.go | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data.go index b5c8238..bcce127 100644 --- a/grpc/get_user_friends_data.go +++ b/grpc/get_user_friends_data.go @@ -3,6 +3,7 @@ package grpc import ( "context" "database/sql" + "time" "github.com/PretendoNetwork/friends/database" database_3ds "github.com/PretendoNetwork/friends/database/3ds" @@ -12,6 +13,7 @@ import ( "github.com/PretendoNetwork/nex-go/v2/types" friends_3ds_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-3ds/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) { @@ -91,12 +93,12 @@ func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb // TODO: Is there a better way to do this? I really don't know what I'm doing here var comment = &pb.Comment{ Contents: string(friend.Status.Contents), - LastChanged: uint64(friend.Status.LastChanged), + LastChanged: timestamppb.New(time.Unix(int64(friend.Status.LastChanged.Second()), 0)), } var mii = &pb.MiiV2{ Name: string(friend.NNAInfo.PrincipalBasicInfo.Mii.Name), MiiData: friend.NNAInfo.PrincipalBasicInfo.Mii.MiiData, - Datetime: uint64(friend.NNAInfo.PrincipalBasicInfo.Mii.Datetime), + Datetime: timestamppb.New(time.Unix(int64(friend.NNAInfo.PrincipalBasicInfo.Mii.Datetime.Second()), 0)), } var principal = &pb.PrincipalBasicInfo{ Pid: uint32(friend.NNAInfo.PrincipalBasicInfo.PID), @@ -124,8 +126,8 @@ func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb NnaInfo: nnaInfo, Presence: presence, Status: comment, - BecameFriend: uint64(friend.BecameFriend), - LastOnline: uint64(friend.LastOnline), + BecameFriend: timestamppb.New(time.Unix(int64(friend.BecameFriend.Second()), 0)), + LastOnline: timestamppb.New(time.Unix(int64(friend.LastOnline.Second()), 0)), } friends = append(friends, info) } @@ -227,11 +229,19 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. continue } var miiData = miiList[miiIndex] - var mii = &pb.MiiV2{ - Name: string(miiData.Mii.Name), - MiiData: miiData.Mii.MiiData, - Datetime: uint64(miiData.ModifiedAt), + var mii = &pb.Mii{ + Name: string(miiData.Mii.Name), + ProfanityFlag: bool(miiData.Mii.ProfanityFlag), + CharacterSet: uint32(miiData.Mii.CharacterSet), + MiiData: miiData.Mii.MiiData, } + var friendMii = &pb.FriendMii{ + Pid: uint32(miiData.PID), + Mii: mii, + ModifiedAt: timestamppb.New(time.Unix(int64(miiData.ModifiedAt.Second()), 0)), + } + var presence = &pb.NintendoPresence{} + var info = &pb.FriendInfo3DS{ Pid: uint32(friend.PID), Region: uint32(friend.Region), @@ -239,12 +249,13 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. Area: uint32(friend.Area), Language: uint32(friend.Language), Platform: uint32(friend.Platform), + Presence: presence, GameKey: gameKey, Message: string(friend.Message), - MessageUpdatedAt: uint64(friend.MessageUpdatedAt), - MiiModifiedAt: uint64(friend.MiiModifiedAt), - LastOnline: uint64(friend.LastOnline), - Mii: mii, + MessageUpdatedAt: timestamppb.New(time.Unix(int64(friend.MessageUpdatedAt.Second()), 0)), + MiiModifiedAt: timestamppb.New(time.Unix(int64(friend.MiiModifiedAt.Second()), 0)), + LastOnline: timestamppb.New(time.Unix(int64(friend.LastOnline.Second()), 0)), + Mii: friendMii, } friends = append(friends, info) } From 87142fc9d7f2c2a19d82559b28df06e582ab5809 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Sun, 3 May 2026 20:13:56 -0500 Subject: [PATCH 04/18] chore: bump grpc version --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7c3d759..aaff955 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 7c791f0..026df5f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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= From f34d3ea2121f38c93130a4ae8e56f8c085adfdc4 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Sun, 3 May 2026 21:05:35 -0500 Subject: [PATCH 05/18] feat(grpc): add presence for 3DS --- grpc/get_user_friends_data.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data.go index bcce127..e01aad9 100644 --- a/grpc/get_user_friends_data.go +++ b/grpc/get_user_friends_data.go @@ -83,7 +83,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb bella.Presence.GameKey.TitleVersion = types.NewUInt16(0) bella.Status.Unknown = types.NewUInt8(0) - bella.Status.Contents = types.NewString("Howdy hey!") + bella.Status.Contents = types.NewString("Greetings programs!") bella.Status.LastChanged = types.NewDateTime(0) friendList = append(friendList, bella) @@ -241,6 +241,22 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. ModifiedAt: timestamppb.New(time.Unix(int64(miiData.ModifiedAt.Second()), 0)), } var 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 + } var info = &pb.FriendInfo3DS{ Pid: uint32(friend.PID), From dc02700dc28ee7e1034eac924c2a0e437447f656 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Tue, 5 May 2026 21:09:33 -0500 Subject: [PATCH 06/18] feat(grpc): implement getUserData rpcs --- database/3ds/get_mii.go | 47 +++++++++++ database/3ds/get_user_data.go | 83 +++++++++++++++++++ database/wiiu/get_user_data.go | 98 ++++++++++++++++++++++ grpc/get_user_data.go | 145 +++++++++++++++++++++++++++++++++ 4 files changed, 373 insertions(+) create mode 100644 database/3ds/get_mii.go create mode 100644 database/3ds/get_user_data.go create mode 100644 database/wiiu/get_user_data.go create mode 100644 grpc/get_user_data.go diff --git a/database/3ds/get_mii.go b/database/3ds/get_mii.go new file mode 100644 index 0000000..595e8cc --- /dev/null +++ b/database/3ds/get_mii.go @@ -0,0 +1,47 @@ +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 uint32) (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 + } + } + + mii := friends_3ds_types.NewMii() + mii.Name = types.NewString(miiName) + mii.ProfanityFlag = types.NewBool(miiProfanity) + mii.CharacterSet = types.NewUInt8(miiCharacterSet) + mii.MiiData = types.NewBuffer(miiData) + + friendMii.PID = types.NewPID(uint64(pid)) + friendMii.Mii = mii + friendMii.ModifiedAt = types.NewDateTime(changedTime) + + return friendMii, nil +} diff --git a/database/3ds/get_user_data.go b/database/3ds/get_user_data.go new file mode 100644 index 0000000..a04540b --- /dev/null +++ b/database/3ds/get_user_data.go @@ -0,0 +1,83 @@ +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 uint32) (friends_3ds_types.FriendPersistentInfo, error) { + userData := 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 userData, database.ErrPIDNotFound + } else { + return userData, err + } + } + + var relationshipType uint8 + + err = row.Scan(&pid, &relationshipType) + if err != nil { + return userData, err + } + + gameKey := friends_3ds_types.NewGameKey() + + 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, + ®ion, + &area, + &language, + &country, + &titleID, + &titleVersion, + &message, + &msgUpdateTime, + &lastOnlineTime, + &miiModifiedAtTime, + ) + if err != nil { + return userData, err + } + + gameKey.TitleID = types.NewUInt64(titleID) + gameKey.TitleVersion = types.NewUInt16(titleVersion) + + userData.PID = types.NewPID(uint64(pid)) + userData.Region = types.NewUInt8(region) + userData.Country = types.NewUInt8(country) + userData.Area = types.NewUInt8(area) + userData.Language = types.NewUInt8(language) + userData.Platform = types.NewUInt8(2) // * Always 3DS + userData.GameKey = gameKey + userData.Message = types.NewString(message) + userData.MessageUpdatedAt = types.NewDateTime(msgUpdateTime) + userData.MiiModifiedAt = types.NewDateTime(miiModifiedAtTime) + userData.LastOnline = types.NewDateTime(lastOnlineTime) + + return userData, nil +} diff --git a/database/wiiu/get_user_data.go b/database/wiiu/get_user_data.go new file mode 100644 index 0000000..f62346b --- /dev/null +++ b/database/wiiu/get_user_data.go @@ -0,0 +1,98 @@ +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 uint32) (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 + } + + comment := friends_wiiu_types.NewComment() + comment.Unknown = types.NewUInt8(0) + comment.Contents = types.NewString(commentContents) + comment.LastChanged = types.NewDateTime(commentChanged) + + mii := friends_wiiu_types.NewMiiV2() + mii.Name = types.NewString(miiName) + mii.Unknown1 = types.NewUInt8(miiUnknown1) + mii.Unknown2 = types.NewUInt8(miiUnknown2) + mii.MiiData = types.NewBuffer(miiData) + mii.Datetime = types.NewDateTime(miiDatetime) + + principalBasicInfo := friends_wiiu_types.NewPrincipalBasicInfo() + principalBasicInfo.PID = types.NewPID(uint64(pid)) + principalBasicInfo.NNID = types.NewString(nnid) + principalBasicInfo.Unknown = types.NewUInt8(unknown) + principalBasicInfo.Mii = mii + + nnaInfo := friends_wiiu_types.NewNNAInfo() + nnaInfo.Unknown1 = types.NewUInt8(unknown1) + nnaInfo.Unknown2 = types.NewUInt8(unknown2) + nnaInfo.PrincipalBasicInfo = principalBasicInfo + + friendInfo.NNAInfo = nnaInfo + + lastOnline := types.NewDateTime(0).Now() + connectedUser, ok := globals.ConnectedUsers.Get(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 = comment + friendInfo.BecameFriend = types.NewDateTime(date) + friendInfo.LastOnline = lastOnline + friendInfo.Unknown = types.NewUInt64(0) + + return friendInfo, nil +} diff --git a/grpc/get_user_data.go b/grpc/get_user_data.go new file mode 100644 index 0000000..b56a64e --- /dev/null +++ b/grpc/get_user_data.go @@ -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_3ds "github.com/PretendoNetwork/friends/database/3ds" + database_wiiu "github.com/PretendoNetwork/friends/database/wiiu" + "github.com/PretendoNetwork/friends/globals" + pb "github.com/PretendoNetwork/grpc/go/friends/v2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (s *gRPCFriendsV2Server) GetUserDataWiiU(ctx context.Context, in *pb.GetUserDataWiiURequest) (*pb.GetUserDataWiiUResponse, error) { + friend, err := database_wiiu.GetUserData(in.GetPid()) + if err != nil { + globals.Logger.Critical(err.Error()) + if err == database.ErrPIDNotFound { + return nil, status.Errorf(codes.NotFound, "PID was not found") + } else { + return nil, status.Errorf(codes.Internal, "Internal error") + } + } + var comment = &pb.Comment{ + Contents: string(friend.Status.Contents), + LastChanged: timestamppb.New(time.Unix(int64(friend.Status.LastChanged.Second()), 0)), + } + var 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)), + } + var principal = &pb.PrincipalBasicInfo{ + Pid: uint32(friend.NNAInfo.PrincipalBasicInfo.PID), + Nnid: string(friend.NNAInfo.PrincipalBasicInfo.NNID), + Mii: mii, + } + var nnaInfo = &pb.NNAInfo{ + PrincipalBasicInfo: principal, + } + var gameKey = &pb.GameKey{ + TitleId: uint64(friend.Presence.GameKey.TitleID), + TitleVersion: uint32(friend.Presence.GameKey.TitleVersion), + } + var 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, + } + var info = &pb.FriendInfoWiiU{ + NnaInfo: nnaInfo, + Presence: presence, + Status: comment, + BecameFriend: timestamppb.New(time.Unix(int64(friend.BecameFriend.Second()), 0)), + LastOnline: timestamppb.New(time.Unix(int64(friend.LastOnline.Second()), 0)), + } + return &pb.GetUserDataWiiUResponse{ + User: info, + }, nil +} + +func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUserData3DSRequest) (*pb.GetUserData3DSResponse, error) { + friend, err := database_3ds.GetUserData(in.GetPid()) + if err != nil { + globals.Logger.Critical(err.Error()) + if err == database.ErrPIDNotFound { + return nil, status.Errorf(codes.NotFound, "PID was not found") + } else { + return nil, status.Errorf(codes.Internal, "Internal error") + } + } + + miiData, err := database_3ds.GetMii(in.GetPid()) + if err != nil { + globals.Logger.Critical(err.Error()) + if err == database.ErrPIDNotFound { + return nil, status.Errorf(codes.NotFound, "PID was not found") + } else { + return nil, status.Errorf(codes.Internal, "Internal error") + } + } + + var gameKey = &pb.GameKey{ + TitleId: uint64(friend.GameKey.TitleID), + TitleVersion: uint32(friend.GameKey.TitleVersion), + } + + var mii = &pb.Mii{ + Name: string(miiData.Mii.Name), + ProfanityFlag: bool(miiData.Mii.ProfanityFlag), + CharacterSet: uint32(miiData.Mii.CharacterSet), + MiiData: miiData.Mii.MiiData, + } + var friendMii = &pb.FriendMii{ + Pid: uint32(miiData.PID), + Mii: mii, + ModifiedAt: timestamppb.New(time.Unix(int64(miiData.ModifiedAt.Second()), 0)), + } + + var 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 + } + + var 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(time.Unix(int64(friend.MessageUpdatedAt.Second()), 0)), + MiiModifiedAt: timestamppb.New(time.Unix(int64(friend.MiiModifiedAt.Second()), 0)), + LastOnline: timestamppb.New(time.Unix(int64(friend.LastOnline.Second()), 0)), + Mii: friendMii, + } + return &pb.GetUserData3DSResponse{ + User: info, + }, nil +} From a95ecba31afec158e27ea50e0b4e0a487d3896a2 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Tue, 5 May 2026 21:12:21 -0500 Subject: [PATCH 07/18] fix(grpc): fix mii data field assignment on getUserData3DS --- grpc/get_user_data.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/grpc/get_user_data.go b/grpc/get_user_data.go index b56a64e..575adfb 100644 --- a/grpc/get_user_data.go +++ b/grpc/get_user_data.go @@ -95,10 +95,11 @@ func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUser } var mii = &pb.Mii{ - Name: string(miiData.Mii.Name), - ProfanityFlag: bool(miiData.Mii.ProfanityFlag), - CharacterSet: uint32(miiData.Mii.CharacterSet), - MiiData: miiData.Mii.MiiData, + Name: string(miiData.Mii.Name), + ProfanityFlag: bool(miiData.Mii.ProfanityFlag), + CharacterSet: uint32(miiData.Mii.CharacterSet), + MiiDataEncrypted: miiData.Mii.MiiData, + // TODO: Implement MiiData } var friendMii = &pb.FriendMii{ Pid: uint32(miiData.PID), From 306399a5dfbe823ed79cca01e2ecc78d77373d3f Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Fri, 8 May 2026 22:11:10 -0500 Subject: [PATCH 08/18] fix(database): switch to PID type for input and clean up redundant struct declarations --- database/3ds/get_mii.go | 13 ++++------- database/3ds/get_user_data.go | 42 +++++++++++++++------------------- database/wiiu/get_user_data.go | 42 ++++++++++++---------------------- 3 files changed, 39 insertions(+), 58 deletions(-) diff --git a/database/3ds/get_mii.go b/database/3ds/get_mii.go index 595e8cc..c4adcec 100644 --- a/database/3ds/get_mii.go +++ b/database/3ds/get_mii.go @@ -9,7 +9,7 @@ import ( ) // GetMii returns the Mii of a specified user -func GetMii(pid uint32) (friends_3ds_types.FriendMii, error) { +func GetMii(pid types.PID) (friends_3ds_types.FriendMii, error) { friendMii := friends_3ds_types.NewFriendMii() rows, err := database.Manager.QueryRow(` @@ -33,14 +33,11 @@ func GetMii(pid uint32) (friends_3ds_types.FriendMii, error) { } } - mii := friends_3ds_types.NewMii() - mii.Name = types.NewString(miiName) - mii.ProfanityFlag = types.NewBool(miiProfanity) - mii.CharacterSet = types.NewUInt8(miiCharacterSet) - mii.MiiData = types.NewBuffer(miiData) - friendMii.PID = types.NewPID(uint64(pid)) - friendMii.Mii = mii + 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 diff --git a/database/3ds/get_user_data.go b/database/3ds/get_user_data.go index a04540b..0e252d8 100644 --- a/database/3ds/get_user_data.go +++ b/database/3ds/get_user_data.go @@ -9,8 +9,8 @@ import ( ) // GetUserData returns a data for a specific user -func GetUserData(pid uint32) (friends_3ds_types.FriendPersistentInfo, error) { - userData := friends_3ds_types.NewFriendPersistentInfo() +func GetUserData(pid types.PID) (friends_3ds_types.FriendPersistentInfo, error) { + friendPersistentInfo := friends_3ds_types.NewFriendPersistentInfo() row, err := database.Manager.QueryRow(` SELECT pid, region, area, @@ -21,9 +21,9 @@ func GetUserData(pid uint32) (friends_3ds_types.FriendPersistentInfo, error) { `, pid) if err != nil { if err == sql.ErrNoRows { - return userData, database.ErrPIDNotFound + return friendPersistentInfo, database.ErrPIDNotFound } else { - return userData, err + return friendPersistentInfo, err } } @@ -31,11 +31,9 @@ func GetUserData(pid uint32) (friends_3ds_types.FriendPersistentInfo, error) { err = row.Scan(&pid, &relationshipType) if err != nil { - return userData, err + return friendPersistentInfo, err } - gameKey := friends_3ds_types.NewGameKey() - var region uint8 var area uint8 var language uint8 @@ -61,23 +59,21 @@ func GetUserData(pid uint32) (friends_3ds_types.FriendPersistentInfo, error) { &miiModifiedAtTime, ) if err != nil { - return userData, err + return friendPersistentInfo, err } - gameKey.TitleID = types.NewUInt64(titleID) - gameKey.TitleVersion = types.NewUInt16(titleVersion) + 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) - userData.PID = types.NewPID(uint64(pid)) - userData.Region = types.NewUInt8(region) - userData.Country = types.NewUInt8(country) - userData.Area = types.NewUInt8(area) - userData.Language = types.NewUInt8(language) - userData.Platform = types.NewUInt8(2) // * Always 3DS - userData.GameKey = gameKey - userData.Message = types.NewString(message) - userData.MessageUpdatedAt = types.NewDateTime(msgUpdateTime) - userData.MiiModifiedAt = types.NewDateTime(miiModifiedAtTime) - userData.LastOnline = types.NewDateTime(lastOnlineTime) - - return userData, nil + return friendPersistentInfo, nil } diff --git a/database/wiiu/get_user_data.go b/database/wiiu/get_user_data.go index f62346b..5d80b76 100644 --- a/database/wiiu/get_user_data.go +++ b/database/wiiu/get_user_data.go @@ -10,7 +10,7 @@ import ( ) // GetUserData returns a data for a specific user -func GetUserData(pid uint32) (friends_wiiu_types.FriendInfo, error) { +func GetUserData(pid types.PID) (friends_wiiu_types.FriendInfo, error) { friendInfo := friends_wiiu_types.NewFriendInfo() row, err := database.Manager.QueryRow(` @@ -54,33 +54,19 @@ func GetUserData(pid uint32) (friends_wiiu_types.FriendInfo, error) { return friendInfo, err } - comment := friends_wiiu_types.NewComment() - comment.Unknown = types.NewUInt8(0) - comment.Contents = types.NewString(commentContents) - comment.LastChanged = types.NewDateTime(commentChanged) - - mii := friends_wiiu_types.NewMiiV2() - mii.Name = types.NewString(miiName) - mii.Unknown1 = types.NewUInt8(miiUnknown1) - mii.Unknown2 = types.NewUInt8(miiUnknown2) - mii.MiiData = types.NewBuffer(miiData) - mii.Datetime = types.NewDateTime(miiDatetime) - - principalBasicInfo := friends_wiiu_types.NewPrincipalBasicInfo() - principalBasicInfo.PID = types.NewPID(uint64(pid)) - principalBasicInfo.NNID = types.NewString(nnid) - principalBasicInfo.Unknown = types.NewUInt8(unknown) - principalBasicInfo.Mii = mii - - nnaInfo := friends_wiiu_types.NewNNAInfo() - nnaInfo.Unknown1 = types.NewUInt8(unknown1) - nnaInfo.Unknown2 = types.NewUInt8(unknown2) - nnaInfo.PrincipalBasicInfo = principalBasicInfo - - friendInfo.NNAInfo = nnaInfo + 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(pid) + connectedUser, ok := globals.ConnectedUsers.Get(uint32(pid)) if ok && connectedUser != nil { // * Online friendInfo.Presence = connectedUser.PresenceV2.Copy().(friends_wiiu_types.NintendoPresenceV2) @@ -89,7 +75,9 @@ func GetUserData(pid uint32) (friends_wiiu_types.FriendInfo, error) { lastOnline = types.NewDateTime(lastOnlineTime) // TODO - Change this } - friendInfo.Status = comment + 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) From 2ad590a71e6660f521b31442166d3b9cc01236f7 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Fri, 8 May 2026 22:20:06 -0500 Subject: [PATCH 09/18] fix(grpc): updated timestamps to be accurate, more cleanup --- grpc/get_user_data.go | 53 ++++++++++++++++++++--------------- grpc/get_user_friends_data.go | 53 +++++++++++++++++++++-------------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/grpc/get_user_data.go b/grpc/get_user_data.go index 575adfb..ccbd35d 100644 --- a/grpc/get_user_data.go +++ b/grpc/get_user_data.go @@ -4,19 +4,19 @@ import ( "context" "time" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "github.com/PretendoNetwork/friends/database" database_3ds "github.com/PretendoNetwork/friends/database/3ds" 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) { - friend, err := database_wiiu.GetUserData(in.GetPid()) + friend, err := database_wiiu.GetUserData(types.PID(in.GetPid())) if err != nil { globals.Logger.Critical(err.Error()) if err == database.ErrPIDNotFound { @@ -25,28 +25,34 @@ func (s *gRPCFriendsV2Server) GetUserDataWiiU(ctx context.Context, in *pb.GetUse return nil, status.Errorf(codes.Internal, "Internal error") } } - var comment = &pb.Comment{ + + comment := &pb.Comment{ Contents: string(friend.Status.Contents), LastChanged: timestamppb.New(time.Unix(int64(friend.Status.LastChanged.Second()), 0)), } - var mii = &pb.MiiV2{ + + 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)), } - var principal = &pb.PrincipalBasicInfo{ + + principal := &pb.PrincipalBasicInfo{ Pid: uint32(friend.NNAInfo.PrincipalBasicInfo.PID), Nnid: string(friend.NNAInfo.PrincipalBasicInfo.NNID), Mii: mii, } - var nnaInfo = &pb.NNAInfo{ + + nnaInfo := &pb.NNAInfo{ PrincipalBasicInfo: principal, } - var gameKey = &pb.GameKey{ + + gameKey := &pb.GameKey{ TitleId: uint64(friend.Presence.GameKey.TitleID), TitleVersion: uint32(friend.Presence.GameKey.TitleVersion), } - var presence = &pb.NintendoPresenceV2{ + + presence := &pb.NintendoPresenceV2{ ChangedFlags: uint32(friend.Presence.ChangedFlags), Online: bool(friend.Presence.Online), GameKey: gameKey, @@ -56,12 +62,13 @@ func (s *gRPCFriendsV2Server) GetUserDataWiiU(ctx context.Context, in *pb.GetUse GatheringId: uint32(friend.Presence.GatheringID), ApplicationData: friend.Presence.ApplicationData, } - var info = &pb.FriendInfoWiiU{ + + info := &pb.FriendInfoWiiU{ NnaInfo: nnaInfo, Presence: presence, Status: comment, - BecameFriend: timestamppb.New(time.Unix(int64(friend.BecameFriend.Second()), 0)), - LastOnline: timestamppb.New(time.Unix(int64(friend.LastOnline.Second()), 0)), + BecameFriend: timestamppb.New(friend.BecameFriend.Standard()), + LastOnline: timestamppb.New(friend.LastOnline.Standard()), } return &pb.GetUserDataWiiUResponse{ User: info, @@ -69,7 +76,7 @@ func (s *gRPCFriendsV2Server) GetUserDataWiiU(ctx context.Context, in *pb.GetUse } func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUserData3DSRequest) (*pb.GetUserData3DSResponse, error) { - friend, err := database_3ds.GetUserData(in.GetPid()) + friend, err := database_3ds.GetUserData(types.PID(in.GetPid())) if err != nil { globals.Logger.Critical(err.Error()) if err == database.ErrPIDNotFound { @@ -79,7 +86,7 @@ func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUser } } - miiData, err := database_3ds.GetMii(in.GetPid()) + miiData, err := database_3ds.GetMii(types.PID(in.GetPid())) if err != nil { globals.Logger.Critical(err.Error()) if err == database.ErrPIDNotFound { @@ -89,25 +96,25 @@ func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUser } } - var gameKey = &pb.GameKey{ + gameKey := &pb.GameKey{ TitleId: uint64(friend.GameKey.TitleID), TitleVersion: uint32(friend.GameKey.TitleVersion), } - var mii = &pb.Mii{ + mii := &pb.Mii{ Name: string(miiData.Mii.Name), ProfanityFlag: bool(miiData.Mii.ProfanityFlag), CharacterSet: uint32(miiData.Mii.CharacterSet), MiiDataEncrypted: miiData.Mii.MiiData, // TODO: Implement MiiData } - var friendMii = &pb.FriendMii{ + friendMii := &pb.FriendMii{ Pid: uint32(miiData.PID), Mii: mii, ModifiedAt: timestamppb.New(time.Unix(int64(miiData.ModifiedAt.Second()), 0)), } - var presence = &pb.NintendoPresence{} + presence := &pb.NintendoPresence{} connectedUser, ok := globals.ConnectedUsers.Get(uint32(friend.PID)) if ok && connectedUser != nil { presence.ChangedFlags = uint32(connectedUser.Presence.ChangedFlags) @@ -125,7 +132,7 @@ func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUser presence.ApplicationArg = connectedUser.Presence.ApplicationArg } - var info = &pb.FriendInfo3DS{ + info := &pb.FriendInfo3DS{ Pid: uint32(friend.PID), Region: uint32(friend.Region), Country: uint32(friend.Country), @@ -135,9 +142,9 @@ func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUser Presence: presence, GameKey: gameKey, Message: string(friend.Message), - MessageUpdatedAt: timestamppb.New(time.Unix(int64(friend.MessageUpdatedAt.Second()), 0)), - MiiModifiedAt: timestamppb.New(time.Unix(int64(friend.MiiModifiedAt.Second()), 0)), - LastOnline: timestamppb.New(time.Unix(int64(friend.LastOnline.Second()), 0)), + MessageUpdatedAt: timestamppb.New(friend.MessageUpdatedAt.Standard()), + MiiModifiedAt: timestamppb.New(friend.MiiModifiedAt.Standard()), + LastOnline: timestamppb.New(friend.LastOnline.Standard()), Mii: friendMii, } return &pb.GetUserData3DSResponse{ diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data.go index e01aad9..6a1c08a 100644 --- a/grpc/get_user_friends_data.go +++ b/grpc/get_user_friends_data.go @@ -90,29 +90,33 @@ func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb } for _, friend := range friendList { - // TODO: Is there a better way to do this? I really don't know what I'm doing here - var comment = &pb.Comment{ + comment := &pb.Comment{ Contents: string(friend.Status.Contents), LastChanged: timestamppb.New(time.Unix(int64(friend.Status.LastChanged.Second()), 0)), } - var mii = &pb.MiiV2{ + + 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)), } - var principal = &pb.PrincipalBasicInfo{ + + principal := &pb.PrincipalBasicInfo{ Pid: uint32(friend.NNAInfo.PrincipalBasicInfo.PID), Nnid: string(friend.NNAInfo.PrincipalBasicInfo.NNID), Mii: mii, } - var nnaInfo = &pb.NNAInfo{ + + nnaInfo := &pb.NNAInfo{ PrincipalBasicInfo: principal, } - var gameKey = &pb.GameKey{ + + gameKey := &pb.GameKey{ TitleId: uint64(friend.Presence.GameKey.TitleID), TitleVersion: uint32(friend.Presence.GameKey.TitleVersion), } - var presence = &pb.NintendoPresenceV2{ + + presence := &pb.NintendoPresenceV2{ ChangedFlags: uint32(friend.Presence.ChangedFlags), Online: bool(friend.Presence.Online), GameKey: gameKey, @@ -122,13 +126,15 @@ func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb GatheringId: uint32(friend.Presence.GatheringID), ApplicationData: friend.Presence.ApplicationData, } - var info = &pb.FriendInfoWiiU{ + + info := &pb.FriendInfoWiiU{ NnaInfo: nnaInfo, Presence: presence, Status: comment, - BecameFriend: timestamppb.New(time.Unix(int64(friend.BecameFriend.Second()), 0)), - LastOnline: timestamppb.New(time.Unix(int64(friend.LastOnline.Second()), 0)), + BecameFriend: timestamppb.New(friend.BecameFriend.Standard()), + LastOnline: timestamppb.New(friend.LastOnline.Standard()), } + friends = append(friends, info) } @@ -213,35 +219,40 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. } for _, friend := range friendInfoList { - // TODO: Is there a better way to do this? I really don't know what I'm doing here - var gameKey = &pb.GameKey{ + gameKey := &pb.GameKey{ TitleId: uint64(friend.GameKey.TitleID), TitleVersion: uint32(friend.GameKey.TitleVersion), } - var miiIndex = -1 + + miiIndex := -1 for index, mii := range miiList { if mii.PID == friend.PID { miiIndex = index break } } + if miiIndex == -1 { continue } - var miiData = miiList[miiIndex] - var mii = &pb.Mii{ + + miiData := miiList[miiIndex] + mii := &pb.Mii{ Name: string(miiData.Mii.Name), ProfanityFlag: bool(miiData.Mii.ProfanityFlag), CharacterSet: uint32(miiData.Mii.CharacterSet), MiiData: miiData.Mii.MiiData, } - var friendMii = &pb.FriendMii{ + + friendMii := &pb.FriendMii{ Pid: uint32(miiData.PID), Mii: mii, ModifiedAt: timestamppb.New(time.Unix(int64(miiData.ModifiedAt.Second()), 0)), } - var presence = &pb.NintendoPresence{} + + 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{ @@ -258,7 +269,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. presence.ApplicationArg = connectedUser.Presence.ApplicationArg } - var info = &pb.FriendInfo3DS{ + info := &pb.FriendInfo3DS{ Pid: uint32(friend.PID), Region: uint32(friend.Region), Country: uint32(friend.Country), @@ -268,9 +279,9 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. Presence: presence, GameKey: gameKey, Message: string(friend.Message), - MessageUpdatedAt: timestamppb.New(time.Unix(int64(friend.MessageUpdatedAt.Second()), 0)), - MiiModifiedAt: timestamppb.New(time.Unix(int64(friend.MiiModifiedAt.Second()), 0)), - LastOnline: timestamppb.New(time.Unix(int64(friend.LastOnline.Second()), 0)), + MessageUpdatedAt: timestamppb.New(friend.MessageUpdatedAt.Standard()), + MiiModifiedAt: timestamppb.New(friend.MiiModifiedAt.Standard()), + LastOnline: timestamppb.New(friend.LastOnline.Standard()), Mii: friendMii, } friends = append(friends, info) From bfeadb61c5fe5385f14d13eb2624161247e24e84 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Sat, 23 May 2026 17:57:04 -0500 Subject: [PATCH 10/18] chore(grpc): remove old row scan --- database/3ds/get_user_data.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/database/3ds/get_user_data.go b/database/3ds/get_user_data.go index 0e252d8..ec778de 100644 --- a/database/3ds/get_user_data.go +++ b/database/3ds/get_user_data.go @@ -27,13 +27,6 @@ func GetUserData(pid types.PID) (friends_3ds_types.FriendPersistentInfo, error) } } - var relationshipType uint8 - - err = row.Scan(&pid, &relationshipType) - if err != nil { - return friendPersistentInfo, err - } - var region uint8 var area uint8 var language uint8 From 067363fd84ee8c65fa7c5e6a55651262c55fe421 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Sat, 23 May 2026 17:57:35 -0500 Subject: [PATCH 11/18] fix(grpc): throw grpc errors --- grpc/get_user_friends_data.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data.go index 6a1c08a..05443aa 100644 --- a/grpc/get_user_friends_data.go +++ b/grpc/get_user_friends_data.go @@ -5,6 +5,9 @@ import ( "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" database_wiiu "github.com/PretendoNetwork/friends/database/wiiu" @@ -22,7 +25,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb if err != nil && err != database.ErrEmptyList { return &pb.GetUserFriendsDataWiiUResponse{ Friends: friends, - }, nil + }, status.Errorf(codes.Internal, "internal server error") } if globals.Config.EnableBella { @@ -149,7 +152,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. if err != nil && err != database.ErrEmptyList { return &pb.GetUserFriendsData3DSResponse{ Friends: friends, - }, nil + }, status.Errorf(codes.Internal, "internal server error") } friendPIDs := make([]uint32, len(friendList)) @@ -163,7 +166,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. globals.Logger.Critical(err.Error()) return &pb.GetUserFriendsData3DSResponse{ Friends: friends, - }, nil + }, status.Errorf(codes.Internal, "internal server error") } miiList, err := database_3ds.GetFriendMiis(friendPIDs) @@ -171,7 +174,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. globals.Logger.Critical(err.Error()) return &pb.GetUserFriendsData3DSResponse{ Friends: friends, - }, nil + }, status.Errorf(codes.Internal, "internal server error") } if globals.Config.EnableBella { From ab0f152eb30bb917b0f99e02c3fddb5c0da7d8e4 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Mon, 25 May 2026 18:43:04 -0500 Subject: [PATCH 12/18] feat(grpc): add decrypted mii data to 3DS response --- README.md | 3 +- globals/config.go | 1 + grpc/get_user_data.go | 7 ++++- grpc/get_user_friends_data.go | 14 ++++++--- utility/decrypt_mii.go | 57 +++++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 utility/decrypt_mii.go diff --git a/README.md b/README.md index 8a77f5a..40c3b9c 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/globals/config.go b/globals/config.go index 8203bc8..02dcf7b 100644 --- a/globals/config.go +++ b/globals/config.go @@ -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{} diff --git a/grpc/get_user_data.go b/grpc/get_user_data.go index ccbd35d..4ab9f03 100644 --- a/grpc/get_user_data.go +++ b/grpc/get_user_data.go @@ -8,6 +8,7 @@ import ( database_3ds "github.com/PretendoNetwork/friends/database/3ds" database_wiiu "github.com/PretendoNetwork/friends/database/wiiu" "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" @@ -106,7 +107,11 @@ func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUser ProfanityFlag: bool(miiData.Mii.ProfanityFlag), CharacterSet: uint32(miiData.Mii.CharacterSet), MiiDataEncrypted: miiData.Mii.MiiData, - // TODO: Implement MiiData + } + + mii_data, err := utility.DecryptMiiData(miiData.Mii.MiiData) + if err == nil { + mii.MiiData = mii_data } friendMii := &pb.FriendMii{ Pid: uint32(miiData.PID), diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data.go index 05443aa..0db2201 100644 --- a/grpc/get_user_friends_data.go +++ b/grpc/get_user_friends_data.go @@ -12,6 +12,7 @@ import ( database_3ds "github.com/PretendoNetwork/friends/database/3ds" database_wiiu "github.com/PretendoNetwork/friends/database/wiiu" "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" @@ -241,10 +242,15 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. miiData := miiList[miiIndex] mii := &pb.Mii{ - Name: string(miiData.Mii.Name), - ProfanityFlag: bool(miiData.Mii.ProfanityFlag), - CharacterSet: uint32(miiData.Mii.CharacterSet), - MiiData: miiData.Mii.MiiData, + 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{ diff --git a/utility/decrypt_mii.go b/utility/decrypt_mii.go new file mode 100644 index 0000000..bdbbd5c --- /dev/null +++ b/utility/decrypt_mii.go @@ -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(mii_data types.Buffer) ([]byte, error) { + if len(mii_data) < 96 { + return nil, fmt.Errorf("Mii data length is incorrect: %d", len(mii_data)) + } + nonce := mii_data[:8] + ciphertext := mii_data[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 +} From a797ea520777009532ab0bd5cc2b68be5fca7a65 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Wed, 27 May 2026 15:41:48 -0500 Subject: [PATCH 13/18] fix(grpc): incorrect mii data length check + variable casing --- utility/decrypt_mii.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utility/decrypt_mii.go b/utility/decrypt_mii.go index bdbbd5c..b7fb266 100644 --- a/utility/decrypt_mii.go +++ b/utility/decrypt_mii.go @@ -32,12 +32,12 @@ func ccmDecrypt(key, nonce, ciphertext []byte) ([]byte, error) { return out[aes.BlockSize:], nil } -func DecryptMiiData(mii_data types.Buffer) ([]byte, error) { - if len(mii_data) < 96 { - return nil, fmt.Errorf("Mii data length is incorrect: %d", len(mii_data)) +func DecryptMiiData(miiData types.Buffer) ([]byte, error) { + if len(miiData) < 0x70 { + return nil, fmt.Errorf("Mii data length is incorrect: %d", len(miiData)) } - nonce := mii_data[:8] - ciphertext := mii_data[8 : 8+0x58] + nonce := miiData[:8] + ciphertext := miiData[8 : 8+0x58] key, err := hex.DecodeString(globals.Config.MiiDecryptKey) if err != nil { From f2f00ec4dc9050a11fa7816b2ce8662c3a353575 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Wed, 27 May 2026 15:45:41 -0500 Subject: [PATCH 14/18] chore(grpc): split out 3DS & Wii U grpc methods --- grpc/get_user_data.go | 158 ------------------ grpc/get_user_data_3ds.go | 97 +++++++++++ grpc/get_user_data_wiiu.go | 75 +++++++++ ...s_data.go => get_user_friends_data_3ds.go} | 129 -------------- grpc/get_user_friends_data_wiiu.go | 144 ++++++++++++++++ 5 files changed, 316 insertions(+), 287 deletions(-) delete mode 100644 grpc/get_user_data.go create mode 100644 grpc/get_user_data_3ds.go create mode 100644 grpc/get_user_data_wiiu.go rename grpc/{get_user_friends_data.go => get_user_friends_data_3ds.go} (53%) create mode 100644 grpc/get_user_friends_data_wiiu.go diff --git a/grpc/get_user_data.go b/grpc/get_user_data.go deleted file mode 100644 index 4ab9f03..0000000 --- a/grpc/get_user_data.go +++ /dev/null @@ -1,158 +0,0 @@ -package grpc - -import ( - "context" - "time" - - "github.com/PretendoNetwork/friends/database" - database_3ds "github.com/PretendoNetwork/friends/database/3ds" - database_wiiu "github.com/PretendoNetwork/friends/database/wiiu" - "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) GetUserDataWiiU(ctx context.Context, in *pb.GetUserDataWiiURequest) (*pb.GetUserDataWiiUResponse, error) { - friend, err := database_wiiu.GetUserData(types.PID(in.GetPid())) - if err != nil { - globals.Logger.Critical(err.Error()) - if err == database.ErrPIDNotFound { - return nil, status.Errorf(codes.NotFound, "PID was not found") - } else { - return nil, status.Errorf(codes.Internal, "Internal error") - } - } - - 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()), - } - return &pb.GetUserDataWiiUResponse{ - User: info, - }, nil -} - -func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUserData3DSRequest) (*pb.GetUserData3DSResponse, error) { - friend, err := database_3ds.GetUserData(types.PID(in.GetPid())) - if err != nil { - globals.Logger.Critical(err.Error()) - if err == database.ErrPIDNotFound { - return nil, status.Errorf(codes.NotFound, "PID was not found") - } else { - return nil, status.Errorf(codes.Internal, "Internal error") - } - } - - miiData, err := database_3ds.GetMii(types.PID(in.GetPid())) - if err != nil { - globals.Logger.Critical(err.Error()) - if err == database.ErrPIDNotFound { - return nil, status.Errorf(codes.NotFound, "PID was not found") - } else { - return nil, status.Errorf(codes.Internal, "Internal error") - } - } - - gameKey := &pb.GameKey{ - TitleId: uint64(friend.GameKey.TitleID), - TitleVersion: uint32(friend.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(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, - } - return &pb.GetUserData3DSResponse{ - User: info, - }, nil -} diff --git a/grpc/get_user_data_3ds.go b/grpc/get_user_data_3ds.go new file mode 100644 index 0000000..0a188f6 --- /dev/null +++ b/grpc/get_user_data_3ds.go @@ -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 { + globals.Logger.Critical(err.Error()) + if err == database.ErrPIDNotFound { + return nil, status.Errorf(codes.NotFound, "PID was not found") + } else { + return nil, status.Errorf(codes.Internal, "Internal error") + } + } + + miiData, err := database_3ds.GetMii(types.PID(in.GetPid())) + if err != nil { + globals.Logger.Critical(err.Error()) + if err == database.ErrPIDNotFound { + return nil, status.Errorf(codes.NotFound, "PID was not found") + } else { + 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 +} diff --git a/grpc/get_user_data_wiiu.go b/grpc/get_user_data_wiiu.go new file mode 100644 index 0000000..7883be4 --- /dev/null +++ b/grpc/get_user_data_wiiu.go @@ -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 { + globals.Logger.Critical(err.Error()) + if err == database.ErrPIDNotFound { + return nil, status.Errorf(codes.NotFound, "PID was not found") + } else { + 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 +} diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data_3ds.go similarity index 53% rename from grpc/get_user_friends_data.go rename to grpc/get_user_friends_data_3ds.go index 0db2201..e85802e 100644 --- a/grpc/get_user_friends_data.go +++ b/grpc/get_user_friends_data_3ds.go @@ -10,143 +10,14 @@ import ( "github.com/PretendoNetwork/friends/database" database_3ds "github.com/PretendoNetwork/friends/database/3ds" - database_wiiu "github.com/PretendoNetwork/friends/database/wiiu" "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" - 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 { - 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 -} - func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb.GetUserFriendsData3DSRequest) (*pb.GetUserFriendsData3DSResponse, error) { var friends []*pb.FriendInfo3DS friendList, err := database_3ds.GetUserFriends(in.Pid) diff --git a/grpc/get_user_friends_data_wiiu.go b/grpc/get_user_friends_data_wiiu.go new file mode 100644 index 0000000..2656ae3 --- /dev/null +++ b/grpc/get_user_friends_data_wiiu.go @@ -0,0 +1,144 @@ +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 { + 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 +} From 38aec18ae80d45ebb8a56383f38e381e00ae2e52 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Fri, 29 May 2026 16:45:01 -0500 Subject: [PATCH 15/18] fix(grpc): add missing log entries for get user friends data --- grpc/get_user_friends_data_3ds.go | 1 + grpc/get_user_friends_data_wiiu.go | 1 + 2 files changed, 2 insertions(+) diff --git a/grpc/get_user_friends_data_3ds.go b/grpc/get_user_friends_data_3ds.go index e85802e..4e1ab7d 100644 --- a/grpc/get_user_friends_data_3ds.go +++ b/grpc/get_user_friends_data_3ds.go @@ -22,6 +22,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. 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") diff --git a/grpc/get_user_friends_data_wiiu.go b/grpc/get_user_friends_data_wiiu.go index 2656ae3..b1e7e15 100644 --- a/grpc/get_user_friends_data_wiiu.go +++ b/grpc/get_user_friends_data_wiiu.go @@ -20,6 +20,7 @@ func (s *gRPCFriendsV2Server) GetUserFriendsDataWiiU(ctx context.Context, in *pb 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") From eacd16bc30dd7265fc2b8b1ab46b78820a2f383f Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Fri, 29 May 2026 17:43:27 -0500 Subject: [PATCH 16/18] feat(config): add checks for Mii decryption key config --- init.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/init.go b/init.go index d8b7497..9d85e48 100644 --- a/init.go +++ b/init.go @@ -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() } From 084cd5e8fc87d932d46d944b1e3c93d9bc851077 Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Fri, 29 May 2026 17:47:44 -0500 Subject: [PATCH 17/18] fix(grpc): move logs to only occur when internal error occurs --- grpc/get_user_data_3ds.go | 4 ++-- grpc/get_user_data_wiiu.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grpc/get_user_data_3ds.go b/grpc/get_user_data_3ds.go index 0a188f6..6474a36 100644 --- a/grpc/get_user_data_3ds.go +++ b/grpc/get_user_data_3ds.go @@ -18,20 +18,20 @@ import ( 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 { - globals.Logger.Critical(err.Error()) 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 { - globals.Logger.Critical(err.Error()) 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") } } diff --git a/grpc/get_user_data_wiiu.go b/grpc/get_user_data_wiiu.go index 7883be4..5a47bba 100644 --- a/grpc/get_user_data_wiiu.go +++ b/grpc/get_user_data_wiiu.go @@ -17,10 +17,10 @@ import ( 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 { - globals.Logger.Critical(err.Error()) 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") } } From 235b31adc6f778ed7bea3b621fab202e79e91acb Mon Sep 17 00:00:00 2001 From: Jemma Poffinbarger Date: Fri, 29 May 2026 17:48:26 -0500 Subject: [PATCH 18/18] chore: go mod tidy --- go.mod | 2 +- go.sum | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/go.mod b/go.mod index aaff955..ed7758e 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 026df5f..3bf3266 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -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=