Skip to content

Commit af46acd

Browse files
authored
Extended OnlineClient information variables (#43)
Extends the OnlineClient information variables to include new information. These are client UID, client away information, voice information, client time information, client groups information, client info, client icon, client country, client IP and client badges.
1 parent 01bb4ee commit af46acd

File tree

4 files changed

+225
-21
lines changed

4 files changed

+225
-21
lines changed

helpers.go

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,26 @@ func DecodeResponse(lines []string, v interface{}) error {
7171
for _, part := range strings.Split(lines[0], "|") {
7272
for _, val := range strings.Split(part, " ") {
7373
parts := strings.SplitN(val, "=", 2)
74-
// TODO(steve): support groups
7574
key := Decode(parts[0])
7675
if len(parts) == 2 {
7776
v := Decode(parts[1])
7877
if i, err := strconv.Atoi(v); err != nil {
79-
input[key] = v
78+
// Only support comma seperated lists
79+
// by keyname to avoid incorrect decoding.
80+
if key == "client_servergroups" {
81+
parts := strings.Split(v, ",")
82+
serverGroups := make([]int, len(parts))
83+
for i, s := range parts {
84+
group, err := strconv.Atoi(s)
85+
if err != nil {
86+
return fmt.Errorf("decode server group: %w", err)
87+
}
88+
serverGroups[i] = group
89+
}
90+
input[key] = serverGroups
91+
} else {
92+
input[key] = v
93+
}
8094
} else {
8195
input[key] = i
8296
}
@@ -136,10 +150,56 @@ func decodeSlice(elemType reflect.Type, slice reflect.Value, input map[string]in
136150
return fmt.Errorf("can't interface %#v", v)
137151
}
138152

153+
// The mapstructure's decoder doesn't support squashing
154+
// for embedded pointers to structs (the type is lost when
155+
// using reflection for nil values). We need to add pointers
156+
// to empty structs within the interface to get around this.
157+
switch v.Interface().(type) {
158+
case *OnlineClient:
159+
ext := &OnlineClientExt{
160+
OnlineClientGroups: &OnlineClientGroups{},
161+
OnlineClientInfo: &OnlineClientInfo{},
162+
OnlineClientTimes: &OnlineClientTimes{},
163+
OnlineClientVoice: &OnlineClientVoice{},
164+
}
165+
v.Interface().(*OnlineClient).OnlineClientExt = ext
166+
}
167+
139168
if err := decodeMap(input, v.Interface()); err != nil {
140169
return err
141170
}
142171

172+
// nil out empty structs
173+
switch v.Interface().(type) {
174+
case *OnlineClient:
175+
ext := v.Interface().(*OnlineClient).OnlineClientExt
176+
emptyExt := OnlineClientExt{}
177+
emptyExtGroups := OnlineClientGroups{}
178+
emptyExtInfo := OnlineClientInfo{}
179+
emptyExtTimes := OnlineClientTimes{}
180+
emptyExtVoice := OnlineClientVoice{}
181+
182+
if *ext.OnlineClientGroups == emptyExtGroups {
183+
v.Interface().(*OnlineClient).OnlineClientExt.OnlineClientGroups = nil
184+
}
185+
186+
if *ext.OnlineClientInfo == emptyExtInfo {
187+
v.Interface().(*OnlineClient).OnlineClientExt.OnlineClientInfo = nil
188+
}
189+
190+
if *ext.OnlineClientTimes == emptyExtTimes {
191+
v.Interface().(*OnlineClient).OnlineClientExt.OnlineClientTimes = nil
192+
}
193+
194+
if *ext.OnlineClientVoice == emptyExtVoice {
195+
v.Interface().(*OnlineClient).OnlineClientExt.OnlineClientVoice = nil
196+
}
197+
198+
if *ext == emptyExt {
199+
v.Interface().(*OnlineClient).OnlineClientExt = nil
200+
}
201+
}
202+
143203
if elemType.Kind() == reflect.Struct {
144204
v = v.Elem()
145205
}

mockserver_test.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ var commands = map[string]string{
4343
"instanceinfo": "serverinstance_database_version=26 serverinstance_filetransfer_port=30033 serverinstance_max_download_total_bandwidth=18446744073709551615 serverinstance_max_upload_total_bandwidth=18446744073709551615 serverinstance_guest_serverquery_group=1 serverinstance_serverquery_flood_commands=50 serverinstance_serverquery_flood_time=3 serverinstance_serverquery_ban_time=600 serverinstance_template_serveradmin_group=3 serverinstance_template_serverdefault_group=5 serverinstance_template_channeladmin_group=1 serverinstance_template_channeldefault_group=4 serverinstance_permissions_version=19 serverinstance_pending_connections_per_ip=0",
4444
"serverrequestconnectioninfo": "connection_filetransfer_bandwidth_sent=0 connection_filetransfer_bandwidth_received=0 connection_filetransfer_bytes_sent_total=617 connection_filetransfer_bytes_received_total=0 connection_packets_sent_total=926413 connection_bytes_sent_total=92911395 connection_packets_received_total=650335 connection_bytes_received_total=61940731 connection_bandwidth_sent_last_second_total=0 connection_bandwidth_sent_last_minute_total=0 connection_bandwidth_received_last_second_total=0 connection_bandwidth_received_last_minute_total=0 connection_connected_time=49408 connection_packetloss_total=0.0000 connection_ping=0.0000 connection_packets_sent_speech=320432180 connection_bytes_sent_speech=43805818511 connection_packets_received_speech=174885295 connection_bytes_received_speech=24127808273 connection_packets_sent_keepalive=55230363 connection_bytes_sent_keepalive=2264444883 connection_packets_received_keepalive=55149547 connection_bytes_received_keepalive=2316390993 connection_packets_sent_control=2376088 connection_bytes_sent_control=525691022 connection_packets_received_control=2376138 connection_bytes_received_control=227044870",
4545
"channellist": "cid=499 pid=0 channel_order=0 channel_name=Default\\sChannel total_clients=1 channel_needed_subscribe_power=0",
46-
"clientlist": "clid=5 cid=7 client_database_id=40 client_nickname=ScP client_type=0 client_away=1 client_away_message=not\\shere",
47-
"clientdblist": "cldbid=7 client_unique_identifier=DZhdQU58qyooEK4Fr8Ly738hEmc= client_nickname=MuhChy client_created=1259147468 client_lastconnected=1259421233",
48-
"whoami": "virtualserver_status=online virtualserver_id=18 virtualserver_unique_identifier=gNITtWtKs9+Uh3L4LKv8\\/YHsn5c= virtualserver_port=9987 client_id=94 client_channel_id=432 client_nickname=serveradmin\\sfrom\\s127.0.0.1:49725 client_database_id=1 client_login_name=serveradmin client_unique_identifier=serveradmin client_origin_server_id=0",
49-
cmdQuit: "",
46+
"clientlist": `clid=42087 cid=39 client_database_id=19 client_nickname=bdeb1337 client_type=0 client_away=0 client_away_message`,
47+
"clientlist -uid -away -voice -times -groups -info -icon -country -ip -badges": `clid=42087 cid=39 client_database_id=19 client_nickname=bdeb1337 client_type=0 client_away=1 client_away_message=afk client_flag_talking=0 client_input_muted=0 client_output_muted=0 client_input_hardware=1 client_output_hardware=1 client_talk_power=75 client_is_talker=0 client_is_priority_speaker=0 client_is_recording=0 client_is_channel_commander=0 client_unique_identifier=DZhdQU58qyooEK4Fr8Ly738hEmc= client_servergroups=6,8 client_channel_group_id=8 client_channel_group_inherited_channel_id=39 client_version=3.6.1\s[Build:\s1690193193] client_platform=OS\sX client_idle_time=1280228 client_created=1661793049 client_lastconnected=1691527133 client_icon_id=0 client_country=BE connection_client_ip=1.3.3.7 client_badges`,
48+
"clientdblist": "cldbid=7 client_unique_identifier=DZhdQU58qyooEK4Fr8Ly738hEmc= client_nickname=MuhChy client_created=1259147468 client_lastconnected=1259421233",
49+
"whoami": "virtualserver_status=online virtualserver_id=18 virtualserver_unique_identifier=gNITtWtKs9+Uh3L4LKv8\\/YHsn5c= virtualserver_port=9987 client_id=94 client_channel_id=432 client_nickname=serveradmin\\sfrom\\s127.0.0.1:49725 client_database_id=1 client_login_name=serveradmin client_unique_identifier=serveradmin client_origin_server_id=0",
50+
cmdQuit: "",
5051
}
5152

5253
// newLockListener creates a new listener on the local IP.
@@ -256,6 +257,11 @@ func (s *server) handle(conn net.Conn) {
256257
l := sc.Text()
257258
parts := strings.Split(l, " ")
258259
cmd := strings.TrimSpace(parts[0])
260+
// Support server commands with specific optional parameters,
261+
// they can be bypassed from the usual parameter trimming here.
262+
if cmd == "clientlist" {
263+
cmd = l
264+
}
259265
resp, ok := commands[cmd]
260266
var err error
261267
switch {

server_cmds.go

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ import (
77
const (
88
// ExtendedServerList can be passed to List to get extended server information.
99
ExtendedServerList = "-extended"
10+
11+
// ClientUID can be passed to ClientList to retrieve client UID information.
12+
ClientUID = "-uid"
13+
// ClientAway can be passed to ClientList to retrieve client away information.
14+
ClientAway = "-away"
15+
// ClientVoice can be passed to ClientList to retrieve client voice information.
16+
ClientVoice = "-voice"
17+
// ClientTimes can be passed to ClientList to retrieve client time information.
18+
ClientTimes = "-times"
19+
// ClientGroups can be passed to ClientList to retrieve client groups information.
20+
ClientGroups = "-groups"
21+
// ClientInfo can be passed to ClientList to retrieve client information.
22+
ClientInfo = "-info"
23+
// ClientIcon can be passed to ClientList to retrieve client icon information.
24+
ClientIcon = "-icon"
25+
// ClientCountry can be passed to ClientList to retrieve client country information.
26+
ClientCountry = "-country"
27+
// ClientIP can be passed to ClientList to retrieve client IP information.
28+
ClientIP = "-ip"
29+
// ClientBadges can be passed to ClientList to retrieve client badge information.
30+
ClientBadges = "-badges"
31+
// ClientListFull can be passed to ClientList to get all extended client information.
32+
ClientListFull = "-uid -away -voice -times -groups -info -icon -country -ip -badges"
1033
)
1134

1235
// ServerMethods groups server methods.
@@ -351,19 +374,70 @@ func (s *ServerMethods) PrivilegeKeyAdd(ttype, id1, id2 int, options ...CmdArg)
351374

352375
// OnlineClient represents a client online on a virtual server.
353376
type OnlineClient struct {
354-
ID int `ms:"clid"`
355-
ChannelID int `ms:"cid"`
356-
DatabaseID int `ms:"client_database_id"`
357-
Nickname string `ms:"client_nickname"`
358-
Type int `ms:"client_type"`
359-
Away bool `ms:"client_away"`
360-
AwayMessage string `ms:"client_away_message"`
377+
// Following variables are always returned by ClientList().
378+
ID int `ms:"clid"`
379+
ChannelID int `ms:"cid"`
380+
DatabaseID int `ms:"client_database_id"`
381+
Nickname string `ms:"client_nickname"`
382+
Type int `ms:"client_type"`
383+
// Following variables are optional and can be requested in ClientList() to get extended client information.
384+
// note: Away and AwayMessage are currently optional but not using pointers for compatibility considerations.
385+
Away bool `ms:"client_away"` // Only populated if ClientAway or ClientListFull is passed to ClientList.
386+
AwayMessage string `ms:"client_away_message"` // Only populated if ClientAway or ClientListFull is passed to ClientList.
387+
*OnlineClientExt `ms:",squash"` // Only populated if any of the options is passed to ClientList.
388+
}
389+
390+
// OnlineClientExt represents all ClientList extensions.
391+
type OnlineClientExt struct {
392+
UniqueIdentifier *string `ms:"client_unique_identifier"` // Only populated if ClientUID or ClientListFull is passed to ClientList.
393+
*OnlineClientVoice `ms:",squash"` // Only populated if ClientVoice or ClientListFull is passed to ClientList.
394+
*OnlineClientTimes `ms:",squash"` // Only populated if ClientTimes or ClientListFull is passed to ClientList.
395+
*OnlineClientGroups `ms:",squash"` // Only populated if ClientGroups or ClientListFull is passed to ClientList.
396+
*OnlineClientInfo `ms:",squash"` // Only populated if ClientInfo or ClientListFull is passed to ClientList.
397+
Country *string `ms:"client_country"` // Only populated if ClientCountry or ClientListFull is passed to ClientList.
398+
IP *string `ms:"connection_client_ip"` // Only populated if ClientIP or ClientListFull is passed to ClientList.
399+
Badges *string `ms:"client_badges"` // Only populated if ClientBadges or ClientListFull is passed to ClientList.
400+
IconID *int `ms:"client_icon_id"` // Only populated if ClientIcon or ClientListFull is passed to ClientList.
401+
}
402+
403+
// OnlineClientVoice represents all ClientList extensions when the ClientVoice parameter is passed.
404+
type OnlineClientVoice struct {
405+
FlagTalking *bool `ms:"client_flag_talking"`
406+
InputMuted *bool `ms:"client_input_muted"`
407+
OutputMuted *bool `ms:"client_output_muted"`
408+
InputHardware *bool `ms:"client_input_hardware"`
409+
OutputHardware *bool `ms:"client_output_hardware"`
410+
TalkPower *int `ms:"client_talk_power"`
411+
IsTalker *bool `ms:"client_is_talker"`
412+
IsPrioritySpeaker *bool `ms:"client_is_priority_speaker"`
413+
IsRecording *bool `ms:"client_is_recording"`
414+
IsChannelCommander *bool `ms:"client_is_channel_commander"`
415+
}
416+
417+
// OnlineClientTimes represents all ClientList extensions when the ClientTimes parameter is passed.
418+
type OnlineClientTimes struct {
419+
IdleTime *int `ms:"client_idle_time"`
420+
Created *int `ms:"client_created"`
421+
LastConnected *int `ms:"client_lastconnected"`
422+
}
423+
424+
// OnlineClientGroups represents all ClientList extensions when the ClientGroups parameter is passed.
425+
type OnlineClientGroups struct {
426+
ChannelGroupID *int `ms:"client_channel_group_id"`
427+
ChannelGroupInheritedChannelID *int `ms:"client_channel_group_inherited_channel_id"`
428+
ServerGroups *[]int `ms:"client_servergroups"`
429+
}
430+
431+
// OnlineClientInfo represents all ClientList extensions when the ClientInfo parameter is passed.
432+
type OnlineClientInfo struct {
433+
Version *string `ms:"client_version"`
434+
Platform *string `ms:"client_platform"`
361435
}
362436

363437
// ClientList returns a list of online clients.
364-
func (s *ServerMethods) ClientList() ([]*OnlineClient, error) {
438+
func (s *ServerMethods) ClientList(options ...string) ([]*OnlineClient, error) {
365439
var clients []*OnlineClient
366-
if _, err := s.ExecCmd(NewCmd("clientlist").WithResponse(&clients)); err != nil {
440+
if _, err := s.ExecCmd(NewCmd("clientlist").WithOptions(options...).WithResponse(&clients)); err != nil {
367441
return nil, err
368442
}
369443
return clients, nil

server_cmds_test.go

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,17 +306,80 @@ func testCmdsServer(t *testing.T, c *Client) {
306306

307307
expected := []*OnlineClient{
308308
{
309-
ID: 5,
310-
ChannelID: 7,
311-
DatabaseID: 40,
312-
Nickname: "ScP",
309+
ID: 42087,
310+
ChannelID: 39,
311+
DatabaseID: 19,
312+
Nickname: "bdeb1337",
313+
Type: 0,
314+
},
315+
}
316+
317+
assert.Equal(t, expected, clients)
318+
}
319+
320+
clientlistextended := func(t *testing.T) {
321+
t.Helper()
322+
clientz, err := c.Server.ClientList(ClientListFull)
323+
if !assert.NoError(t, err) {
324+
return
325+
}
326+
327+
// helper variables & functions for pointers
328+
falseP := false
329+
trueP := true
330+
stringptr := func(s string) *string {
331+
return &s
332+
}
333+
intptr := func(i int) *int {
334+
return &i
335+
}
336+
337+
expected := []*OnlineClient{
338+
{
339+
ID: 42087,
340+
ChannelID: 39,
341+
DatabaseID: 19,
342+
Nickname: "bdeb1337",
313343
Type: 0,
314344
Away: true,
315-
AwayMessage: "not here",
345+
AwayMessage: "afk",
346+
OnlineClientExt: &OnlineClientExt{
347+
UniqueIdentifier: stringptr("DZhdQU58qyooEK4Fr8Ly738hEmc="),
348+
OnlineClientVoice: &OnlineClientVoice{
349+
FlagTalking: &falseP,
350+
InputMuted: &falseP,
351+
OutputMuted: &falseP,
352+
InputHardware: &trueP,
353+
OutputHardware: &trueP,
354+
TalkPower: intptr(75),
355+
IsTalker: &falseP,
356+
IsPrioritySpeaker: &falseP,
357+
IsRecording: &falseP,
358+
IsChannelCommander: &falseP,
359+
},
360+
OnlineClientTimes: &OnlineClientTimes{
361+
IdleTime: intptr(1280228),
362+
Created: intptr(1661793049),
363+
LastConnected: intptr(1691527133),
364+
},
365+
OnlineClientGroups: &OnlineClientGroups{
366+
ChannelGroupID: intptr(8),
367+
ChannelGroupInheritedChannelID: intptr(39),
368+
ServerGroups: &[]int{6, 8},
369+
},
370+
OnlineClientInfo: &OnlineClientInfo{
371+
Version: stringptr("3.6.1 [Build: 1690193193]"),
372+
Platform: stringptr("OS X"),
373+
},
374+
IconID: intptr(0),
375+
Country: stringptr("BE"),
376+
IP: stringptr("1.3.3.7"),
377+
Badges: stringptr(""),
378+
},
316379
},
317380
}
318381

319-
assert.Equal(t, expected, clients)
382+
assert.Equal(t, expected, clientz)
320383
}
321384

322385
clientdblist := func(t *testing.T) {
@@ -358,6 +421,7 @@ func testCmdsServer(t *testing.T, c *Client) {
358421
{"instanceinfo", instanceinfo},
359422
{"channellist", channellist},
360423
{"clientlist", clientlist},
424+
{"clientlistextended", clientlistextended},
361425
{"clientdblist", clientdblist},
362426
}
363427

0 commit comments

Comments
 (0)