Skip to content

Added clientinfo command support #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ install:

script:
- go test -v -race ./...
- gometalinter.v1 --cyclo-over=16 ./...
- gometalinter.v1 --cyclo-over=17 ./...
20 changes: 19 additions & 1 deletion helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ func decodeMap(d map[string]interface{}, r interface{}) error {
WeaklyTypedInput: true,
TagName: "ms",
Result: r,
DecodeHook: timeHookFunc,
DecodeHook: mapstructure.ComposeDecodeHookFunc(
sliceHookFunc,
timeHookFunc,
),
}
dec, err := mapstructure.NewDecoder(cfg)
if err != nil {
Expand Down Expand Up @@ -142,6 +145,21 @@ func decodeSlice(elemType reflect.Type, slice reflect.Value, input map[string]in
return nil
}

// sliceHookFunc supports int/string decoding to slice
func sliceHookFunc(from reflect.Kind, to reflect.Kind, data interface{}) (interface{}, error) {
if to == reflect.Slice {
if from == reflect.String {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these if's cant be true so a switch on from would be better.

return strings.Split(data.(string), ","), nil
}

if from == reflect.Int {
return []int{data.(int)}, nil
}
}

return data, nil
}

var timeType = reflect.TypeOf(time.Time{})

// timeHookFunc supports decoding to time
Expand Down
1 change: 1 addition & 0 deletions mockserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var (
"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",
"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",
"channellist": "cid=499 pid=0 channel_order=0 channel_name=Default\\sChannel total_clients=1 channel_needed_subscribe_power=0",
"clientinfo": `cid=20 client_idle_time=28122 client_unique_identifier=P5H2hrN6+gpQI4n\/dXp3p17vtY0= client_nickname=Rabe85 client_version=3.0.0-alpha24\s[Build:\s8785]\s(UI:\s8785) client_platform=Windows client_input_muted=0 client_output_muted=0 client_outputonly_muted=0 client_input_hardware=1 client_output_hardware=1 client_default_channel=\/20 client_meta_data client_is_recording=0 client_version_sign=+\/BWvaeokGg4YkO1v3ouZB5vtIIgUZ5bM5cRfxBstfnHUdro2ja+5b+3sFUzEy8\/vvEISXVD6U95blTb638MCQ== client_security_hash client_login_name client_database_id=8 client_channel_group_id=8 client_servergroups=6,10 client_created=1503431624 client_lastconnected=1530383977 client_totalconnections=138 client_away=0 client_away_message client_type=0 client_flag_avatar=dd213abf2a94396ece544b22c4e56821 client_talk_power=75 client_talk_request=0 client_talk_request_msg client_description client_is_talker=0 client_month_bytes_uploaded=0 client_month_bytes_downloaded=0 client_total_bytes_uploaded=0 client_total_bytes_downloaded=3014720 client_is_priority_speaker=1 client_nickname_phonetic=rabeh client_needed_serverquery_view_power=75 client_default_token client_icon_id=0 client_is_channel_commander=1 client_country=DE client_channel_group_inherited_channel_id=20 client_badges=overwolf=0 client_base64HashClientUID=kdohhblmninnfhaecihcijemaigdnkdhgjllefed connection_filetransfer_bandwidth_sent=0 connection_filetransfer_bandwidth_received=0 connection_packets_sent_total=46880 connection_bytes_sent_total=6426774 connection_packets_received_total=14098 connection_bytes_received_total=1644574 connection_bandwidth_sent_last_second_total=81 connection_bandwidth_sent_last_minute_total=92 connection_bandwidth_received_last_second_total=83 connection_bandwidth_received_last_minute_total=97 connection_connected_time=2084247 connection_client_ip=83.123.45.6`,
"clientlist": "clid=5 cid=7 client_database_id=40 client_nickname=ScP client_type=0 client_away=1 client_away_message=not\\shere",
"clientdblist": "cldbid=7 client_unique_identifier=DZhdQU58qyooEK4Fr8Ly738hEmc= client_nickname=MuhChy client_created=1259147468 client_lastconnected=1259421233",
"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",
Expand Down
121 changes: 116 additions & 5 deletions server_cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,16 +263,16 @@ func (s *ServerMethods) Stop(id int) error {

// Group represents a virtual server group.
type Group struct {
ID int `ms:"sgid"`
ID int `ms:"sgid"`
Name string
Type int
IconID int
Saved bool `ms:"savedb"`
SortID int
NameMode int
ModifyPower int `ms:"n_modifyp"`
MemberAddPower int `ms:"n_member_addp"`
MemberRemovePower int `ms:"n_member_addp"`
ModifyPower int `ms:"n_modifyp"`
MemberAddPower int `ms:"n_member_addp"`
MemberRemovePower int `ms:"n_member_addp"`
}

// GroupList returns a list of available groups for the selected server.
Expand Down Expand Up @@ -340,14 +340,125 @@ func (s *ServerMethods) PrivilegeKeyAdd(ttype, id1, id2 int, options ...CmdArg)

// OnlineClient represents a client online on a virtual server.
type OnlineClient struct {
ID int `ms:"cid"`
ID int `ms:"clid"`
ChannelID int `ms:"cid"`
DatabaseID int `ms:"client_database_id"`
Nickname string `ms:"client_nickname"`
Type int `ms:"client_type"`
Away bool `ms:"client_away"`
AwayMessage string `ms:"client_away_message"`
}

// DetailedOnlineClient extends OnlineClient with all information from the
// clientinfo server query command about a client online on a virtual server.
type DetailedOnlineClient struct {
OnlineClient `ms:",squash"`

// Creation date and time of the clients first connection to the server.
Created time.Time `ms:"client_created"`
// Termination date and time of the clients last connection to the server.
// Note: The manual claims this to be the *creation* date and time but the
// values received while testing suggest otherwise.
LastConnected time.Time `ms:"client_lastconnected"`

// Empty or a string of 32 hexadecimal characters. Indicates whether the
// client has set an avatar or not.
// What platform the client is running on. For example "Windows".
Base64HashClientUID string `ms:"client_base64HashClientUID"`
// For example "83.123.45.6". According to the manual this is always IPv4.
ConnectionClientIP string `ms:"connection_client_ip"`
// For example "DE" for Germany.
Country string `ms:"client_country"`
// Takes the form of "/channelID"
DefaultChannel string `ms:"client_default_channel"`
DefaultToken string `ms:"client_default_token"`
Description string `ms:"client_description"`
LoginName string `ms:"client_login_name"`
NicknamePhonetic string `ms:"client_nickname_phonetic"`
SecurityHash string `ms:"client_security_hash"`
TalkRequestMsg string `ms:"client_talk_request_msg"`
UniqueIdentifier string `ms:"client_unique_identifier"`
FlagAvatar string `ms:"client_flag_avatar"`
Platform string `ms:"client_platform"`
// Which version of the Teamspeak client application this client uses.
Version string `ms:"client_version"`
VersionSign string `ms:"client_version_sign"`

// Milliseconds since the client connected to the server.
ConnectionConnectedTime int64 `ms:"connection_connected_time"`
ConnectionBytesReceivedTotal int64 `ms:"connection_bytes_received_total"`
ConnectionBytesSentTotal int64 `ms:"connection_bytes_sent_total"`
ConnectionPacketsReceivedTotal int64 `ms:"connection_packets_received_total"`
ConnectionPacketsSentTotal int64 `ms:"connection_packets_sent_total"`
// Milliseconds since the client did something, for example sending
// a message, muting themselves or talking.
IdleTime int64 `ms:"client_idle_time"`
MonthBytesDownloaded int64 `ms:"client_month_bytes_downloaded"`
MonthBytesUploaded int64 `ms:"client_month_bytes_uploaded"`
TotalBytesDownloaded int64 `ms:"client_total_bytes_downloaded"`
TotalBytesUploaded int64 `ms:"client_total_bytes_uploaded"`

// Current bandwidth used for outgoing file transfers (Bytes/s)
ConnectionFiletransferBandwidthSent int `ms:"connection_filetransfer_bandwidth_sent"`
// Current bandwidth used for incoming file transfers (Bytes/s)
ConnectionFiletransferBandwidthReceived int `ms:"connection_filetransfer_bandwidth_received"`
// Average bandwidth used for outgoing data in the last second (Bytes/s)
ConnectionBandwidthSentLastSecondTotal int `ms:"connection_bandwidth_sent_last_second_total"`
// Average bandwidth used for outgoing data in the last minute (Bytes/s)
ConnectionBandwidthSentLastMinuteTotal int `ms:"connection_bandwidth_sent_last_minute_total"`
// Average bandwidth used for incoming data in the last second (Bytes/s)
ConnectionBandwidthReceivedLastSecondTotal int `ms:"connection_bandwidth_received_last_second_total"`
// Average bandwidth used for incoming data in the last minute (Bytes/s)
ConnectionBandwidthReceivedLastMinuteTotal int `ms:"connection_bandwidth_received_last_minute_total"`
ChannelGroupID int `ms:"client_channel_group_id"`
ChannelGroupInheritedChannelID int `ms:"client_channel_group_inherited_channel_id"`
NeededServerqueryViewPower int `ms:"client_needed_serverquery_view_power"`
Servergroups []int `ms:"client_servergroups"`
TalkPower int `ms:"client_talk_power"`
// How often the client has connected to the server.
Totalconnections int `ms:"client_totalconnections"`

// CRC32 checksum of the client icon
IconID uint32 `ms:"client_icon_id"`

// False if the client has their microphone disabled, for example
// because they unplugged it. Do not confuse this with InputMuted.
InputHardware bool `ms:"client_input_hardware"`
// True if the client has their microphone muted
InputMuted bool `ms:"client_input_muted"`
IsChannelCommander bool `ms:"client_is_channel_commander"`
IsPrioritySpeaker bool `ms:"client_is_priority_speaker"`
IsRecording bool `ms:"client_is_recording"`
// False if the client has their speakers disabled, for example
// because they are unplugged. Do not confuse this with OutputMuted.
OutputHardware bool `ms:"client_output_hardware"`
// True if the client has their speakers muted
OutputMuted bool `ms:"client_output_muted"`
OutputOnlyMuted bool `ms:"client_outputonly_muted"`
TalkRequest bool `ms:"client_talk_request"`

// Indicates whether the client is able to talk or not.
//TODO(Henner25): This is always 0, even if my talk power is high enough?
IsTalker bool `ms:"client_is_talker"`

//TODO(Henner25): I always got "overwolf=0". I assume it is a list of "key=value|key2=value2...". In that case, the type of this should be a map (or maybe an array, if it's only true/false).
//Badges string `ms:"client_badges"`

//TODO(Henner25): I never managed to receive any value for this field
//MetaData interface{} `ms:"client_meta_data"`
}

// ClientInfo returns detailed information about a single online client.
func (s *ServerMethods) ClientInfo(clientID int) (*DetailedOnlineClient, error) {
var client DetailedOnlineClient
if _, err := s.ExecCmd(NewCmd("clientinfo").WithArgs(NewArg("clid", clientID)).WithResponse(&client)); err != nil {
return nil, err
}

client.ID = clientID // the clientinfo command does not include the clid in the result set
return &client, nil
}

// ClientList returns a list of online clients.
func (s *ServerMethods) ClientList() ([]*OnlineClient, error) {
var clients []*OnlineClient
Expand Down
81 changes: 77 additions & 4 deletions server_cmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ func TestCmdsServer(t *testing.T) {
return
}
expected := &Server{
Status: "template",
MaxClients: 32,
Name: "Test Server",
Status: "template",
MaxClients: 32,
Name: "Test Server",
AntiFloodPointsNeededCommandBlock: 150,
AntiFloodPointsNeededIPBlock: 250,
AntiFloodPointsTickReduce: 5,
Expand Down Expand Up @@ -251,6 +251,77 @@ func TestCmdsServer(t *testing.T) {
assert.Equal(t, expected, channels)
}

clientinfo := func(t *testing.T) {
client, err := c.Server.ClientInfo(8)
if !assert.NoError(t, err) {
return
}

expected := &DetailedOnlineClient{
OnlineClient: OnlineClient{
ID: 8,
ChannelID: 20,
DatabaseID: 8,
Nickname: "Rabe85",
Type: 0,
Away: false,
AwayMessage: "",
},
IdleTime: 28122,
UniqueIdentifier: "P5H2hrN6+gpQI4n/dXp3p17vtY0=",
Version: "3.0.0-alpha24 [Build: 8785] (UI: 8785)",
Platform: "Windows",
InputMuted: false,
OutputMuted: false,
OutputOnlyMuted: false,
InputHardware: true,
OutputHardware: true,
DefaultChannel: "/20",
IsRecording: false,
VersionSign: "+/BWvaeokGg4YkO1v3ouZB5vtIIgUZ5bM5cRfxBstfnHUdro2ja+5b+3sFUzEy8/vvEISXVD6U95blTb638MCQ==",
SecurityHash: "",
LoginName: "",
ChannelGroupID: 8,
Servergroups: []int{6, 10},
Created: time.Unix(1503431624, 0),
LastConnected: time.Unix(1530383977, 0),
Totalconnections: 138,
FlagAvatar: "dd213abf2a94396ece544b22c4e56821",
TalkPower: 75,
TalkRequest: false,
TalkRequestMsg: "",
Description: "",
IsTalker: false,
MonthBytesUploaded: 0,
MonthBytesDownloaded: 0,
TotalBytesUploaded: 0,
TotalBytesDownloaded: 3014720,
IsPrioritySpeaker: true,
NicknamePhonetic: "rabeh",
NeededServerqueryViewPower: 75,
DefaultToken: "",
IconID: 0,
IsChannelCommander: true,
Country: "DE",
ChannelGroupInheritedChannelID: 20,
Base64HashClientUID: "kdohhblmninnfhaecihcijemaigdnkdhgjllefed",
ConnectionFiletransferBandwidthSent: 0,
ConnectionFiletransferBandwidthReceived: 0,
ConnectionPacketsSentTotal: 46880,
ConnectionBytesSentTotal: 6426774,
ConnectionPacketsReceivedTotal: 14098,
ConnectionBytesReceivedTotal: 1644574,
ConnectionBandwidthSentLastSecondTotal: 81,
ConnectionBandwidthSentLastMinuteTotal: 92,
ConnectionBandwidthReceivedLastSecondTotal: 83,
ConnectionBandwidthReceivedLastMinuteTotal: 97,
ConnectionConnectedTime: 2084247,
ConnectionClientIP: "83.123.45.6",
}

assert.Equal(t, expected, client)
}

clientlist := func(t *testing.T) {
clients, err := c.Server.ClientList()
if !assert.NoError(t, err) {
Expand All @@ -259,7 +330,8 @@ func TestCmdsServer(t *testing.T) {

expected := []*OnlineClient{
{
ID: 7,
ID: 5,
ChannelID: 7,
DatabaseID: 40,
Nickname: "ScP",
Type: 0,
Expand Down Expand Up @@ -308,6 +380,7 @@ func TestCmdsServer(t *testing.T) {
{"serverrequestconnectioninfo", serverrequestconnectioninfo},
{"instanceinfo", instanceinfo},
{"channellist", channellist},
{"clientinfo", clientinfo},
{"clientlist", clientlist},
{"clientdblist", clientdblist},
}
Expand Down