From 1733e6baa66fcdc1bceeb2ce46682af180ba1b7d Mon Sep 17 00:00:00 2001 From: Andrei Mihu Date: Mon, 18 Mar 2019 00:05:32 +0000 Subject: [PATCH] Console users handler functions. (#310) --- console/console.pb.go | 336 +++++++++++++-------------- console/console.proto | 10 +- console/console.swagger.json | 21 +- main.go | 2 +- server/api.go | 15 +- server/config.go | 101 +++++++- server/console.go | 11 +- server/console_account.go | 435 ++++++++++++++++++++++++++++++++++- server/console_config.go | 32 ++- server/console_unlink.go | 209 +++++++++++++++++ server/console_user.go | 193 +++++++++++++++- server/core_leaderboard.go | 4 +- 12 files changed, 1150 insertions(+), 219 deletions(-) diff --git a/console/console.pb.go b/console/console.pb.go index eb1a28196..59dd80412 100644 --- a/console/console.pb.go +++ b/console/console.pb.go @@ -680,9 +680,7 @@ type ListUsersRequest struct { // Search only banned users. Banned bool `protobuf:"varint,2,opt,name=banned,proto3" json:"banned,omitempty"` // Search only recorded deletes. - Tombstones bool `protobuf:"varint,3,opt,name=tombstones,proto3" json:"tombstones,omitempty"` - // An (optional) cursor to fetch next page. - Cursor []byte `protobuf:"bytes,4,opt,name=cursor,proto3" json:"cursor,omitempty"` + Tombstones bool `protobuf:"varint,3,opt,name=tombstones,proto3" json:"tombstones,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -734,19 +732,14 @@ func (m *ListUsersRequest) GetTombstones() bool { return false } -func (m *ListUsersRequest) GetCursor() []byte { - if m != nil { - return m.Cursor - } - return nil -} - // List of storage objects. type StorageList struct { // List of storage objects matching list/filter operation. Objects []*api.StorageObject `protobuf:"bytes,1,rep,name=objects,proto3" json:"objects,omitempty"` + // Approximate total number of storage objects. + TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` // An (optional) cursor for paging results. - Cursor []byte `protobuf:"bytes,2,opt,name=cursor,proto3" json:"cursor,omitempty"` + Cursor []byte `protobuf:"bytes,3,opt,name=cursor,proto3" json:"cursor,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -784,6 +777,13 @@ func (m *StorageList) GetObjects() []*api.StorageObject { return nil } +func (m *StorageList) GetTotalCount() int32 { + if m != nil { + return m.TotalCount + } + return 0 +} + func (m *StorageList) GetCursor() []byte { if m != nil { return m.Cursor @@ -985,8 +985,8 @@ func (m *UpdateAccountRequest) GetWallet() *wrappers.StringValue { type UserList struct { // A list of users. Users []*api.User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` - // A cursor to fetch more results. - Cursor []byte `protobuf:"bytes,2,opt,name=cursor,proto3" json:"cursor,omitempty"` + // Approximate total number of users. + TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -1024,11 +1024,11 @@ func (m *UserList) GetUsers() []*api.User { return nil } -func (m *UserList) GetCursor() []byte { +func (m *UserList) GetTotalCount() int32 { if m != nil { - return m.Cursor + return m.TotalCount } - return nil + return 0 } // List of nodes and their stats. @@ -1508,159 +1508,159 @@ func init() { func init() { proto.RegisterFile("console/console.proto", fileDescriptor_9289ac5ba895f2a7) } var fileDescriptor_9289ac5ba895f2a7 = []byte{ - // 2421 bytes of a gzipped FileDescriptorProto + // 2427 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x4d, 0x73, 0x1b, 0xc7, - 0xd1, 0xf6, 0x82, 0x04, 0x01, 0x34, 0x08, 0x90, 0x1a, 0xd2, 0x7a, 0x97, 0xa0, 0x44, 0xc3, 0x6b, - 0x99, 0x92, 0x20, 0x11, 0x90, 0xc0, 0x37, 0x91, 0xcc, 0xb8, 0x92, 0x88, 0x94, 0x44, 0x31, 0x91, - 0x94, 0xd4, 0x52, 0x8c, 0xaa, 0x94, 0x54, 0x50, 0x83, 0xdd, 0x21, 0xb0, 0xe6, 0x62, 0x07, 0xde, - 0x19, 0x90, 0xa1, 0x59, 0xac, 0xca, 0xd7, 0x31, 0x97, 0x38, 0x87, 0xdc, 0x52, 0x95, 0x73, 0xfe, - 0x45, 0xfe, 0x81, 0x2b, 0x27, 0xdf, 0x73, 0xcb, 0x5f, 0xc8, 0x21, 0x35, 0x1f, 0x0b, 0x2c, 0x3e, - 0x96, 0x84, 0xe4, 0xd2, 0x41, 0x25, 0x4c, 0xcf, 0xd3, 0xfd, 0xf4, 0xf4, 0xf6, 0xf4, 0x4c, 0x0f, - 0xe1, 0x43, 0x87, 0x06, 0x8c, 0xfa, 0xa4, 0xa6, 0xff, 0xaf, 0x76, 0x43, 0xca, 0x29, 0x2a, 0x06, - 0xf8, 0x08, 0x77, 0x70, 0x55, 0x4b, 0x4b, 0x95, 0x96, 0xc7, 0xdb, 0xbd, 0x66, 0xd5, 0xa1, 0x9d, - 0x5a, 0x9b, 0x84, 0xd4, 0x73, 0x7c, 0xdc, 0x64, 0x35, 0x85, 0xaa, 0xe1, 0xae, 0x27, 0xfe, 0x29, - 0xdd, 0xd2, 0xb5, 0x16, 0xa5, 0x2d, 0x9f, 0x28, 0x69, 0x10, 0x50, 0x8e, 0xb9, 0x47, 0x03, 0xa6, - 0x67, 0x57, 0xf5, 0xac, 0x1c, 0x35, 0x7b, 0x87, 0x35, 0xd2, 0xe9, 0xf2, 0x53, 0x3d, 0xf9, 0xd1, - 0xe8, 0x24, 0xf7, 0x3a, 0x84, 0x71, 0xdc, 0xe9, 0x6a, 0xc0, 0xda, 0x28, 0xe0, 0x24, 0xc4, 0xdd, - 0x2e, 0x09, 0x23, 0xeb, 0x77, 0xe5, 0x7f, 0xce, 0x46, 0x8b, 0x04, 0x1b, 0xec, 0x04, 0xb7, 0x5a, - 0x24, 0xac, 0xd1, 0xae, 0xe4, 0x1f, 0xf7, 0xc5, 0x3a, 0x82, 0xe5, 0x47, 0x8e, 0x43, 0x7b, 0x01, - 0x7f, 0x4c, 0x7c, 0xc2, 0x89, 0x4d, 0xbe, 0xec, 0x11, 0xc6, 0x51, 0x11, 0x52, 0x9e, 0x6b, 0x1a, - 0x65, 0xe3, 0x56, 0xce, 0x4e, 0x79, 0x2e, 0xda, 0x81, 0x85, 0x90, 0x38, 0x34, 0x74, 0x1b, 0xae, - 0xc0, 0x79, 0x34, 0x30, 0x53, 0x65, 0xe3, 0x56, 0xbe, 0x5e, 0xaa, 0x2a, 0x7f, 0xaa, 0x91, 0x3f, - 0xd5, 0x6d, 0x4a, 0xfd, 0x5f, 0x60, 0xbf, 0x47, 0xec, 0xa2, 0x52, 0x79, 0xac, 0x35, 0xac, 0x6f, - 0x67, 0xa0, 0xa0, 0xd9, 0x9e, 0xfc, 0xa6, 0x4b, 0x43, 0x8e, 0x36, 0x20, 0x83, 0x95, 0x40, 0x72, - 0xe5, 0xeb, 0x4b, 0x55, 0x1d, 0x76, 0x11, 0x4c, 0x8d, 0xb5, 0x23, 0x0c, 0xda, 0x84, 0x0c, 0x6d, - 0x7e, 0x41, 0x1c, 0xce, 0xcc, 0x54, 0x79, 0xe6, 0x56, 0xbe, 0xbe, 0x12, 0x87, 0xef, 0x73, 0x1a, - 0xe2, 0x16, 0xf9, 0x99, 0x44, 0xd8, 0x11, 0x12, 0xdd, 0x85, 0xcc, 0x61, 0xe8, 0x91, 0xc0, 0x65, - 0xe6, 0x8c, 0x54, 0x42, 0x71, 0xa5, 0xa7, 0x72, 0xca, 0x8e, 0x20, 0xe8, 0x36, 0xcc, 0xb5, 0x42, - 0xda, 0xeb, 0x32, 0x73, 0x56, 0x82, 0xaf, 0xc4, 0xc1, 0xbb, 0x62, 0xc6, 0xd6, 0x00, 0xf4, 0x7d, - 0xc8, 0x76, 0x08, 0x63, 0xb8, 0x45, 0x98, 0x99, 0x96, 0xe0, 0x52, 0x1c, 0xbc, 0xd3, 0xc6, 0x41, - 0x40, 0xfc, 0x17, 0x0a, 0x62, 0xf7, 0xb1, 0xe8, 0x25, 0x2c, 0xf9, 0x04, 0xbb, 0x24, 0x6c, 0x52, - 0x1c, 0xba, 0x0d, 0x15, 0x24, 0x66, 0xce, 0x49, 0x13, 0xd7, 0xe3, 0x26, 0x9e, 0x0f, 0x60, 0xb6, - 0x44, 0xd9, 0xc8, 0x1f, 0x15, 0x31, 0xf4, 0x43, 0x28, 0x04, 0x94, 0x7b, 0x87, 0x9e, 0xa3, 0x3e, - 0xad, 0x99, 0x91, 0x96, 0xcc, 0xb8, 0xa5, 0x97, 0x31, 0x80, 0x3d, 0x0c, 0x47, 0x3b, 0x50, 0x3c, - 0xc1, 0xbe, 0x4f, 0x78, 0xc3, 0x27, 0x6e, 0x8b, 0x84, 0xcc, 0xcc, 0x4a, 0x03, 0xd7, 0xaa, 0xc3, - 0x5b, 0xa0, 0xfa, 0x5a, 0xa2, 0x9e, 0x4b, 0x90, 0x5d, 0x38, 0x89, 0x8d, 0x98, 0xb5, 0x0a, 0x39, - 0xfd, 0xb9, 0xf6, 0xdc, 0xd1, 0xec, 0xb1, 0x5e, 0xc0, 0xd2, 0xa3, 0x1e, 0x6f, 0x93, 0x80, 0x0b, - 0xd2, 0x7e, 0x92, 0x95, 0x20, 0xdb, 0x63, 0x24, 0x0c, 0x70, 0x87, 0x68, 0x70, 0x7f, 0x2c, 0xe6, - 0xba, 0x98, 0xb1, 0x13, 0x1a, 0xba, 0x32, 0xd3, 0x72, 0x76, 0x7f, 0x6c, 0xfd, 0xd5, 0x80, 0xb9, - 0x1d, 0x1a, 0x1c, 0x7a, 0x2d, 0x74, 0x15, 0xe6, 0x1c, 0xf9, 0x4b, 0x1b, 0xd0, 0x23, 0xb4, 0x05, - 0xd9, 0x13, 0x1c, 0x06, 0x5e, 0xd0, 0x8a, 0x52, 0x65, 0x6d, 0x74, 0x35, 0xca, 0x42, 0xf5, 0xb5, - 0x82, 0xd9, 0x7d, 0x7c, 0xe9, 0x33, 0xc8, 0x68, 0x21, 0x5a, 0x86, 0xf4, 0xa1, 0x47, 0xfc, 0x68, - 0x2d, 0x6a, 0x80, 0x4c, 0xc8, 0xe8, 0x8f, 0xa9, 0x5d, 0x8b, 0x86, 0xd6, 0x3a, 0x14, 0x77, 0x94, - 0xf9, 0x7d, 0xc2, 0x98, 0x47, 0x03, 0x61, 0x81, 0xd3, 0x23, 0x12, 0x44, 0x16, 0xe4, 0xc0, 0xda, - 0x86, 0x25, 0xb5, 0xdf, 0x74, 0xfa, 0x25, 0xec, 0xba, 0x55, 0xc8, 0xa9, 0xbc, 0x6c, 0x78, 0xfd, - 0x28, 0x28, 0xc1, 0x9e, 0x6b, 0xed, 0xc0, 0x55, 0x65, 0x43, 0x66, 0xe5, 0x01, 0x23, 0x61, 0x92, - 0x99, 0x15, 0xc8, 0xca, 0x94, 0x1d, 0x58, 0xc9, 0xc8, 0xf1, 0x9e, 0x6b, 0xfd, 0xce, 0x80, 0x92, - 0xb2, 0x32, 0xbc, 0x7b, 0xb4, 0xa5, 0x35, 0x00, 0x87, 0xfa, 0x3e, 0x71, 0xe4, 0x8e, 0x57, 0x16, - 0x63, 0x12, 0xb4, 0x08, 0x33, 0x47, 0xe4, 0x54, 0x1b, 0x15, 0x3f, 0xd1, 0xff, 0x41, 0x46, 0x7c, - 0x43, 0x41, 0x35, 0xa3, 0xbe, 0x88, 0x18, 0xee, 0xc9, 0xa0, 0x1d, 0x93, 0x50, 0xc4, 0xc4, 0x9c, - 0x55, 0x3e, 0xe8, 0xa1, 0xf5, 0x0c, 0x56, 0x94, 0x0b, 0x43, 0xf9, 0x95, 0x1c, 0x12, 0x9d, 0xac, - 0x83, 0x90, 0x28, 0xc1, 0x9e, 0x6b, 0x6d, 0x00, 0x7a, 0xee, 0x31, 0xae, 0x97, 0x12, 0x99, 0x88, - 0xb9, 0x64, 0xc4, 0x5d, 0xb2, 0xbe, 0x82, 0x45, 0x01, 0x17, 0xa1, 0x63, 0x11, 0xf8, 0x2a, 0xcc, - 0x1d, 0x7a, 0x3e, 0x27, 0x61, 0x84, 0x55, 0x23, 0x21, 0x6f, 0x8a, 0xfd, 0xac, 0x48, 0xb3, 0xb6, - 0x1e, 0x89, 0x08, 0x71, 0xda, 0x69, 0x32, 0x4e, 0x03, 0xc2, 0xe4, 0x92, 0xb3, 0x76, 0x4c, 0x22, - 0x13, 0xb4, 0x17, 0x32, 0x1a, 0xca, 0x55, 0xcf, 0xdb, 0x7a, 0x64, 0xbd, 0x81, 0xbc, 0x76, 0x53, - 0xb8, 0x10, 0xaf, 0x6c, 0xc6, 0xd4, 0x95, 0x6d, 0x60, 0x3b, 0x35, 0x64, 0x7b, 0x1b, 0x96, 0x0e, - 0x02, 0xdf, 0x0b, 0x8e, 0x1e, 0x93, 0x63, 0xcf, 0x21, 0x17, 0x84, 0xd2, 0x95, 0x80, 0x58, 0x28, - 0x95, 0x60, 0xcf, 0xb5, 0xfe, 0x9b, 0x86, 0xe5, 0x83, 0xae, 0x8b, 0x39, 0x89, 0xaa, 0x70, 0x82, - 0x95, 0x87, 0xb1, 0x4d, 0xac, 0x8e, 0x84, 0x6b, 0x63, 0x47, 0xc2, 0x3e, 0x0f, 0xbd, 0xa0, 0xa5, - 0x0e, 0x85, 0xc1, 0x16, 0xff, 0x11, 0xcc, 0xbb, 0x1e, 0xeb, 0xfa, 0xf8, 0xb4, 0x21, 0xb5, 0x67, - 0xa6, 0xd0, 0xce, 0x6b, 0x8d, 0x97, 0xc2, 0xc0, 0x43, 0x51, 0x80, 0x39, 0x76, 0x31, 0xc7, 0x32, - 0xba, 0x97, 0x52, 0x47, 0x68, 0xf4, 0x03, 0x00, 0x7c, 0x8c, 0x39, 0x0e, 0x1b, 0xbd, 0xd0, 0x37, - 0xd3, 0x53, 0xe8, 0xe6, 0x14, 0xfe, 0x20, 0xf4, 0xd1, 0x03, 0xc8, 0xfa, 0x38, 0x68, 0x35, 0x38, - 0x6e, 0x99, 0x73, 0x53, 0xa8, 0x66, 0x04, 0xfa, 0x15, 0x6e, 0x09, 0x7f, 0x7d, 0xaa, 0xaa, 0xae, - 0x99, 0x99, 0xc6, 0xdf, 0x08, 0x2d, 0x34, 0xc5, 0x3d, 0xe0, 0x2b, 0x1a, 0x10, 0x33, 0x3b, 0x8d, - 0x66, 0x84, 0x46, 0x9f, 0x41, 0xce, 0xe9, 0x31, 0x4e, 0x3b, 0xe2, 0x23, 0xe7, 0xa6, 0x51, 0x55, - 0xf0, 0x3d, 0x17, 0xd5, 0x21, 0x4d, 0x3a, 0xd8, 0xf3, 0x4d, 0x98, 0x42, 0x4d, 0x41, 0x91, 0x0d, - 0xd0, 0xcf, 0x29, 0x66, 0xe6, 0x65, 0x2a, 0x6f, 0x8e, 0x56, 0xde, 0x49, 0x79, 0x55, 0x7d, 0xac, - 0x33, 0x8f, 0x3d, 0x09, 0x78, 0x78, 0x6a, 0xe7, 0xa2, 0x4c, 0x64, 0xe8, 0xff, 0x61, 0x4e, 0xed, - 0x70, 0x73, 0x7e, 0x0a, 0x47, 0x34, 0xb6, 0xf4, 0x39, 0x14, 0x87, 0x4d, 0x46, 0xc5, 0xca, 0x18, - 0x14, 0xab, 0x65, 0x48, 0x1f, 0x0b, 0x25, 0x9d, 0xfd, 0x6a, 0xb0, 0x95, 0x7a, 0x68, 0x58, 0x3f, - 0x81, 0xac, 0x28, 0x0b, 0x72, 0x6f, 0xae, 0x43, 0x5a, 0xe4, 0x6c, 0xb4, 0x33, 0x17, 0xe3, 0x3b, - 0x53, 0x96, 0x5d, 0x35, 0x9d, 0xb8, 0x1d, 0xff, 0x9e, 0x06, 0xd8, 0xe7, 0x98, 0xf7, 0x98, 0x34, - 0xf7, 0x00, 0xd2, 0x01, 0x75, 0x49, 0x64, 0xee, 0xe3, 0xd1, 0xe8, 0x0c, 0xa0, 0xfa, 0xa7, 0xad, - 0xf0, 0xa5, 0x6f, 0x67, 0x61, 0x4e, 0x49, 0x10, 0x82, 0xd9, 0xd8, 0xa9, 0x29, 0x7f, 0xa3, 0x4f, - 0xa0, 0xc0, 0xd4, 0xa1, 0xd3, 0x50, 0x37, 0x2a, 0xe1, 0x45, 0xda, 0x9e, 0xd7, 0xc2, 0x1d, 0x79, - 0x83, 0xfa, 0x14, 0x8a, 0xdd, 0x90, 0x30, 0x12, 0x38, 0x44, 0xa3, 0x66, 0x24, 0xaa, 0x10, 0x49, - 0x15, 0xec, 0x23, 0xc8, 0x77, 0x30, 0x77, 0xda, 0x1a, 0x33, 0x2b, 0x31, 0x20, 0x45, 0x0a, 0x70, - 0x13, 0x16, 0x5a, 0x34, 0xa4, 0x3d, 0xee, 0x05, 0x91, 0xa1, 0xb4, 0x04, 0x15, 0xfb, 0x62, 0x05, - 0xbc, 0x01, 0x45, 0x7c, 0xdc, 0x6a, 0xf8, 0x98, 0x93, 0xc0, 0x39, 0x6d, 0x74, 0x98, 0xdc, 0x32, - 0x86, 0x3d, 0x8f, 0x8f, 0x5b, 0xcf, 0x95, 0xf0, 0x05, 0x43, 0x77, 0x00, 0x0d, 0xa3, 0x1a, 0x1d, - 0x4f, 0xed, 0x11, 0xc3, 0x5e, 0x88, 0x23, 0x5f, 0x78, 0x01, 0xba, 0x0d, 0x57, 0x46, 0xc0, 0xed, - 0x50, 0xee, 0x0a, 0xc3, 0x2e, 0xc6, 0xb1, 0xcf, 0x42, 0x71, 0xf2, 0x85, 0x98, 0x93, 0x06, 0x23, - 0x8e, 0x4c, 0x7e, 0xc3, 0xce, 0x88, 0xf1, 0x3e, 0x71, 0x50, 0x19, 0xe6, 0xa3, 0x29, 0x49, 0x06, - 0x72, 0x1a, 0xf4, 0xb4, 0xe0, 0x59, 0x83, 0x7c, 0x1f, 0xd1, 0x0e, 0xcd, 0xbc, 0x04, 0xe4, 0x34, - 0xe0, 0x59, 0x28, 0xea, 0xa7, 0x17, 0x74, 0x7b, 0xbc, 0x71, 0xd4, 0x64, 0x32, 0x35, 0x0d, 0x3b, - 0x2b, 0x05, 0x3f, 0x6d, 0x32, 0x64, 0x41, 0xa1, 0x3f, 0x29, 0xed, 0x17, 0x24, 0x20, 0x1f, 0x01, - 0x04, 0x41, 0x19, 0xe6, 0x07, 0x98, 0x76, 0x68, 0x16, 0x95, 0x0b, 0x11, 0xe4, 0x59, 0x88, 0xae, - 0x03, 0xd0, 0x1e, 0x8f, 0x38, 0x16, 0x94, 0x07, 0x4a, 0x22, 0x48, 0x6e, 0x40, 0x71, 0x30, 0x2d, - 0x59, 0x16, 0x55, 0x70, 0xfb, 0x10, 0x41, 0x63, 0x41, 0x21, 0x86, 0x6a, 0x73, 0xf3, 0x8a, 0x72, - 0xa5, 0x0f, 0x7a, 0xc6, 0xad, 0xff, 0x18, 0x30, 0x1f, 0x3f, 0x7e, 0xc7, 0xca, 0x7c, 0xec, 0x10, - 0x4d, 0x0d, 0x9d, 0xeb, 0xd7, 0x20, 0xe7, 0xb4, 0x71, 0xd0, 0x22, 0x8c, 0x70, 0x7d, 0xe4, 0x0f, - 0x04, 0xe2, 0x1a, 0x37, 0x54, 0xa2, 0x73, 0x43, 0x45, 0x38, 0xef, 0x84, 0x44, 0x44, 0x58, 0x54, - 0x2b, 0x5d, 0x85, 0xc7, 0xfb, 0x89, 0x57, 0x51, 0x03, 0x64, 0x83, 0x82, 0x0b, 0x81, 0x50, 0xee, - 0xc9, 0x32, 0xa2, 0x94, 0xe7, 0x2e, 0x57, 0x56, 0x70, 0x21, 0xb0, 0x9e, 0xc2, 0x62, 0x7c, 0xb1, - 0x72, 0x5b, 0xd6, 0x21, 0xed, 0x71, 0xd2, 0x89, 0xb6, 0xe5, 0xc5, 0x97, 0x5f, 0x05, 0xb5, 0xfe, - 0x99, 0x82, 0x95, 0xd7, 0xa1, 0xf7, 0xfe, 0x2f, 0x4f, 0xf5, 0xa8, 0x50, 0x4d, 0x73, 0xcc, 0x29, - 0x68, 0xfc, 0xc2, 0x95, 0x1e, 0xba, 0x70, 0xa1, 0xc7, 0xb0, 0xd0, 0x25, 0x61, 0xc7, 0x53, 0xc5, - 0x22, 0x24, 0xd8, 0xd5, 0xf1, 0x5b, 0x1d, 0xb3, 0xbb, 0x17, 0xf0, 0xcd, 0xba, 0xee, 0xe6, 0x06, - 0x3a, 0x36, 0xc1, 0x2e, 0x7a, 0x0a, 0x8b, 0x31, 0x2b, 0x27, 0x22, 0x0c, 0xfa, 0x54, 0xbb, 0xd0, - 0x4c, 0x8c, 0x5a, 0x86, 0xae, 0xfe, 0xcd, 0x0a, 0x64, 0xf4, 0xa5, 0x19, 0xfd, 0xc1, 0x80, 0xf9, - 0x78, 0xa7, 0x80, 0x3e, 0x19, 0xfd, 0x0c, 0x13, 0xfa, 0x88, 0xd2, 0xa4, 0xab, 0x7d, 0xec, 0x0e, - 0x6e, 0x55, 0xbf, 0x7e, 0x94, 0x6d, 0xce, 0xc1, 0x2c, 0x7c, 0x80, 0x3e, 0xf8, 0xfd, 0xbf, 0xfe, - 0xfd, 0x97, 0xd4, 0x75, 0xcb, 0xac, 0x1d, 0xd7, 0xa3, 0x86, 0xbf, 0x86, 0x63, 0x36, 0xb7, 0x8c, - 0x0a, 0x6a, 0x42, 0x66, 0x1b, 0x07, 0xa2, 0xb4, 0xa3, 0x95, 0x31, 0xfe, 0xa8, 0xc9, 0x29, 0x5d, - 0x1d, 0x5b, 0xe5, 0x13, 0xd1, 0xc7, 0x5b, 0x37, 0x24, 0xc5, 0x9a, 0x75, 0x6d, 0x88, 0x42, 0xa9, - 0xd5, 0xce, 0x3c, 0xf7, 0xbc, 0xd6, 0xc4, 0x01, 0xa2, 0x50, 0x50, 0x97, 0x5e, 0x6d, 0x10, 0xdd, - 0x48, 0x60, 0x1a, 0xea, 0xcb, 0x13, 0x49, 0xcb, 0x92, 0xb4, 0x54, 0x31, 0x93, 0x48, 0xd1, 0x6f, - 0x0d, 0x98, 0x8f, 0xf7, 0x1c, 0xe3, 0xa1, 0x9d, 0xd0, 0x91, 0x24, 0xf2, 0x6d, 0x4a, 0xbe, 0x8d, - 0xca, 0x9d, 0xc4, 0x45, 0xaa, 0x3e, 0xa5, 0x76, 0xd6, 0x6f, 0x60, 0xce, 0xd1, 0x1f, 0x0d, 0x58, - 0x18, 0x69, 0x59, 0xd0, 0xfa, 0x64, 0x2f, 0x46, 0x7b, 0x9a, 0x44, 0x47, 0xee, 0x4b, 0x47, 0xee, - 0x54, 0x6e, 0x27, 0x3a, 0x22, 0x5b, 0x9d, 0xda, 0x59, 0xd4, 0x01, 0x9d, 0xa3, 0x5f, 0x45, 0xa1, - 0xd7, 0xbb, 0x16, 0x25, 0xd8, 0x4e, 0xe4, 0x5c, 0x95, 0x9c, 0x1f, 0x56, 0x96, 0xe2, 0x9c, 0x4c, - 0x1b, 0xfb, 0xc6, 0x88, 0x7a, 0xbb, 0xa1, 0xa2, 0x80, 0x2a, 0x93, 0x17, 0x3a, 0xa9, 0x72, 0x24, - 0x12, 0x1f, 0x4b, 0xe2, 0x6e, 0xe5, 0xde, 0x04, 0xe2, 0xda, 0xd9, 0xa0, 0xb4, 0x9c, 0xd7, 0xce, - 0x8e, 0xc8, 0xe9, 0x79, 0xed, 0x4c, 0x57, 0x93, 0xf3, 0x37, 0x9f, 0x57, 0xb6, 0xde, 0x56, 0xa7, - 0x76, 0xa6, 0xeb, 0xc5, 0x39, 0x7a, 0x0d, 0x79, 0xe5, 0xed, 0x81, 0xba, 0xe8, 0xbc, 0x65, 0xbc, - 0x4c, 0xe9, 0x36, 0xaa, 0x2c, 0xc6, 0x5d, 0x10, 0x34, 0xe8, 0x4f, 0x06, 0xa0, 0xf1, 0xde, 0x0f, - 0xdd, 0x9e, 0x1c, 0xab, 0x09, 0xfd, 0xe1, 0x77, 0x48, 0x50, 0x75, 0x4f, 0xac, 0x9d, 0xf5, 0xdb, - 0xc9, 0x73, 0x14, 0x42, 0x41, 0x3d, 0x4c, 0x45, 0x9b, 0xf2, 0x82, 0xed, 0x7f, 0x3d, 0x61, 0x4a, - 0x19, 0xb0, 0x6e, 0x4a, 0xfe, 0x8f, 0xd1, 0x47, 0x89, 0xfc, 0x44, 0x3d, 0x81, 0xfd, 0x1a, 0x60, - 0x97, 0x4c, 0x43, 0x38, 0xe9, 0x69, 0x2c, 0xda, 0xf7, 0x28, 0x79, 0xdf, 0xbf, 0x86, 0xdc, 0x2e, - 0xe1, 0xd1, 0x73, 0x49, 0xe2, 0x97, 0x9b, 0xf8, 0x38, 0x62, 0x95, 0xa4, 0xf9, 0x65, 0x84, 0xe2, - 0xe6, 0xf5, 0x13, 0x0b, 0x91, 0x8e, 0x3f, 0xd5, 0xef, 0x66, 0xd3, 0x3a, 0xae, 0xf1, 0x53, 0xc4, - 0x47, 0x15, 0x0e, 0xe4, 0x49, 0xff, 0x77, 0xd5, 0x93, 0xdb, 0x05, 0x2c, 0x2b, 0xa3, 0xd7, 0x72, - 0xa9, 0x22, 0x8e, 0x76, 0x6b, 0x5d, 0x72, 0x95, 0xd1, 0xda, 0xc5, 0x35, 0x02, 0xfd, 0x52, 0x52, - 0xe9, 0x2b, 0x76, 0x52, 0xa8, 0x4a, 0xc9, 0xf7, 0xf5, 0xc9, 0xe1, 0x62, 0xca, 0xde, 0x09, 0x2c, - 0xec, 0x12, 0x3e, 0x94, 0xe6, 0x17, 0xac, 0xa6, 0x7c, 0xd1, 0xf5, 0x43, 0x72, 0x5d, 0x1e, 0x40, - 0x95, 0xd8, 0xe8, 0x08, 0xf2, 0xb1, 0x47, 0x11, 0x64, 0x8d, 0x5a, 0x1e, 0x7f, 0x31, 0x29, 0xad, - 0x8e, 0xaf, 0xb1, 0xff, 0x54, 0x11, 0x55, 0x3f, 0x34, 0xb1, 0xfa, 0x61, 0xc8, 0xf5, 0x9f, 0x54, - 0x50, 0x79, 0x12, 0x55, 0xfc, 0xb5, 0xa5, 0x64, 0x8e, 0xb5, 0x86, 0xba, 0xe9, 0x8a, 0x6a, 0x06, - 0x1a, 0xaf, 0x19, 0x87, 0x90, 0x3b, 0x08, 0x9a, 0xef, 0x7e, 0x3e, 0xeb, 0x6c, 0xb0, 0x92, 0xb3, - 0xa1, 0x27, 0xcc, 0xa3, 0x2f, 0x61, 0x5e, 0xbd, 0xa2, 0xec, 0xc8, 0x86, 0xf8, 0x5d, 0xa8, 0xaa, - 0x92, 0xea, 0x96, 0xb5, 0x7e, 0x01, 0x95, 0x60, 0xa8, 0xa9, 0x9e, 0x1b, 0x9d, 0x45, 0x94, 0xaa, - 0x73, 0x1d, 0x3f, 0xa2, 0x27, 0x3c, 0xeb, 0x7c, 0x77, 0x72, 0xd5, 0x69, 0x23, 0x0a, 0x79, 0x65, - 0xfe, 0x89, 0xec, 0xe4, 0xdf, 0x61, 0xb9, 0x1b, 0x92, 0xf1, 0xa6, 0xf5, 0xe9, 0x65, 0x8c, 0xea, - 0xad, 0xa0, 0x07, 0x45, 0x45, 0xf8, 0x14, 0x3b, 0xa4, 0x49, 0xe9, 0xd1, 0xbb, 0x70, 0xde, 0x93, - 0x9c, 0x15, 0xeb, 0xd6, 0x65, 0x9c, 0x87, 0x11, 0xc9, 0x29, 0x2c, 0x2a, 0xda, 0x5d, 0xdc, 0x21, - 0x3b, 0x24, 0xe0, 0xef, 0x96, 0x46, 0x75, 0x49, 0x7c, 0xd7, 0xaa, 0x5c, 0x46, 0xdc, 0xc2, 0x1d, - 0xe2, 0x28, 0x9a, 0x7e, 0x4a, 0xed, 0x4a, 0x93, 0xef, 0x35, 0xa5, 0x94, 0xfa, 0xe0, 0xab, 0xee, - 0x73, 0x82, 0x3b, 0xef, 0xf5, 0xab, 0x32, 0xc9, 0x40, 0xa1, 0x30, 0xf4, 0xbe, 0x33, 0x7e, 0xb1, - 0x9d, 0xf4, 0xfc, 0x73, 0xd9, 0xc5, 0xd6, 0x4a, 0x3e, 0xe0, 0xfe, 0x6c, 0x00, 0x1a, 0x6f, 0xc2, - 0xc6, 0xef, 0x10, 0x89, 0x8d, 0x5a, 0x22, 0xf7, 0x43, 0xc9, 0x5d, 0xb7, 0xde, 0xfa, 0xba, 0xb5, - 0xfd, 0x8f, 0xd4, 0xd7, 0x8f, 0xfe, 0x96, 0x42, 0xe7, 0xf0, 0xe1, 0x4b, 0xe9, 0x44, 0x59, 0x6b, - 0x97, 0x1f, 0xfd, 0x7c, 0xaf, 0x7c, 0x5c, 0xb7, 0x1a, 0xf0, 0xf1, 0xab, 0x36, 0x29, 0xeb, 0x49, - 0xd1, 0xd3, 0xd0, 0x90, 0x95, 0xd7, 0xcb, 0x3b, 0x34, 0xe0, 0xa1, 0xd7, 0xec, 0x71, 0x1a, 0x32, - 0x74, 0xa3, 0xcd, 0x79, 0x97, 0x6d, 0xd5, 0x6a, 0x17, 0xfd, 0xe5, 0xb1, 0xb4, 0xdc, 0x26, 0xbe, - 0x4f, 0x7f, 0x3c, 0x98, 0x10, 0xb8, 0xfa, 0x4c, 0xbd, 0x7a, 0xaf, 0x54, 0xbc, 0x5f, 0x7f, 0x50, - 0xbd, 0x57, 0xbd, 0x57, 0xbd, 0xbf, 0xf5, 0x60, 0xf3, 0x7b, 0xf7, 0x2b, 0x86, 0x51, 0x5f, 0xc4, - 0xdd, 0xae, 0xaf, 0xff, 0xf2, 0x53, 0xfb, 0x82, 0xd1, 0x60, 0x6b, 0x4c, 0xf2, 0xe6, 0x0a, 0x2c, - 0x40, 0x6e, 0x1b, 0x33, 0xcf, 0x11, 0x8e, 0xa1, 0x54, 0xd6, 0x68, 0x2e, 0x40, 0x21, 0x2e, 0xfa, - 0x20, 0xdc, 0x86, 0x4f, 0xb4, 0xf3, 0x8c, 0x84, 0xc7, 0x24, 0xec, 0x2f, 0xd0, 0xa5, 0x4e, 0xaf, - 0x43, 0x02, 0xf5, 0x57, 0x46, 0xb4, 0x1a, 0x2d, 0x61, 0xd8, 0xbd, 0x9a, 0x4b, 0x1d, 0xf6, 0x26, - 0xa3, 0x75, 0x9a, 0x73, 0x32, 0xee, 0x9b, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x5c, 0xa6, 0x55, - 0xf6, 0x8a, 0x1d, 0x00, 0x00, + 0xd1, 0xf6, 0x92, 0x02, 0x01, 0x34, 0x08, 0x90, 0x1a, 0xd1, 0x7a, 0x97, 0xa0, 0x44, 0xc1, 0x6b, + 0x99, 0x96, 0x60, 0x13, 0x90, 0xc0, 0x37, 0x91, 0xcc, 0xb8, 0x92, 0x88, 0x94, 0x44, 0xb1, 0x22, + 0x29, 0xa9, 0xa5, 0x15, 0x55, 0x39, 0xa9, 0xa0, 0x06, 0xbb, 0x43, 0x60, 0xcd, 0xc5, 0x0e, 0x3c, + 0x33, 0x20, 0xc3, 0xb0, 0x58, 0x95, 0xaf, 0x63, 0x2e, 0x71, 0x0e, 0xb9, 0xa5, 0x2a, 0xe7, 0xfc, + 0x8b, 0xfc, 0x03, 0x57, 0x4e, 0xbe, 0xe7, 0x96, 0xbf, 0x90, 0x43, 0x6a, 0x3e, 0x16, 0x58, 0x7c, + 0x2c, 0x09, 0xcb, 0xa5, 0x83, 0x4a, 0x98, 0x99, 0xa7, 0xfb, 0xe9, 0xe9, 0xed, 0xee, 0x99, 0x1e, + 0xc2, 0xbb, 0x1e, 0x8d, 0x38, 0x0d, 0x49, 0xdd, 0xfc, 0x5f, 0xeb, 0x31, 0x2a, 0x28, 0x2a, 0x45, + 0xf8, 0x08, 0x77, 0x71, 0xcd, 0xcc, 0x96, 0xab, 0xed, 0x40, 0x74, 0xfa, 0xad, 0x9a, 0x47, 0xbb, + 0xf5, 0x0e, 0x61, 0x34, 0xf0, 0x42, 0xdc, 0xe2, 0x75, 0x8d, 0xaa, 0xe3, 0x5e, 0x20, 0xff, 0x69, + 0xd9, 0xf2, 0x8d, 0x36, 0xa5, 0xed, 0x90, 0xe8, 0xd9, 0x28, 0xa2, 0x02, 0x8b, 0x80, 0x46, 0xdc, + 0xac, 0xae, 0x99, 0x55, 0x35, 0x6a, 0xf5, 0x0f, 0xeb, 0xa4, 0xdb, 0x13, 0xa7, 0x66, 0xf1, 0xd6, + 0xf8, 0xa2, 0x08, 0xba, 0x84, 0x0b, 0xdc, 0xed, 0x19, 0xc0, 0xfa, 0x38, 0xe0, 0x84, 0xe1, 0x5e, + 0x8f, 0xb0, 0x58, 0xfb, 0xc7, 0xea, 0x3f, 0x6f, 0xb3, 0x4d, 0xa2, 0x4d, 0x7e, 0x82, 0xdb, 0x6d, + 0xc2, 0xea, 0xb4, 0xa7, 0xf8, 0x27, 0x6d, 0x71, 0x8e, 0x60, 0xe5, 0x91, 0xe7, 0xd1, 0x7e, 0x24, + 0x1e, 0x93, 0x90, 0x08, 0xe2, 0x92, 0x2f, 0xfb, 0x84, 0x0b, 0x54, 0x82, 0xb9, 0xc0, 0xb7, 0xad, + 0x8a, 0x75, 0x27, 0xef, 0xce, 0x05, 0x3e, 0xda, 0x85, 0x25, 0x46, 0x3c, 0xca, 0xfc, 0xa6, 0x2f, + 0x71, 0x01, 0x8d, 0xec, 0xb9, 0x8a, 0x75, 0xa7, 0xd0, 0x28, 0xd7, 0xb4, 0x3d, 0xb5, 0xd8, 0x9e, + 0xda, 0x0e, 0xa5, 0xe1, 0xcf, 0x71, 0xd8, 0x27, 0x6e, 0x49, 0x8b, 0x3c, 0x36, 0x12, 0xce, 0x37, + 0xf3, 0x50, 0x34, 0x6c, 0x4f, 0x7e, 0xdd, 0xa3, 0x4c, 0xa0, 0x4d, 0xc8, 0x62, 0x3d, 0xa1, 0xb8, + 0x0a, 0x8d, 0x6b, 0x35, 0xe3, 0x76, 0xe9, 0x4c, 0x83, 0x75, 0x63, 0x0c, 0xda, 0x82, 0x2c, 0x6d, + 0x7d, 0x41, 0x3c, 0xc1, 0xed, 0xb9, 0xca, 0xfc, 0x9d, 0x42, 0x63, 0x35, 0x09, 0x3f, 0x10, 0x94, + 0xe1, 0x36, 0xf9, 0xa9, 0x42, 0xb8, 0x31, 0x12, 0x7d, 0x0c, 0xd9, 0x43, 0x16, 0x90, 0xc8, 0xe7, + 0xf6, 0xbc, 0x12, 0x42, 0x49, 0xa1, 0xa7, 0x6a, 0xc9, 0x8d, 0x21, 0xe8, 0x2e, 0x2c, 0xb4, 0x19, + 0xed, 0xf7, 0xb8, 0x7d, 0x45, 0x81, 0xaf, 0x26, 0xc1, 0x7b, 0x72, 0xc5, 0x35, 0x00, 0xf4, 0x7d, + 0xc8, 0x75, 0x09, 0xe7, 0xb8, 0x4d, 0xb8, 0x9d, 0x51, 0xe0, 0x72, 0x12, 0xbc, 0xdb, 0xc1, 0x51, + 0x44, 0xc2, 0x17, 0x1a, 0xe2, 0x0e, 0xb0, 0xe8, 0x25, 0x5c, 0x0b, 0x09, 0xf6, 0x09, 0x6b, 0x51, + 0xcc, 0xfc, 0xa6, 0x76, 0x12, 0xb7, 0x17, 0x94, 0x8a, 0x9b, 0x49, 0x15, 0xcf, 0x87, 0x30, 0x57, + 0xa1, 0x5c, 0x14, 0x8e, 0x4f, 0x71, 0xf4, 0x43, 0x28, 0x46, 0x54, 0x04, 0x87, 0x81, 0xa7, 0x3f, + 0xad, 0x9d, 0x55, 0x9a, 0xec, 0xa4, 0xa6, 0x97, 0x09, 0x80, 0x3b, 0x0a, 0x47, 0xbb, 0x50, 0x3a, + 0xc1, 0x61, 0x48, 0x44, 0x33, 0x24, 0x7e, 0x9b, 0x30, 0x6e, 0xe7, 0x94, 0x82, 0x1b, 0xb5, 0xd1, + 0x14, 0xa8, 0xbd, 0x56, 0xa8, 0xe7, 0x0a, 0xe4, 0x16, 0x4f, 0x12, 0x23, 0xee, 0xac, 0x41, 0xde, + 0x7c, 0xae, 0x7d, 0x7f, 0x3c, 0x7a, 0x9c, 0x17, 0x70, 0xed, 0x51, 0x5f, 0x74, 0x48, 0x24, 0x24, + 0xe9, 0x20, 0xc8, 0xca, 0x90, 0xeb, 0x73, 0xc2, 0x22, 0xdc, 0x25, 0x06, 0x3c, 0x18, 0xcb, 0xb5, + 0x1e, 0xe6, 0xfc, 0x84, 0x32, 0x5f, 0x45, 0x5a, 0xde, 0x1d, 0x8c, 0x9d, 0xbf, 0x5a, 0xb0, 0xb0, + 0x4b, 0xa3, 0xc3, 0xa0, 0x8d, 0xae, 0xc3, 0x82, 0xa7, 0x7e, 0x19, 0x05, 0x66, 0x84, 0xb6, 0x21, + 0x77, 0x82, 0x59, 0x14, 0x44, 0xed, 0x38, 0x54, 0xd6, 0xc7, 0x77, 0xa3, 0x35, 0xd4, 0x5e, 0x6b, + 0x98, 0x3b, 0xc0, 0x97, 0x3f, 0x81, 0xac, 0x99, 0x44, 0x2b, 0x90, 0x39, 0x0c, 0x48, 0x18, 0xef, + 0x45, 0x0f, 0x90, 0x0d, 0x59, 0xf3, 0x31, 0x8d, 0x69, 0xf1, 0xd0, 0xd9, 0x80, 0xd2, 0xae, 0x56, + 0x7f, 0x40, 0x38, 0x0f, 0x68, 0x24, 0x35, 0x08, 0x7a, 0x44, 0xa2, 0x58, 0x83, 0x1a, 0x38, 0x3b, + 0x70, 0x4d, 0xe7, 0x9b, 0x09, 0xbf, 0x94, 0xac, 0x5b, 0x83, 0xbc, 0x8e, 0xcb, 0x66, 0x30, 0xf0, + 0x82, 0x9e, 0xd8, 0xf7, 0x9d, 0x5d, 0xb8, 0xae, 0x75, 0xa8, 0xa8, 0x7c, 0xc5, 0x09, 0x4b, 0x53, + 0xb3, 0x0a, 0x39, 0x15, 0xb2, 0x43, 0x2d, 0x59, 0x35, 0xde, 0xf7, 0x9d, 0xdf, 0x59, 0x50, 0xd6, + 0x5a, 0x46, 0xb3, 0xc7, 0x68, 0x5a, 0x07, 0xf0, 0x68, 0x18, 0x12, 0x4f, 0x65, 0xbc, 0xd6, 0x98, + 0x98, 0x41, 0xcb, 0x30, 0x7f, 0x44, 0x4e, 0x8d, 0x52, 0xf9, 0x13, 0xfd, 0x1f, 0x64, 0xe5, 0x37, + 0x94, 0x54, 0xf3, 0xfa, 0x8b, 0xc8, 0xe1, 0xbe, 0x72, 0xda, 0x31, 0x61, 0xd2, 0x27, 0xf6, 0x15, + 0x6d, 0x83, 0x19, 0x3a, 0xcf, 0x60, 0x55, 0x9b, 0x30, 0x12, 0x5f, 0xe9, 0x2e, 0x31, 0xc1, 0x3a, + 0x74, 0x89, 0x9e, 0xd8, 0xf7, 0x9d, 0x4d, 0x40, 0xcf, 0x03, 0x2e, 0xcc, 0x56, 0x62, 0x15, 0x09, + 0x93, 0xac, 0xa4, 0x49, 0x4e, 0x0b, 0x96, 0x25, 0x5c, 0xba, 0x8e, 0xc7, 0xe0, 0xeb, 0xb0, 0x70, + 0x18, 0x84, 0x82, 0xb0, 0x18, 0xab, 0x47, 0x72, 0xbe, 0x25, 0xf3, 0x59, 0x93, 0xe6, 0x5c, 0x33, + 0x92, 0x1e, 0x12, 0xb4, 0xdb, 0xe2, 0x82, 0x46, 0x84, 0xab, 0x2d, 0xe7, 0xdc, 0xc4, 0x8c, 0x73, + 0x06, 0x05, 0x63, 0x8e, 0xa4, 0x4a, 0x56, 0x30, 0x6b, 0xe6, 0x0a, 0x76, 0x0b, 0x0a, 0x82, 0x0a, + 0x1c, 0x36, 0x75, 0xa5, 0x94, 0x06, 0x64, 0x24, 0x89, 0xc0, 0xe1, 0xae, 0xaa, 0x8b, 0x32, 0x0b, + 0xfa, 0x8c, 0x53, 0xa6, 0x0c, 0x58, 0x74, 0xcd, 0x48, 0x86, 0xd9, 0xab, 0x28, 0x0c, 0xa2, 0xa3, + 0xc7, 0xe4, 0x38, 0xf0, 0xc8, 0x05, 0x3e, 0xf5, 0x15, 0x20, 0xe1, 0x53, 0x3d, 0xb1, 0xef, 0x3b, + 0xff, 0xcd, 0xc0, 0xca, 0xab, 0x9e, 0x8f, 0x05, 0x89, 0xcb, 0x71, 0x8a, 0x96, 0x87, 0x89, 0x6c, + 0xd6, 0x67, 0xc3, 0x8d, 0x89, 0xb3, 0xe1, 0x40, 0xb0, 0x20, 0x6a, 0xeb, 0xd3, 0x61, 0x98, 0xeb, + 0x3f, 0x82, 0x45, 0x3f, 0xe0, 0xbd, 0x10, 0x9f, 0x36, 0x95, 0xf4, 0xfc, 0x0c, 0xd2, 0x05, 0x23, + 0xf1, 0x52, 0x2a, 0x78, 0x28, 0x2b, 0xb1, 0xc0, 0x3e, 0x16, 0x58, 0x05, 0xd7, 0xa5, 0xd4, 0x31, + 0x1a, 0xfd, 0x00, 0x00, 0x1f, 0x63, 0x81, 0x59, 0xb3, 0xcf, 0x42, 0x3b, 0x33, 0x83, 0x6c, 0x5e, + 0xe3, 0x5f, 0xb1, 0x10, 0x3d, 0x80, 0x5c, 0x88, 0xa3, 0x76, 0x53, 0xe0, 0xb6, 0xbd, 0x30, 0x83, + 0x68, 0x56, 0xa2, 0x3f, 0xc3, 0x6d, 0x69, 0x6f, 0x48, 0x75, 0xf9, 0xb5, 0xb3, 0xb3, 0xd8, 0x1b, + 0xa3, 0xa5, 0xa4, 0xbc, 0x10, 0xfc, 0x86, 0x46, 0xc4, 0xce, 0xcd, 0x22, 0x19, 0xa3, 0xd1, 0x27, + 0x90, 0xf7, 0xfa, 0x5c, 0xd0, 0xae, 0xfc, 0xc8, 0xf9, 0x59, 0x44, 0x35, 0x7c, 0xdf, 0x47, 0x0d, + 0xc8, 0x90, 0x2e, 0x0e, 0x42, 0x1b, 0x66, 0x10, 0xd3, 0x50, 0xe4, 0x02, 0x0c, 0x62, 0x8a, 0xdb, + 0x05, 0x15, 0xeb, 0x5b, 0xe3, 0x25, 0x78, 0x5a, 0x5c, 0xd5, 0x1e, 0x9b, 0xc8, 0xe3, 0x4f, 0x22, + 0xc1, 0x4e, 0xdd, 0x7c, 0x1c, 0x89, 0x1c, 0xfd, 0x3f, 0x2c, 0xe8, 0x54, 0xb7, 0x17, 0x67, 0x30, + 0xc4, 0x60, 0xcb, 0x9f, 0x42, 0x69, 0x54, 0x65, 0x5c, 0xb5, 0xac, 0x61, 0xd5, 0x5a, 0x81, 0xcc, + 0xb1, 0x14, 0x32, 0xd1, 0xaf, 0x07, 0xdb, 0x73, 0x0f, 0x2d, 0xe7, 0x00, 0x72, 0xb2, 0x3e, 0xa8, + 0xe4, 0xdd, 0x80, 0x8c, 0x8c, 0xd9, 0x38, 0x75, 0x97, 0x93, 0xa9, 0xab, 0xea, 0xaf, 0x5e, 0xbe, + 0x34, 0x5f, 0x9d, 0xbf, 0x67, 0x00, 0x0e, 0x04, 0x16, 0x7d, 0xae, 0xf4, 0x3e, 0x80, 0x4c, 0x44, + 0x7d, 0x12, 0xeb, 0x7d, 0x6f, 0xdc, 0x4d, 0x43, 0xa8, 0xf9, 0xe9, 0x6a, 0x7c, 0xf9, 0x9b, 0x2b, + 0xb0, 0xa0, 0x67, 0x10, 0x82, 0x2b, 0x89, 0x73, 0x54, 0xfd, 0x46, 0xef, 0x43, 0x91, 0xeb, 0x63, + 0x68, 0xc4, 0x92, 0x45, 0x33, 0xa9, 0x6b, 0xc7, 0x07, 0x50, 0xea, 0x31, 0xc2, 0x49, 0xe4, 0x11, + 0x83, 0x9a, 0x57, 0xa8, 0x62, 0x3c, 0xab, 0x61, 0xb7, 0xa0, 0xd0, 0xc5, 0xc2, 0xeb, 0x18, 0xcc, + 0x15, 0xbd, 0x27, 0x35, 0xa5, 0x01, 0x1f, 0xc2, 0x52, 0x9b, 0x32, 0xda, 0x17, 0x41, 0x14, 0x2b, + 0xca, 0x28, 0x50, 0x69, 0x30, 0xad, 0x81, 0xb7, 0xa1, 0x84, 0x8f, 0xdb, 0xcd, 0x10, 0x0b, 0x12, + 0x79, 0xa7, 0xcd, 0x2e, 0x57, 0xb9, 0x63, 0xb9, 0x8b, 0xf8, 0xb8, 0xfd, 0x5c, 0x4f, 0xbe, 0xe0, + 0xe8, 0x23, 0x40, 0xa3, 0xa8, 0x66, 0x37, 0xd0, 0xc9, 0x62, 0xb9, 0x4b, 0x49, 0xe4, 0x8b, 0x20, + 0x42, 0x77, 0xe1, 0xea, 0x18, 0xb8, 0xc3, 0x54, 0x7a, 0x58, 0x6e, 0x29, 0x89, 0x7d, 0xc6, 0xe4, + 0x59, 0xc8, 0xb0, 0x20, 0x4d, 0x4e, 0x3c, 0x95, 0x05, 0x96, 0x9b, 0x95, 0xe3, 0x03, 0xe2, 0xa1, + 0x0a, 0x2c, 0xc6, 0x4b, 0x8a, 0x0c, 0xd4, 0x32, 0x98, 0x65, 0xc9, 0xb3, 0x0e, 0x85, 0x01, 0xa2, + 0xc3, 0xec, 0x82, 0x02, 0xe4, 0x0d, 0xe0, 0x19, 0x93, 0x85, 0x34, 0x88, 0x7a, 0x7d, 0xd1, 0x3c, + 0x6a, 0x71, 0x15, 0xa3, 0x96, 0x9b, 0x53, 0x13, 0x3f, 0x69, 0x71, 0xe4, 0x40, 0x71, 0xb0, 0xa8, + 0xf4, 0x17, 0x15, 0xa0, 0x10, 0x03, 0x24, 0x41, 0x05, 0x16, 0x87, 0x98, 0x0e, 0xb3, 0x4b, 0xda, + 0x84, 0x18, 0xf2, 0x8c, 0xa1, 0x9b, 0x00, 0xb4, 0x2f, 0x62, 0x8e, 0x25, 0x6d, 0x81, 0x9e, 0x91, + 0x24, 0xb7, 0xa1, 0x34, 0x5c, 0x56, 0x2c, 0xcb, 0xda, 0xb9, 0x03, 0x88, 0xa4, 0x71, 0xa0, 0x98, + 0x40, 0x75, 0x84, 0x7d, 0x55, 0x9b, 0x32, 0x00, 0x3d, 0x13, 0xce, 0x7f, 0x2c, 0x58, 0x4c, 0x1e, + 0xc8, 0x13, 0xf5, 0x3e, 0x71, 0xac, 0xce, 0x8d, 0x9c, 0xf4, 0x37, 0x20, 0xef, 0x75, 0x70, 0xd4, + 0x26, 0x9c, 0x08, 0x73, 0x09, 0x18, 0x4e, 0xc8, 0x8b, 0xdd, 0x48, 0xad, 0xce, 0x8f, 0x54, 0xe3, + 0x82, 0xc7, 0x88, 0xf4, 0xb0, 0x2c, 0x5b, 0xa6, 0x1c, 0x4f, 0x76, 0x18, 0x9f, 0xc5, 0x2d, 0x91, + 0x0b, 0x1a, 0x2e, 0x27, 0xa4, 0x70, 0x5f, 0xd5, 0x13, 0x2d, 0xbc, 0x70, 0xb9, 0xb0, 0x86, 0xcb, + 0x09, 0xe7, 0x29, 0x2c, 0x27, 0x37, 0xab, 0xd2, 0xb2, 0x01, 0x99, 0x40, 0x90, 0x6e, 0x9c, 0x96, + 0x17, 0x5f, 0x87, 0x35, 0xd4, 0xf9, 0xe7, 0x1c, 0xac, 0xbe, 0x66, 0xc1, 0xdb, 0xbf, 0x4e, 0x35, + 0xe2, 0x8a, 0x35, 0xcb, 0x79, 0xa7, 0xa1, 0xc9, 0x2b, 0x58, 0x66, 0xe4, 0x0a, 0x86, 0x1e, 0xc3, + 0x52, 0x8f, 0xb0, 0x6e, 0xa0, 0x8b, 0x05, 0x23, 0xd8, 0x37, 0xfe, 0x5b, 0x9b, 0xd0, 0xbb, 0x1f, + 0x89, 0xad, 0x86, 0xe9, 0xef, 0x86, 0x32, 0x2e, 0xc1, 0x3e, 0x7a, 0x0a, 0xcb, 0x09, 0x2d, 0x27, + 0xd2, 0x0d, 0xe6, 0x78, 0xbb, 0x50, 0x4d, 0x82, 0x5a, 0xb9, 0xae, 0xf1, 0xf5, 0x2a, 0x64, 0xcd, + 0x35, 0x1a, 0xfd, 0xc1, 0x82, 0xc5, 0x64, 0xef, 0x80, 0xde, 0x1f, 0xff, 0x0c, 0x53, 0x3a, 0x8b, + 0xf2, 0xb4, 0xcb, 0x7e, 0xe2, 0x56, 0xee, 0xd4, 0xbe, 0x7a, 0x94, 0x6b, 0x2d, 0xc0, 0x15, 0x78, + 0x07, 0xbd, 0xf3, 0xfb, 0x7f, 0xfd, 0xfb, 0x2f, 0x73, 0x37, 0x1d, 0xbb, 0x7e, 0xdc, 0x88, 0x9f, + 0x00, 0xea, 0x38, 0xa1, 0x73, 0xdb, 0xaa, 0xa2, 0x16, 0x64, 0x77, 0x70, 0x24, 0x6b, 0x3c, 0x5a, + 0x9d, 0xe0, 0x8f, 0xdb, 0x9e, 0xf2, 0xf5, 0x89, 0x5d, 0x3e, 0x91, 0x9d, 0xbd, 0x73, 0x5b, 0x51, + 0xac, 0x3b, 0x37, 0x46, 0x28, 0xb4, 0x58, 0xfd, 0x2c, 0xf0, 0xcf, 0xeb, 0x2d, 0x1c, 0x21, 0x0a, + 0x45, 0x7d, 0x0d, 0x36, 0x0a, 0xd1, 0xed, 0x14, 0xa6, 0x91, 0x4e, 0x3d, 0x95, 0xb4, 0xa2, 0x48, + 0xcb, 0x55, 0x3b, 0x8d, 0x14, 0xfd, 0xd6, 0x82, 0xc5, 0x64, 0x17, 0x32, 0xe9, 0xda, 0x29, 0x3d, + 0x4a, 0x2a, 0xdf, 0x96, 0xe2, 0xdb, 0xac, 0x7e, 0x94, 0xba, 0x49, 0xdd, 0xb9, 0xd4, 0xcf, 0x06, + 0x2d, 0xcd, 0x39, 0xfa, 0xa3, 0x05, 0x4b, 0x63, 0x4d, 0x0c, 0xda, 0x98, 0x6e, 0xc5, 0x78, 0x97, + 0x93, 0x6a, 0xc8, 0x7d, 0x65, 0xc8, 0x47, 0xd5, 0xbb, 0xa9, 0x86, 0xa8, 0xe6, 0xa7, 0x7e, 0x16, + 0xf7, 0x44, 0xe7, 0xe8, 0x97, 0xb1, 0xeb, 0x4d, 0xd6, 0xa2, 0x14, 0xdd, 0xa9, 0x9c, 0x6b, 0x8a, + 0xf3, 0xdd, 0xea, 0xb5, 0x24, 0x27, 0x37, 0xca, 0xbe, 0xb6, 0xe2, 0x6e, 0x6f, 0xa4, 0x28, 0xa0, + 0xea, 0xf4, 0x8d, 0x4e, 0xab, 0x1c, 0xa9, 0xc4, 0xc7, 0x8a, 0xb8, 0x57, 0xbd, 0x37, 0x85, 0xb8, + 0x7e, 0x36, 0x2c, 0x2d, 0xe7, 0xf5, 0xb3, 0x23, 0x72, 0x7a, 0x5e, 0x3f, 0x33, 0xd5, 0xe4, 0xfc, + 0xf3, 0x4f, 0xab, 0xdb, 0xdf, 0x56, 0xa6, 0x7e, 0x66, 0xea, 0xc5, 0x39, 0x7a, 0x0d, 0x05, 0x6d, + 0xad, 0x6a, 0x9e, 0xbe, 0xb5, 0xbf, 0x6c, 0x65, 0x36, 0xaa, 0x2e, 0x27, 0x4d, 0x90, 0x34, 0xe8, + 0x4f, 0x16, 0xa0, 0xc9, 0x6e, 0x10, 0xdd, 0x9d, 0xee, 0xab, 0x29, 0x1d, 0xe3, 0x77, 0x08, 0x50, + 0x7d, 0x61, 0xac, 0x9f, 0x0d, 0x1a, 0xcc, 0x73, 0xc4, 0xa0, 0xa8, 0x9f, 0xaa, 0xe2, 0xa4, 0xbc, + 0x20, 0xfd, 0x6f, 0xa6, 0x2c, 0x69, 0x05, 0xce, 0x87, 0x8a, 0xff, 0x3d, 0x74, 0x2b, 0x95, 0x9f, + 0xe8, 0x47, 0xb1, 0x5f, 0x01, 0xec, 0x91, 0x59, 0x08, 0xa7, 0x3d, 0x96, 0xc5, 0x79, 0x8f, 0xd2, + 0xf3, 0xfe, 0x35, 0xe4, 0xf7, 0x88, 0x88, 0x1f, 0x50, 0x52, 0xbf, 0xdc, 0xd4, 0xe7, 0x12, 0xa7, + 0xac, 0xd4, 0xaf, 0x20, 0x94, 0x54, 0x6f, 0x1e, 0x5d, 0x88, 0x32, 0xfc, 0xa9, 0x79, 0x49, 0x9b, + 0xd5, 0x70, 0x83, 0x9f, 0xc1, 0x3f, 0xba, 0x70, 0xa0, 0x40, 0xd9, 0xbf, 0xa7, 0x1f, 0xe1, 0x2e, + 0x60, 0x59, 0x1d, 0xbf, 0x9f, 0x2b, 0x11, 0x79, 0xb4, 0x3b, 0x1b, 0x8a, 0xab, 0x82, 0xd6, 0x2f, + 0xae, 0x11, 0xe8, 0x17, 0x8a, 0xca, 0x5c, 0xb1, 0xd3, 0x5c, 0x55, 0x4e, 0xbf, 0xaf, 0x4f, 0x77, + 0x17, 0xd7, 0xfa, 0x4e, 0x60, 0x69, 0x8f, 0x88, 0x91, 0x30, 0xbf, 0x60, 0x37, 0x95, 0x8b, 0xae, + 0x1f, 0x8a, 0xeb, 0x72, 0x07, 0xea, 0xc0, 0x46, 0x47, 0x50, 0x48, 0x3c, 0x93, 0x20, 0x67, 0x5c, + 0xf3, 0xe4, 0x1b, 0x4a, 0x79, 0x6d, 0x72, 0x8f, 0x83, 0x47, 0x8d, 0xb8, 0xfa, 0xa1, 0xa9, 0xd5, + 0x0f, 0x43, 0x7e, 0xf0, 0xc8, 0x82, 0x2a, 0xd3, 0xa8, 0x92, 0xef, 0x2f, 0x65, 0x7b, 0xa2, 0x47, + 0x34, 0xdd, 0x57, 0x5c, 0x33, 0xd0, 0x64, 0xcd, 0x38, 0x84, 0xfc, 0xab, 0xa8, 0xf5, 0xe6, 0xe7, + 0xb3, 0x89, 0x06, 0x27, 0x3d, 0x1a, 0xfa, 0x52, 0x3d, 0xfa, 0x12, 0x16, 0xf5, 0x73, 0xca, 0xae, + 0xea, 0x8c, 0xdf, 0x84, 0xaa, 0xa6, 0xa8, 0xee, 0x38, 0x1b, 0x17, 0x50, 0x49, 0x86, 0xba, 0x6e, + 0xbe, 0xd1, 0x59, 0x4c, 0xa9, 0x5b, 0xd8, 0xc9, 0x23, 0x7a, 0xca, 0xfb, 0xce, 0x77, 0x27, 0xd7, + 0x2d, 0x37, 0xa2, 0x50, 0xd0, 0xea, 0x9f, 0xa8, 0x96, 0xfe, 0x0d, 0xb6, 0xbb, 0xa9, 0x18, 0x3f, + 0x74, 0x3e, 0xb8, 0x8c, 0x51, 0x3f, 0x1a, 0xf4, 0xa1, 0xa4, 0x09, 0x9f, 0x62, 0x8f, 0xb4, 0x28, + 0x3d, 0x7a, 0x13, 0xce, 0x7b, 0x8a, 0xb3, 0xea, 0xdc, 0xb9, 0x8c, 0xf3, 0x30, 0x26, 0x39, 0x85, + 0x65, 0x4d, 0xbb, 0x87, 0xbb, 0x64, 0x97, 0x44, 0xe2, 0xcd, 0xc2, 0xa8, 0xa1, 0x88, 0x3f, 0x76, + 0xaa, 0x97, 0x11, 0xb7, 0x71, 0x97, 0x78, 0x9a, 0x66, 0x10, 0x52, 0x7b, 0x4a, 0xe5, 0x5b, 0x0d, + 0x29, 0x2d, 0x3e, 0xfc, 0xaa, 0x07, 0x82, 0xe0, 0xee, 0x5b, 0xfd, 0xaa, 0x5c, 0x31, 0x50, 0x28, + 0x8e, 0x3c, 0xf4, 0x4c, 0x5e, 0x6c, 0xa7, 0xbd, 0x03, 0x5d, 0x76, 0xb1, 0x75, 0xd2, 0x0f, 0xb8, + 0x3f, 0x5b, 0x80, 0x26, 0x9b, 0xb0, 0xc9, 0x3b, 0x44, 0x6a, 0xa3, 0x96, 0xca, 0xfd, 0x50, 0x71, + 0x37, 0x9c, 0x6f, 0x7d, 0xdd, 0xda, 0xf9, 0xc7, 0xdc, 0x57, 0x8f, 0xfe, 0x36, 0x87, 0xce, 0xe1, + 0xdd, 0x97, 0xca, 0x88, 0x8a, 0x91, 0xae, 0x3c, 0xfa, 0xd9, 0x7e, 0xe5, 0xb8, 0xe1, 0x34, 0xe1, + 0xbd, 0xcf, 0x3a, 0xa4, 0x62, 0x16, 0x65, 0x4f, 0x43, 0x19, 0xaf, 0x6c, 0x54, 0x76, 0x69, 0x24, + 0x58, 0xd0, 0xea, 0x0b, 0xca, 0x38, 0xba, 0xdd, 0x11, 0xa2, 0xc7, 0xb7, 0xeb, 0xf5, 0x8b, 0xfe, + 0x16, 0x59, 0x5e, 0xe9, 0x90, 0x30, 0xa4, 0x3f, 0x1e, 0x2e, 0x48, 0x5c, 0x63, 0xbe, 0x51, 0xbb, + 0x57, 0x2e, 0xdd, 0x6f, 0x3c, 0xa8, 0xdd, 0xab, 0xdd, 0xab, 0xdd, 0xdf, 0x7e, 0xb0, 0xf5, 0xbd, + 0xfb, 0x55, 0xcb, 0x6a, 0x2c, 0xe3, 0x5e, 0x2f, 0x34, 0x7f, 0x0b, 0xaa, 0x7f, 0xc1, 0x69, 0xb4, + 0x3d, 0x31, 0xf3, 0xf9, 0x55, 0x58, 0x82, 0xfc, 0x0e, 0xe6, 0x81, 0x27, 0x0d, 0x43, 0x73, 0x39, + 0xab, 0xb5, 0x04, 0xc5, 0xe4, 0xd4, 0x3b, 0x6c, 0x07, 0xde, 0x37, 0xc6, 0x73, 0xc2, 0x8e, 0x09, + 0x1b, 0x6c, 0xd0, 0xa7, 0x5e, 0xbf, 0x4b, 0x22, 0xfd, 0x77, 0x47, 0xb4, 0x16, 0x6f, 0x61, 0xd4, + 0xbc, 0xba, 0x4f, 0x3d, 0xfe, 0x79, 0xd6, 0xc8, 0xb4, 0x16, 0x94, 0xdf, 0xb7, 0xfe, 0x17, 0x00, + 0x00, 0xff, 0xff, 0xc5, 0xd4, 0x26, 0xd7, 0x9c, 0x1d, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/console/console.proto b/console/console.proto index 6e57821bb..1d76087a7 100644 --- a/console/console.proto +++ b/console/console.proto @@ -335,16 +335,16 @@ message ListUsersRequest { bool banned = 2; // Search only recorded deletes. bool tombstones = 3; - // An (optional) cursor to fetch next page. - bytes cursor = 4; } // List of storage objects. message StorageList { // List of storage objects matching list/filter operation. repeated nakama.api.StorageObject objects = 1; + // Approximate total number of storage objects. + int32 total_count = 2; // An (optional) cursor for paging results. - bytes cursor = 2; + bytes cursor = 3; } // Unlink a particular device ID from a user's account. @@ -387,8 +387,8 @@ message UpdateAccountRequest { message UserList { // A list of users. repeated nakama.api.User users = 1; - // A cursor to fetch more results. - bytes cursor = 2; + // Approximate total number of users. + int32 total_count = 2; } // List of nodes and their stats. diff --git a/console/console.swagger.json b/console/console.swagger.json index fefdb98c3..8e7f7a5a5 100644 --- a/console/console.swagger.json +++ b/console/console.swagger.json @@ -813,14 +813,6 @@ "required": false, "type": "boolean", "format": "boolean" - }, - { - "name": "cursor", - "description": "An (optional) cursor to fetch next page.", - "in": "query", - "required": false, - "type": "string", - "format": "byte" } ], "tags": [ @@ -1496,6 +1488,11 @@ }, "description": "List of storage objects matching list/filter operation." }, + "total_count": { + "type": "integer", + "format": "int32", + "description": "Approximate total number of storage objects." + }, "cursor": { "type": "string", "format": "byte", @@ -1514,10 +1511,10 @@ }, "description": "A list of users." }, - "cursor": { - "type": "string", - "format": "byte", - "description": "A cursor to fetch more results." + "total_count": { + "type": "integer", + "format": "int32", + "description": "Approximate total number of users." } }, "description": "A list of users." diff --git a/main.go b/main.go index ebec076d6..5e6833e7a 100644 --- a/main.go +++ b/main.go @@ -118,7 +118,7 @@ func main() { pipeline := server.NewPipeline(logger, config, db, jsonpbMarshaler, jsonpbUnmarshaler, sessionRegistry, matchRegistry, matchmaker, tracker, router, runtime) metrics := server.NewMetrics(logger, startupLogger, config) - consoleServer := server.StartConsoleServer(logger, startupLogger, config, db, configWarnings) + consoleServer := server.StartConsoleServer(logger, startupLogger, db, config, tracker, configWarnings) apiServer := server.StartApiServer(logger, startupLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, leaderboardCache, leaderboardRankCache, sessionRegistry, matchRegistry, matchmaker, tracker, router, pipeline, runtime) gaenabled := len(os.Getenv("NAKAMA_TELEMETRY")) < 1 diff --git a/server/api.go b/server/api.go index 8c0489b2a..10a247aac 100644 --- a/server/api.go +++ b/server/api.go @@ -124,7 +124,7 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, j apigrpc.RegisterNakamaServer(grpcServer, s) startupLogger.Info("Starting API server for gRPC requests", zap.Int("port", config.GetSocket().Port-1)) go func() { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.GetSocket().Port-1)) + listener, err := net.Listen("tcp", fmt.Sprintf("%v:%d", config.GetSocket().Address, config.GetSocket().Port-1)) if err != nil { startupLogger.Fatal("API server listener failed to start", zap.Error(err)) } @@ -157,6 +157,9 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, j }), ) dialAddr := fmt.Sprintf("127.0.0.1:%d", config.GetSocket().Port-1) + if config.GetSocket().Address != "" { + dialAddr = fmt.Sprintf("%v:%d", config.GetSocket().Address, config.GetSocket().Port-1) + } dialOpts := []grpc.DialOption{ //TODO (mo, zyro): Do we need to pass the statsHandler here as well? grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(int(config.GetSocket().MaxMessageSizeBytes))), @@ -230,8 +233,14 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, j startupLogger.Fatal("API server gateway listener failed to start", zap.Error(err)) } - if err := s.grpcGatewayServer.Serve(listener); err != nil && err != http.ErrServerClosed { - startupLogger.Fatal("API server gateway listener failed", zap.Error(err)) + if config.GetSocket().TLSCert != nil { + if err := s.grpcGatewayServer.ServeTLS(listener, "", ""); err != nil && err != http.ErrServerClosed { + startupLogger.Fatal("API server gateway listener failed", zap.Error(err)) + } + } else { + if err := s.grpcGatewayServer.Serve(listener); err != nil && err != http.ErrServerClosed { + startupLogger.Fatal("API server gateway listener failed", zap.Error(err)) + } } }() diff --git a/server/config.go b/server/config.go index 44ae84d27..0cca8b5e4 100644 --- a/server/config.go +++ b/server/config.go @@ -15,6 +15,8 @@ package server import ( + "fmt" + "net/url" "os" "path/filepath" "strings" @@ -46,6 +48,8 @@ type Config interface { GetTracker() *TrackerConfig GetConsole() *ConsoleConfig GetLeaderboard() *LeaderboardConfig + + Clone() (Config, error) } func ParseArgs(logger *zap.Logger, args []string) Config { @@ -160,6 +164,15 @@ func CheckConfig(logger *zap.Logger, config Config) map[string]string { if config.GetSocket().PingPeriodMs >= config.GetSocket().PongWaitMs { logger.Fatal("Ping period value must be less than pong wait value", zap.Int("socket.ping_period_ms", config.GetSocket().PingPeriodMs), zap.Int("socket.pong_wait_ms", config.GetSocket().PongWaitMs)) } + if len(config.GetDatabase().Addresses) < 1 { + logger.Fatal("At least one database address must be specified", zap.Strings("database.address", config.GetDatabase().Addresses)) + } + for _, address := range config.GetDatabase().Addresses { + rawUrl := fmt.Sprintf("postgresql://%s", address) + if _, err := url.Parse(rawUrl); err != nil { + logger.Fatal("Bad database connection URL", zap.String("database.address", address), zap.Error(err)) + } + } if config.GetRuntime().MinCount < 0 { logger.Fatal("Minimum runtime instance count must be >= 0", zap.Int("runtime.min_count", config.GetRuntime().MinCount)) } @@ -236,13 +249,23 @@ func CheckConfig(logger *zap.Logger, config Config) map[string]string { } if config.GetSocket().SSLCertificate != "" && config.GetSocket().SSLPrivateKey != "" { logger.Warn("WARNING: enabling direct SSL termination is not recommended, use an SSL-capable proxy or load balancer for production!") - cert, err := tls.LoadX509KeyPair(config.GetSocket().SSLCertificate, config.GetSocket().SSLPrivateKey) + certPEMBlock, err := ioutil.ReadFile(config.GetSocket().SSLCertificate) + if err != nil { + logger.Fatal("Error loading SSL certificate cert file", zap.Error(err)) + } + keyPEMBlock, err := ioutil.ReadFile(config.GetSocket().SSLPrivateKey) + if err != nil { + logger.Fatal("Error loading SSL certificate key file", zap.Error(err)) + } + cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) if err != nil { logger.Fatal("Error loading SSL certificate", zap.Error(err)) } configWarnings["socket.ssl_certificate"] = "Enabling direct SSL termination is not recommended, use an SSL-capable proxy or load balancer for production!" configWarnings["socket.ssl_private_key"] = "Enabling direct SSL termination is not recommended, use an SSL-capable proxy or load balancer for production!" logger.Info("SSL mode enabled") + config.GetSocket().CertPEMBlock = certPEMBlock + config.GetSocket().KeyPEMBlock = keyPEMBlock config.GetSocket().TLSCert = []tls.Certificate{cert} } @@ -312,6 +335,59 @@ func NewConfig(logger *zap.Logger) *config { } } +func (c *config) Clone() (Config, error) { + configLogger := *(c.Logger) + configMetrics := *(c.Metrics) + configSession := *(c.Session) + configSocket := *(c.Socket) + configDatabase := *(c.Database) + configSocial := *(c.Social) + configRuntime := *(c.Runtime) + configMatch := *(c.Match) + configTracker := *(c.Tracker) + configConsole := *(c.Console) + configLeaderboard := *(c.Leaderboard) + nc := &config{ + Name: c.Name, + Datadir: c.Datadir, + ShutdownGraceSec: c.ShutdownGraceSec, + Logger: &configLogger, + Metrics: &configMetrics, + Session: &configSession, + Socket: &configSocket, + Database: &configDatabase, + Social: &configSocial, + Runtime: &configRuntime, + Match: &configMatch, + Tracker: &configTracker, + Console: &configConsole, + Leaderboard: &configLeaderboard, + } + nc.Socket.CertPEMBlock = make([]byte, len(c.Socket.CertPEMBlock)) + copy(nc.Socket.CertPEMBlock, c.Socket.CertPEMBlock) + nc.Socket.KeyPEMBlock = make([]byte, len(c.Socket.KeyPEMBlock)) + copy(nc.Socket.KeyPEMBlock, c.Socket.KeyPEMBlock) + if len(c.Socket.TLSCert) != 0 { + if cert, err := tls.X509KeyPair(nc.Socket.CertPEMBlock, nc.Socket.KeyPEMBlock); err != nil { + return nil, err + } else { + nc.Socket.TLSCert = []tls.Certificate{cert} + } + } + nc.Database.Addresses = make([]string, len(c.Database.Addresses)) + copy(nc.Database.Addresses, c.Database.Addresses) + nc.Runtime.Env = make([]string, len(c.Runtime.Env)) + copy(nc.Runtime.Env, c.Runtime.Env) + nc.Runtime.Environment = make(map[string]string, len(c.Runtime.Environment)) + for k, v := range c.Runtime.Environment { + nc.Runtime.Environment[k] = v + } + nc.Leaderboard.BlacklistRankCache = make([]string, len(c.Leaderboard.BlacklistRankCache)) + copy(nc.Leaderboard.BlacklistRankCache, c.Leaderboard.BlacklistRankCache) + + return nc, nil +} + func (c *config) GetName() string { return c.Name } @@ -448,7 +524,9 @@ type SocketConfig struct { OutgoingQueueSize int `yaml:"outgoing_queue_size" json:"outgoing_queue_size" usage:"The maximum number of messages waiting to be sent to the client. If this is exceeded the client is considered too slow and will disconnect. Used when processing real-time connections."` SSLCertificate string `yaml:"ssl_certificate" json:"ssl_certificate" usage:"Path to certificate file if you want the server to use SSL directly. Must also supply ssl_private_key. NOT recommended for production use."` SSLPrivateKey string `yaml:"ssl_private_key" json:"ssl_private_key" usage:"Path to private key file if you want the server to use SSL directly. Must also supply ssl_certificate. NOT recommended for production use."` - TLSCert []tls.Certificate // Created by processing SSLCertificate and SSLPrivateKey, not set from input args directly. + CertPEMBlock []byte `yaml:"-" json:"-"` // Created by fully reading the file contents of SSLCertificate, not set from input args directly. + KeyPEMBlock []byte `yaml:"-" json:"-"` // Created by fully reading the file contents of SSLPrivateKey, not set from input args directly. + TLSCert []tls.Certificate `yaml:"-" json:"-"` // Created by processing CertPEMBlock and KeyPEMBlock, not set from input args directly. } // NewTransportConfig creates a new TransportConfig struct. @@ -513,14 +591,14 @@ func NewSocialConfig() *SocialConfig { // RuntimeConfig is configuration relevant to the Runtime Lua VM. type RuntimeConfig struct { - Environment map[string]string - Env []string `yaml:"env" json:"env" usage:"Values to pass into Runtime as environment variables."` - Path string `yaml:"path" json:"path" usage:"Path for the server to scan for Lua and Go library files."` - HTTPKey string `yaml:"http_key" json:"http_key" usage:"Runtime HTTP Invocation key."` - MinCount int `yaml:"min_count" json:"min_count" usage:"Minimum number of runtime instances to allocate. Default 16."` - MaxCount int `yaml:"max_count" json:"max_count" usage:"Maximum number of runtime instances to allocate. Default 256."` - CallStackSize int `yaml:"call_stack_size" json:"call_stack_size" usage:"Size of each runtime instance's call stack. Default 128."` - RegistrySize int `yaml:"registry_size" json:"registry_size" usage:"Size of each runtime instance's registry. Default 512."` + Environment map[string]string `yaml:"-" json:"-"` + Env []string `yaml:"env" json:"env" usage:"Values to pass into Runtime as environment variables."` + Path string `yaml:"path" json:"path" usage:"Path for the server to scan for Lua and Go library files."` + HTTPKey string `yaml:"http_key" json:"http_key" usage:"Runtime HTTP Invocation key."` + MinCount int `yaml:"min_count" json:"min_count" usage:"Minimum number of runtime instances to allocate. Default 16."` + MaxCount int `yaml:"max_count" json:"max_count" usage:"Maximum number of runtime instances to allocate. Default 256."` + CallStackSize int `yaml:"call_stack_size" json:"call_stack_size" usage:"Size of each runtime instance's call stack. Default 128."` + RegistrySize int `yaml:"registry_size" json:"registry_size" usage:"Size of each runtime instance's registry. Default 512."` } // NewRuntimeConfig creates a new RuntimeConfig struct. @@ -572,6 +650,7 @@ func NewTrackerConfig() *TrackerConfig { // ConsoleConfig is configuration relevant to the embedded console. type ConsoleConfig struct { Port int `yaml:"port" json:"port" usage:"The port for accepting connections for the embedded console, listening on all interfaces."` + Address string `yaml:"address" json:"address" usage:"The IP address of the interface to listen for console traffic on. Default listen on all available addresses/interfaces."` MaxMessageSizeBytes int64 `yaml:"max_message_size_bytes" json:"max_message_size_bytes" usage:"Maximum amount of data in bytes allowed to be read from the client socket per message."` ReadTimeoutMs int `yaml:"read_timeout_ms" json:"read_timeout_ms" usage:"Maximum duration in milliseconds for reading the entire request."` WriteTimeoutMs int `yaml:"write_timeout_ms" json:"write_timeout_ms" usage:"Maximum duration in milliseconds before timing out writes of the response."` @@ -603,6 +682,6 @@ type LeaderboardConfig struct { // NewLeaderboardConfig creates a new LeaderboardConfig struct. func NewLeaderboardConfig() *LeaderboardConfig { return &LeaderboardConfig{ - BlacklistRankCache: []string{""}, + BlacklistRankCache: []string{}, } } diff --git a/server/console.go b/server/console.go index 840c07d58..59dc7f1e4 100644 --- a/server/console.go +++ b/server/console.go @@ -43,12 +43,13 @@ type ConsoleServer struct { logger *zap.Logger db *sql.DB config Config + tracker Tracker configWarnings map[string]string grpcServer *grpc.Server grpcGatewayServer *http.Server } -func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, config Config, db *sql.DB, configWarnings map[string]string) *ConsoleServer { +func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, config Config, tracker Tracker, configWarnings map[string]string) *ConsoleServer { var gatewayContextTimeoutMs string if config.GetConsole().IdleTimeoutMs > 500 { // Ensure the GRPC Gateway timeout is just under the idle timeout (if possible) to ensure it has priority. @@ -68,6 +69,7 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, config Co logger: logger, db: db, config: config, + tracker: tracker, configWarnings: configWarnings, grpcServer: grpcServer, } @@ -75,7 +77,7 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, config Co console.RegisterConsoleServer(grpcServer, s) startupLogger.Info("Starting Console server for gRPC requests", zap.Int("port", config.GetConsole().Port-3)) go func() { - listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", config.GetConsole().Port-3)) + listener, err := net.Listen("tcp", fmt.Sprintf("%v:%d", config.GetConsole().Address, config.GetConsole().Port-3)) if err != nil { startupLogger.Fatal("Console server listener failed to start", zap.Error(err)) } @@ -88,6 +90,9 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, config Co ctx := context.Background() grpcGateway := runtime.NewServeMux() dialAddr := fmt.Sprintf("127.0.0.1:%d", config.GetConsole().Port-3) + if config.GetConsole().Address != "" { + dialAddr = fmt.Sprintf("%v:%d", config.GetConsole().Address, config.GetConsole().Port-3) + } dialOpts := []grpc.DialOption{ //TODO (mo, zyro): Do we need to pass the statsHandler here as well? grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(int(config.GetConsole().MaxMessageSizeBytes))), @@ -140,7 +145,7 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, config Co // Set up and start GRPC Gateway server. s.grpcGatewayServer = &http.Server{ - Addr: fmt.Sprintf(":%d", config.GetConsole().Port), + Addr: fmt.Sprintf("%v:%d", config.GetConsole().Address, config.GetConsole().Port), ReadTimeout: time.Millisecond * time.Duration(int64(config.GetConsole().ReadTimeoutMs)), WriteTimeout: time.Millisecond * time.Duration(int64(config.GetConsole().WriteTimeoutMs)), IdleTimeout: time.Millisecond * time.Duration(int64(config.GetConsole().IdleTimeoutMs)), diff --git a/server/console_account.go b/server/console_account.go index 96c9bbd17..f038513c2 100644 --- a/server/console_account.go +++ b/server/console_account.go @@ -17,24 +17,32 @@ package server import ( "context" "encoding/json" + "fmt" + "github.com/cockroachdb/cockroach-go/crdb" "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/timestamp" "github.com/heroiclabs/nakama/api" "github.com/heroiclabs/nakama/console" + "github.com/pkg/errors" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "strconv" + "strings" ) func (s *ConsoleServer) DeleteAccount(ctx context.Context, in *console.AccountDeleteRequest) (*empty.Empty, error) { - userID := uuid.FromStringOrNil(in.Id) + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } if userID == uuid.Nil { - return nil, status.Error(codes.InvalidArgument, "Invalid user ID was provided.") + return nil, status.Error(codes.InvalidArgument, "Cannot delete the system user.") } - err := DeleteAccount(ctx, s.logger, s.db, userID, in.RecordDeletion != nil && in.RecordDeletion.Value) - if err != nil { + if err = DeleteAccount(ctx, s.logger, s.db, userID, in.RecordDeletion != nil && in.RecordDeletion.Value); err != nil { + // Error already logged in function above. return nil, status.Error(codes.Internal, "An error occurred while trying to delete the user.") } @@ -42,21 +50,66 @@ func (s *ConsoleServer) DeleteAccount(ctx context.Context, in *console.AccountDe } func (s *ConsoleServer) DeleteFriend(ctx context.Context, in *console.DeleteFriendRequest) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + if _, err := uuid.FromString(in.FriendId); err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid friend ID.") + } + + if err = DeleteFriends(ctx, s.logger, s.db, userID, []string{in.FriendId}); err != nil { + // Error already logged in function above. + return nil, status.Error(codes.Internal, "An error occurred while trying to delete the friend relationship.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) DeleteGroupUser(ctx context.Context, in *console.DeleteGroupUserRequest) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + groupID, err := uuid.FromString(in.GroupId) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid group ID.") + } + + if err = KickGroupUsers(ctx, s.logger, s.db, uuid.Nil, groupID, []uuid.UUID{userID}); err != nil { + // Error already logged in function above. + return nil, status.Error(codes.Internal, "An error occurred while trying to remove the user from the group.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) DeleteWalletLedger(ctx context.Context, in *console.DeleteWalletLedgerRequest) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + walletID, err := uuid.FromString(in.WalletId) + if err != nil || walletID == uuid.Nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid wallet ledger item ID.") + } + + _, err = s.db.ExecContext(ctx, "DELETE FROM wallet_ledger WHERE id = $1 AND user_id = $2", walletID, userID) + if err != nil { + s.logger.Error("Error deleting from wallet ledger.", zap.String("id", walletID.String()), zap.String("user_id", userID.String()), zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to remove the user's wallet ledger item.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) ExportAccount(ctx context.Context, in *console.AccountId) (*console.AccountExport, error) { - userID := uuid.FromStringOrNil(in.Id) + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } if userID == uuid.Nil { - return nil, status.Error(codes.InvalidArgument, "Invalid user ID was provided.") + return nil, status.Error(codes.InvalidArgument, "Cannot export the system user.") } // Core user account. @@ -157,21 +210,383 @@ func (s *ConsoleServer) ExportAccount(ctx context.Context, in *console.AccountId } func (s *ConsoleServer) GetAccount(ctx context.Context, in *console.AccountId) (*api.Account, error) { - return &api.Account{}, nil + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + account, err := GetAccount(ctx, s.logger, s.db, s.tracker, userID) + if err != nil { + // Error already logged in function above. + if err == ErrAccountNotFound { + return nil, status.Error(codes.NotFound, "Account not found.") + } + return nil, status.Error(codes.Internal, "An error occurred while trying to retrieve user account.") + } + + return account, nil } func (s *ConsoleServer) GetFriends(ctx context.Context, in *console.AccountId) (*api.Friends, error) { - return &api.Friends{}, nil + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + friends, err := GetFriends(ctx, s.logger, s.db, s.tracker, userID) + if err != nil { + // Error already logged in function above. + return nil, status.Error(codes.Internal, "An error occurred while trying to list the user's friends.") + } + + return friends, nil } func (s *ConsoleServer) GetGroups(ctx context.Context, in *console.AccountId) (*api.UserGroupList, error) { - return &api.UserGroupList{}, nil + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + groups, err := ListUserGroups(ctx, s.logger, s.db, userID) + if err != nil { + // Error already logged in function above. + return nil, status.Error(codes.Internal, "An error occurred while trying to list the user's groups.") + } + + return groups, nil } func (s *ConsoleServer) GetWalletLedger(ctx context.Context, in *console.AccountId) (*console.WalletLedgerList, error) { - return &console.WalletLedgerList{}, nil + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + ledger, err := ListWalletLedger(ctx, s.logger, s.db, userID) + if err != nil { + // Error already logged in function above. + return nil, status.Error(codes.Internal, "An error occurred while trying to list the user's wallet ledger.") + } + + // Convert to console wire format. + consoleLedger := make([]*console.WalletLedger, 0, len(ledger)) + for _, ledgerItem := range ledger { + changeset, err := json.Marshal(ledgerItem.Changeset) + if err != nil { + s.logger.Error("Error encoding wallet ledger changeset.", zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list the user's wallet ledger.") + } + metadata, err := json.Marshal(ledgerItem.Metadata) + if err != nil { + s.logger.Error("Error encoding wallet ledger metadata.", zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list the user's wallet ledger.") + } + consoleLedger = append(consoleLedger, &console.WalletLedger{ + Id: ledgerItem.ID, + UserId: ledgerItem.UserID, + Changeset: string(changeset), + Metadata: string(metadata), + CreateTime: ×tamp.Timestamp{Seconds: ledgerItem.CreateTime}, + UpdateTime: ×tamp.Timestamp{Seconds: ledgerItem.UpdateTime}, + }) + } + + return &console.WalletLedgerList{Items: consoleLedger}, nil } func (s *ConsoleServer) UpdateAccount(ctx context.Context, in *console.UpdateAccountRequest) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + if userID == uuid.Nil { + return nil, status.Error(codes.InvalidArgument, "Cannot update the system user.") + } + + statements := make([]string, 0) + params := []interface{}{userID} + + if v := in.Username; v != nil { + if invalidCharsRegex.MatchString(v.Value) { + return nil, status.Error(codes.InvalidArgument, "Username invalid, no spaces or control characters allowed.") + } + params = append(params, v.Value) + statements = append(statements, "username = $"+strconv.Itoa(len(params))) + } + + if v := in.DisplayName; v != nil { + if d := v.Value; d == "" { + statements = append(statements, "display_name = NULL") + } else { + params = append(params, d) + statements = append(statements, "display_name = $"+strconv.Itoa(len(params))) + } + } + + if v := in.Metadata; v != nil { + var metadataMap map[string]interface{} + if err := json.Unmarshal([]byte(v.Value), metadataMap); err != nil { + return nil, status.Error(codes.InvalidArgument, "Metadata must be a valid JSON object.") + } + params = append(params, v.Value) + statements = append(statements, "metadata = $"+strconv.Itoa(len(params))) + } + + if v := in.AvatarUrl; v != nil { + if a := v.Value; a == "" { + statements = append(statements, "avatar_url = NULL") + } else { + params = append(params, a) + statements = append(statements, "avatar_url = $"+strconv.Itoa(len(params))) + } + } + + if v := in.LangTag; v != nil { + if l := v.Value; l == "" { + statements = append(statements, "lang_tag = NULL") + } else { + params = append(params, l) + statements = append(statements, "lang_tag = $"+strconv.Itoa(len(params))) + } + } + + if v := in.Location; v != nil { + if l := v.Value; l == "" { + statements = append(statements, "location = NULL") + } else { + params = append(params, l) + statements = append(statements, "location = $"+strconv.Itoa(len(params))) + } + } + + if v := in.Timezone; v != nil { + if t := v.Value; t == "" { + statements = append(statements, "timezone = NULL") + } else { + params = append(params, t) + statements = append(statements, "timezone = $"+strconv.Itoa(len(params))) + } + } + + var removeCustomId bool + if v := in.CustomId; v != nil { + if c := v.Value; c == "" { + removeCustomId = true + } else { + if invalidCharsRegex.MatchString(c) { + return nil, status.Error(codes.InvalidArgument, "Custom ID invalid, no spaces or control characters allowed.") + } else if len(c) < 6 || len(c) > 128 { + return nil, status.Error(codes.InvalidArgument, "Custom ID invalid, must be 6-128 bytes.") + } else { + params = append(params, c) + statements = append(statements, "custom_id = $"+strconv.Itoa(len(params))) + } + } + } + + var removeEmail bool + if v := in.Email; v != nil { + if e := v.Value; e == "" { + removeEmail = true + } else { + if invalidCharsRegex.MatchString(e) { + return nil, status.Error(codes.InvalidArgument, "Invalid email address, no spaces or control characters allowed.") + } else if !emailRegex.MatchString(e) { + return nil, status.Error(codes.InvalidArgument, "Invalid email address format.") + } else if len(e) < 10 || len(e) > 255 { + return nil, status.Error(codes.InvalidArgument, "Invalid email address, must be 10-255 bytes.") + } else { + params = append(params, e) + statements = append(statements, "email = $"+strconv.Itoa(len(params))) + } + } + } + + if v := in.Wallet; v != nil { + var walletMap map[string]interface{} + if err := json.Unmarshal([]byte(v.Value), walletMap); err != nil { + return nil, status.Error(codes.InvalidArgument, "Wallet must be a valid JSON object.") + } + if err := checkWalletFormat(walletMap, ""); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + params = append(params, v.Value) + statements = append(statements, "wallet = $"+strconv.Itoa(len(params))) + } + + for oldDeviceID, newDeviceID := range in.DeviceIds { + if invalidCharsRegex.MatchString(oldDeviceID) { + return nil, status.Error(codes.InvalidArgument, "Old device ID invalid, no spaces or control characters allowed.") + } else if len(oldDeviceID) < 10 || len(oldDeviceID) > 128 { + return nil, status.Error(codes.InvalidArgument, "Old device ID invalid, must be 10-128 bytes.") + } + + if newDeviceID != "" { + // Only validate if device ID is not being removed. + if invalidCharsRegex.MatchString(newDeviceID) { + return nil, status.Error(codes.InvalidArgument, "New device ID invalid, no spaces or control characters allowed.") + } else if len(newDeviceID) < 10 || len(newDeviceID) > 128 { + return nil, status.Error(codes.InvalidArgument, "New device ID invalid, must be 10-128 bytes.") + } + } + } + + if len(statements) == 0 && !removeCustomId && !removeEmail && len(in.DeviceIds) == 0 { + // Nothing to update. + return &empty.Empty{}, nil + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + s.logger.Error("Could not begin database transaction.", zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to update the user.") + } + + if err = crdb.ExecuteInTx(ctx, tx, func() error { + for oldDeviceID, newDeviceID := range in.DeviceIds { + if newDeviceID == "" { + query := `DELETE FROM user_device WHERE id = $2 AND user_id = $1 +AND (EXISTS (SELECT id FROM users WHERE id = $1 AND + (facebook_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL + OR custom_id IS NOT NULL)) + OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 AND id <> $2 LIMIT 1))` + + res, err := tx.ExecContext(ctx, query, userID, oldDeviceID) + if err != nil { + s.logger.Error("Could not unlink device ID.", zap.Error(err), zap.Any("input", in)) + return err + } + if count, _ := res.RowsAffected(); count == 0 { + return StatusError(codes.InvalidArgument, "Cannot unlink device ID when there are no other identifiers.", ErrRowsAffectedCount) + } + } else { + query := `UPDATE user_device SET id = $1 WHERE id = $2 AND user_id = $3` + res, err := tx.ExecContext(ctx, query, newDeviceID, oldDeviceID, userID) + if err != nil { + s.logger.Error("Could not update device ID.", zap.Error(err), zap.Any("input", in)) + return err + } + if count, _ := res.RowsAffected(); count == 0 { + return StatusError(codes.InvalidArgument, "Device ID is already linked to a different user.", ErrRowsAffectedCount) + } + } + } + + if len(statements) != 0 { + query := "UPDATE users SET update_time = now(), " + strings.Join(statements, ", ") + " WHERE id = $1" + _, err := tx.ExecContext(ctx, query, params...) + if err != nil { + s.logger.Error("Could not update user account.", zap.Error(err), zap.Any("input", in)) + return err + } + } + + if removeCustomId && removeEmail { + query := `UPDATE users SET custom_id = NULL, email = NULL, update_time = now() +WHERE id = $1 +AND ((facebook_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := tx.ExecContext(ctx, query, userID) + if err != nil { + return err + } + if rowsAffected, _ := res.RowsAffected(); rowsAffected == 0 { + return StatusError(codes.InvalidArgument, "Cannot unlink both custom ID and email address when there are no other identifiers.", ErrRowsAffectedCount) + } + } else if removeCustomId { + query := `UPDATE users SET custom_id = NULL, update_time = now() +WHERE id = $1 +AND ((facebook_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := tx.ExecContext(ctx, query, userID) + if err != nil { + return err + } + if rowsAffected, _ := res.RowsAffected(); rowsAffected == 0 { + return StatusError(codes.InvalidArgument, "Cannot unlink custom ID when there are no other identifiers.", ErrRowsAffectedCount) + } + } else if removeEmail { + query := `UPDATE users SET email = NULL, password = NULL, update_time = now() +WHERE id = $1 +AND ((facebook_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR custom_id IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := tx.ExecContext(ctx, query, userID) + if err != nil { + return err + } + if rowsAffected, _ := res.RowsAffected(); rowsAffected == 0 { + return StatusError(codes.InvalidArgument, "Cannot unlink email address when there are no other identifiers.", ErrRowsAffectedCount) + } + } + + if len(in.DeviceIds) != 0 && len(statements) == 0 && !removeCustomId && !removeEmail { + // Ensure the user account update time is touched if the device IDs have changed but no other updates were applied to the core user record. + _, err := tx.ExecContext(ctx, "UPDATE users SET update_time = now() WHERE id = $1", userID) + if err != nil { + s.logger.Error("Could not unlink device ID.", zap.Error(err), zap.Any("input", in)) + return err + } + } + + return nil + }); err != nil { + if e, ok := err.(*statusError); ok { + // Errors such as unlinking the last profile or username in use. + return nil, e.Status() + } + s.logger.Error("Error updating user.", zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to update the user.") + } + return &empty.Empty{}, nil } + +func checkWalletFormat(wallet map[string]interface{}, path string) error { + for k, v := range wallet { + var currentPath string + if path == "" { + currentPath = k + } else { + currentPath = fmt.Sprintf("%v.%v", path, k) + } + + if vm, ok := v.(map[string]interface{}); ok { + // Nested wallets are fine. + if err := checkWalletFormat(vm, currentPath); err != nil { + return err + } + } else if vf, ok := v.(float64); ok { + // If it's a value, check it's not negative. + if vf < 0 { + return errors.Errorf("Wallet rejected negative value at path '%v'.", currentPath) + } + } else { + // Not a nested wallet a value. + return errors.Errorf("Wallet value type at path '%v' must be map or float64.", currentPath) + } + } + + return nil +} diff --git a/server/console_config.go b/server/console_config.go index 0122b20c6..d77260348 100644 --- a/server/console_config.go +++ b/server/console_config.go @@ -17,18 +17,44 @@ package server import ( "context" "encoding/json" + "fmt" "github.com/golang/protobuf/ptypes/empty" "github.com/heroiclabs/nakama/console" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "net/url" + "strings" ) +const ObfuscationString = "********" + func (s *ConsoleServer) GetConfig(ctx context.Context, in *empty.Empty) (*console.Config, error) { - config, err := json.Marshal(s.config) + cfg, err := s.config.Clone() + if err != nil { + s.logger.Error("Error cloning config.", zap.Error(err)) + return nil, status.Error(codes.Internal, "Error processing config.") + } + + cfg.GetConsole().Password = ObfuscationString + for i, address := range cfg.GetDatabase().Addresses { + rawUrl := fmt.Sprintf("postgresql://%s", address) + parsedUrl, err := url.Parse(rawUrl) + if err != nil { + s.logger.Error("Error parsing database address in config.", zap.Error(err)) + return nil, status.Error(codes.Internal, "Error processing config.") + } + if parsedUrl.User != nil { + if password, isSet := parsedUrl.User.Password(); isSet { + cfg.GetDatabase().Addresses[i] = strings.ReplaceAll(address, parsedUrl.User.Username()+":"+password, parsedUrl.User.Username()+":"+ObfuscationString) + } + } + } + + cfgBytes, err := json.Marshal(cfg) if err != nil { s.logger.Error("Error encoding config.", zap.Error(err)) - return nil, status.Error(codes.Internal, "Error encoding config.") + return nil, status.Error(codes.Internal, "Error processing config.") } configWarnings := make([]*console.Config_Warning, 0, len(s.configWarnings)) @@ -40,7 +66,7 @@ func (s *ConsoleServer) GetConfig(ctx context.Context, in *empty.Empty) (*consol } return &console.Config{ - Config: string(config), + Config: string(cfgBytes), Warnings: configWarnings, }, nil } diff --git a/server/console_unlink.go b/server/console_unlink.go index 16870f606..0270aa5f0 100644 --- a/server/console_unlink.go +++ b/server/console_unlink.go @@ -16,34 +16,243 @@ package server import ( "context" + "github.com/cockroachdb/cockroach-go/crdb" + "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" "github.com/heroiclabs/nakama/console" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func (s *ConsoleServer) UnlinkCustom(ctx context.Context, in *console.AccountId) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + query := `UPDATE users SET custom_id = NULL, update_time = now() +WHERE id = $1 +AND custom_id IS NOT NULL +AND ((facebook_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := s.db.ExecContext(ctx, query, userID) + + if err != nil { + s.logger.Error("Could not unlink custom ID.", zap.Error(err), zap.Any("input", in)) + return nil, status.Error(codes.Internal, "Error while trying to unlink custom ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return nil, status.Error(codes.PermissionDenied, "Cannot unlink custom ID when there are no other identifiers.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) UnlinkDevice(ctx context.Context, in *console.UnlinkDeviceRequest) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + if in.DeviceId == "" { + return nil, status.Error(codes.InvalidArgument, "Requires a valid device ID.") + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + s.logger.Error("Could not begin database transaction.", zap.Error(err)) + return nil, status.Error(codes.Internal, "Could not unlink Device ID.") + } + + err = crdb.ExecuteInTx(ctx, tx, func() error { + query := `DELETE FROM user_device WHERE id = $2 AND user_id = $1 +AND (EXISTS (SELECT id FROM users WHERE id = $1 AND + (facebook_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL + OR custom_id IS NOT NULL)) + OR EXISTS (SELECT id FROM user_device WHERE user_id = $1 AND id <> $2 LIMIT 1))` + + res, err := tx.ExecContext(ctx, query, userID, in.DeviceId) + if err != nil { + s.logger.Debug("Could not unlink device ID.", zap.Error(err), zap.Any("input", in)) + return err + } + if count, _ := res.RowsAffected(); count == 0 { + return StatusError(codes.PermissionDenied, "Cannot unlink device ID when there are no other identifiers.", ErrRowsAffectedCount) + } + + res, err = tx.ExecContext(ctx, "UPDATE users SET update_time = now() WHERE id = $1", userID) + if err != nil { + s.logger.Debug("Could not unlink device ID.", zap.Error(err), zap.Any("input", in)) + return err + } + if count, _ := res.RowsAffected(); count == 0 { + return StatusError(codes.PermissionDenied, "Cannot unlink device ID when there are no other identifiers.", ErrRowsAffectedCount) + } + + return nil + }) + + if err != nil { + if e, ok := err.(*statusError); ok { + return nil, e.Status() + } + s.logger.Error("Error in database transaction.", zap.Error(err)) + return nil, status.Error(codes.Internal, "Could not unlink device ID.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) UnlinkEmail(ctx context.Context, in *console.AccountId) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + query := `UPDATE users SET email = NULL, password = NULL, update_time = now() +WHERE id = $1 +AND email IS NOT NULL +AND ((facebook_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR custom_id IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := s.db.ExecContext(ctx, query, userID) + + if err != nil { + s.logger.Error("Could not unlink email.", zap.Error(err), zap.Any("input", in)) + return nil, status.Error(codes.Internal, "Error while trying to unlink email.") + } else if count, _ := res.RowsAffected(); count == 0 { + return nil, status.Error(codes.PermissionDenied, "Cannot unlink email address when there are no other identifiers.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) UnlinkFacebook(ctx context.Context, in *console.AccountId) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + query := `UPDATE users SET facebook_id = NULL, update_time = now() +WHERE id = $1 +AND facebook_id IS NOT NULL +AND ((custom_id IS NOT NULL + OR google_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := s.db.ExecContext(ctx, query, userID) + + if err != nil { + s.logger.Error("Could not unlink Facebook ID.", zap.Error(err), zap.Any("input", in)) + return nil, status.Error(codes.Internal, "Error while trying to unlink Facebook ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return nil, status.Error(codes.PermissionDenied, "Cannot unlink Facebook ID when there are no other identifiers.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) UnlinkGameCenter(ctx context.Context, in *console.AccountId) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + query := `UPDATE users SET gamecenter_id = NULL, update_time = now() +WHERE id = $1 +AND gamecenter_id IS NOT NULL +AND ((custom_id IS NOT NULL + OR google_id IS NOT NULL + OR facebook_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := s.db.ExecContext(ctx, query, userID) + + if err != nil { + s.logger.Error("Could not unlink GameCenter ID.", zap.Error(err), zap.Any("input", in)) + return nil, status.Error(codes.Internal, "Error while trying to unlink GameCenter ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return nil, status.Error(codes.PermissionDenied, "Cannot unlink Game Center ID when there are no other identifiers.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) UnlinkGoogle(ctx context.Context, in *console.AccountId) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + query := `UPDATE users SET google_id = NULL, update_time = now() +WHERE id = $1 +AND google_id IS NOT NULL +AND ((custom_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR facebook_id IS NOT NULL + OR steam_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := s.db.ExecContext(ctx, query, userID) + + if err != nil { + s.logger.Error("Could not unlink Google ID.", zap.Error(err), zap.Any("input", in)) + return nil, status.Error(codes.Internal, "Error while trying to unlink Google ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return nil, status.Error(codes.PermissionDenied, "Cannot unlink Google ID when there are no other identifiers.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) UnlinkSteam(ctx context.Context, in *console.AccountId) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + + query := `UPDATE users SET steam_id = NULL, update_time = now() +WHERE id = $1 +AND steam_id IS NOT NULL +AND ((custom_id IS NOT NULL + OR gamecenter_id IS NOT NULL + OR facebook_id IS NOT NULL + OR google_id IS NOT NULL + OR email IS NOT NULL) + OR + EXISTS (SELECT id FROM user_device WHERE user_id = $1 LIMIT 1))` + + res, err := s.db.ExecContext(ctx, query, userID) + + if err != nil { + s.logger.Error("Could not unlink Steam ID.", zap.Error(err), zap.Any("input", in)) + return nil, status.Error(codes.Internal, "Error while trying to unlink Steam ID.") + } else if count, _ := res.RowsAffected(); count == 0 { + return nil, status.Error(codes.PermissionDenied, "Cannot unlink Steam ID when there are no other identifiers.") + } + return &empty.Empty{}, nil } diff --git a/server/console_user.go b/server/console_user.go index 75821a966..bd567d366 100644 --- a/server/console_user.go +++ b/server/console_user.go @@ -16,22 +16,213 @@ package server import ( "context" + "database/sql" + "github.com/gofrs/uuid" "github.com/golang/protobuf/ptypes/empty" + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/heroiclabs/nakama/api" "github.com/heroiclabs/nakama/console" + "github.com/lib/pq" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func (s *ConsoleServer) BanUser(ctx context.Context, in *console.AccountId) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + if userID == uuid.Nil { + return nil, status.Error(codes.InvalidArgument, "Cannot ban the system user.") + } + + if err := BanUsers(ctx, s.logger, s.db, []string{in.Id}); err != nil { + // Error logged in the core function above. + return nil, status.Error(codes.Internal, "An error occurred while trying to ban the user.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) UnbanUser(ctx context.Context, in *console.AccountId) (*empty.Empty, error) { + userID, err := uuid.FromString(in.Id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Requires a valid user ID.") + } + if userID == uuid.Nil { + return nil, status.Error(codes.InvalidArgument, "Cannot unban the system user.") + } + + if err := UnbanUsers(ctx, s.logger, s.db, []string{in.Id}); err != nil { + // Error logged in the core function above. + return nil, status.Error(codes.Internal, "An error occurred while trying to unban the user.") + } + return &empty.Empty{}, nil } func (s *ConsoleServer) DeleteUsers(ctx context.Context, in *empty.Empty) (*empty.Empty, error) { + // Delete all but the system user. Related data will be removed by cascading constraints. + _, err := s.db.ExecContext(ctx, "DELETE FROM users WHERE id <> '00000000-0000-0000-0000-000000000000'") + if err != nil { + s.logger.Error("Error deleting all user accounts.", zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to delete all users.") + } return &empty.Empty{}, nil } func (s *ConsoleServer) ListUsers(ctx context.Context, in *console.ListUsersRequest) (*console.UserList, error) { - return &console.UserList{}, nil + // Searching only through tombstone records. + if in.Tombstones { + var userID *uuid.UUID + if in.Filter != "" { + uid, err := uuid.FromString(in.Filter) + if err != nil { + // Filtering for a tombstone using username, no results are possible. + return &console.UserList{ + TotalCount: countUsers(ctx, s.logger, s.db), + }, nil + } + userID = &uid + } + + if userID != nil { + // Looking up a single specific tombstone. + var createTime pq.NullTime + err := s.db.QueryRowContext(ctx, "SELECT create_time FROM user_tombstone WHERE user_id = $1", *userID).Scan(&createTime) + if err != nil { + if err == sql.ErrNoRows { + return &console.UserList{ + TotalCount: countUsers(ctx, s.logger, s.db), + }, nil + } + s.logger.Error("Error looking up user tombstone.", zap.Any("in", in), zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list users.") + } + + return &console.UserList{ + Users: []*api.User{ + &api.User{ + Id: in.Filter, + UpdateTime: ×tamp.Timestamp{Seconds: createTime.Time.Unix()}, + }, + }, + TotalCount: countUsers(ctx, s.logger, s.db), + }, nil + } + + query := "SELECT user_id, create_time FROM user_tombstone LIMIT 50" + + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + s.logger.Error("Error querying user tombstones.", zap.Any("in", in), zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list users.") + } + + users := make([]*api.User, 0, 50) + + for rows.Next() { + var id string + var createTime pq.NullTime + if err = rows.Scan(&id, &createTime); err != nil { + rows.Close() + s.logger.Error("Error scanning user tombstones.", zap.Any("in", in), zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list users.") + } + + users = append(users, &api.User{ + Id: id, + UpdateTime: ×tamp.Timestamp{Seconds: createTime.Time.Unix()}, + }) + } + rows.Close() + + return &console.UserList{ + Users: users, + TotalCount: countUsers(ctx, s.logger, s.db), + }, nil + } + + if in.Filter != "" { + _, err := uuid.FromString(in.Filter) + // If the filter is not a valid user ID treat it as a username instead. + + var query string + params := []interface{}{in.Filter} + if err != nil { + query = "SELECT id, username, display_name, avatar_url, lang_tag, location, timezone, metadata, facebook_id, google_id, gamecenter_id, steam_id, edge_count, create_time, update_time FROM users WHERE username = $1" + } else { + query = "SELECT id, username, display_name, avatar_url, lang_tag, location, timezone, metadata, facebook_id, google_id, gamecenter_id, steam_id, edge_count, create_time, update_time FROM users WHERE (username = $1 OR id = $1)" + } + + if in.Banned { + query += " AND disable_time <> '1970-01-01 00:00:00 UTC'" + } + + rows, err := s.db.QueryContext(ctx, query, params...) + if err != nil { + s.logger.Error("Error querying users.", zap.Any("in", in), zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list users.") + } + + users := make([]*api.User, 0, 2) + + for rows.Next() { + user, err := convertUser(s.tracker, rows) + if err != nil { + rows.Close() + s.logger.Error("Error scanning users.", zap.Any("in", in), zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list users.") + } + users = append(users, user) + } + rows.Close() + + return &console.UserList{ + Users: users, + TotalCount: countUsers(ctx, s.logger, s.db), + }, nil + } + + var query string + + if in.Banned { + query = "SELECT id, username, display_name, avatar_url, lang_tag, location, timezone, metadata, facebook_id, google_id, gamecenter_id, steam_id, edge_count, create_time, update_time FROM users WHERE disable_time <> '1970-01-01 00:00:00 UTC' LIMIT 50" + } else { + query = "SELECT id, username, display_name, avatar_url, lang_tag, location, timezone, metadata, facebook_id, google_id, gamecenter_id, steam_id, edge_count, create_time, update_time FROM users LIMIT 50" + } + + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + s.logger.Error("Error querying users.", zap.Any("in", in), zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list users.") + } + + users := make([]*api.User, 0, 50) + + for rows.Next() { + user, err := convertUser(s.tracker, rows) + if err != nil { + rows.Close() + s.logger.Error("Error scanning users.", zap.Any("in", in), zap.Error(err)) + return nil, status.Error(codes.Internal, "An error occurred while trying to list users.") + } + + users = append(users, user) + } + rows.Close() + + return &console.UserList{ + Users: users, + TotalCount: countUsers(ctx, s.logger, s.db), + }, nil +} + +func countUsers(ctx context.Context, logger *zap.Logger, db *sql.DB) int32 { + var count int + if err := db.QueryRowContext(ctx, "SELECT count(id) FROM users").Scan(&count); err != nil { + logger.Error("Error counting users.", zap.Error(err)) + } + return int32(count) } diff --git a/server/core_leaderboard.go b/server/core_leaderboard.go index 989b82ed4..aafb1cc6e 100644 --- a/server/core_leaderboard.go +++ b/server/core_leaderboard.go @@ -220,7 +220,7 @@ func LeaderboardRecordsList(ctx context.Context, logger *zap.Logger, db *sql.DB, if nextCursor != nil { cursorBuf := new(bytes.Buffer) - if gob.NewEncoder(cursorBuf).Encode(nextCursor); err != nil { + if err := gob.NewEncoder(cursorBuf).Encode(nextCursor); err != nil { logger.Error("Error creating leaderboard records list next cursor", zap.Error(err)) return nil, err } @@ -228,7 +228,7 @@ func LeaderboardRecordsList(ctx context.Context, logger *zap.Logger, db *sql.DB, } if prevCursor != nil { cursorBuf := new(bytes.Buffer) - if gob.NewEncoder(cursorBuf).Encode(prevCursor); err != nil { + if err := gob.NewEncoder(cursorBuf).Encode(prevCursor); err != nil { logger.Error("Error creating leaderboard records list previous cursor", zap.Error(err)) return nil, err } -- GitLab