diff --git a/.travis.yml b/.travis.yml index 8222cf5..bb96cd9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ install: script: - go test -v -race ./... - - gometalinter.v1 --cyclo-over=16 ./... + - gometalinter.v1 --cyclo-over=17 ./... diff --git a/helpers.go b/helpers.go index 9124c0c..5b3e74d 100644 --- a/helpers.go +++ b/helpers.go @@ -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 { @@ -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 { + 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 diff --git a/mockserver_test.go b/mockserver_test.go index 580e74b..ae565ad 100644 --- a/mockserver_test.go +++ b/mockserver_test.go @@ -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", diff --git a/server_cmds.go b/server_cmds.go index 9ba2b9a..cfbbe9c 100644 --- a/server_cmds.go +++ b/server_cmds.go @@ -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. @@ -340,7 +340,8 @@ 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"` @@ -348,6 +349,116 @@ type OnlineClient struct { 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 diff --git a/server_cmds_test.go b/server_cmds_test.go index 70e029d..03e6b2d 100644 --- a/server_cmds_test.go +++ b/server_cmds_test.go @@ -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, @@ -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) { @@ -259,7 +330,8 @@ func TestCmdsServer(t *testing.T) { expected := []*OnlineClient{ { - ID: 7, + ID: 5, + ChannelID: 7, DatabaseID: 40, Nickname: "ScP", Type: 0, @@ -308,6 +380,7 @@ func TestCmdsServer(t *testing.T) { {"serverrequestconnectioninfo", serverrequestconnectioninfo}, {"instanceinfo", instanceinfo}, {"channellist", channellist}, + {"clientinfo", clientinfo}, {"clientlist", clientlist}, {"clientdblist", clientdblist}, }