TheHolyWaffle / TeamSpeak-3-Java-API

A Java wrapper of TeamSpeak's 3 server query API.
MIT License
306 stars 107 forks source link

Random Join Events #302

Closed PeXArtZ closed 5 years ago

PeXArtZ commented 5 years ago

Hello,

I made a bot a while ago and got a online counter in it, which shows the currently online users in a channel. This worked good, since I noticed now, that I got a lot of error messages from it. I got up to 8 (i guess) error messages per minute, which all said, that this channel name is already in use. To find out I created a new channel and tested it with this code: `

public class OnlineCount {

public static int ints = 0;

public static void start() {

    Load.api.registerEvent(TS3EventType.SERVER);

    Load.api.addTS3Listeners(new TS3EventAdapter() {
        @Override
        public void onClientJoin(ClientJoinEvent e) {

            final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
            executor.schedule(new Runnable() {
                @Override
                public void run() {
                    ints++;
                    Load.api.editChannel(76, ChannelProperty.CHANNEL_NAME, String.valueOf(ints));
                }
            }, 3, TimeUnit.SECONDS);

        }

`

After that, this happened: https://www.frostiqz.de/images/Screenshot_1.png Nobody joined or left, but the bot kept doing its stuff.

Heres the original line I used to edit the channel: Load.api.editChannel(5, ChannelProperty.CHANNEL_NAME , "[cspacer]ยป Aktuell Online: " + (Load.api.getHostInfo().getTotalClientsOnline()) + "/32 ยซ");

rogermb commented 5 years ago

Are you creating a ScheduledThreadPoolExecutor each time a client joins or leaves? O.o

Also, if you're accessing that "ints" variable from multiple threads (which you undoubtedly are if you keep creating new executors), you're going to run into concurrency issues, specifically data races.

But most likely, the cause of this issue is because you're re-registering the same event listener multiple times. Make sure that over the entire lifecycle of your TS3Query, you only register this specific event listener once.

PeXArtZ commented 5 years ago

The Thread was only for test purposes. Normally, Im using the last line in my post. I registered every event in a 'events.java' and got a new .java for every function. I noticed, that the bot is much slower, when I take all my code and paste it in one file. Im trying to register every event once, hope this works.

Henny022 commented 5 years ago

you register the event for server, and not for channel

PeXArtZ commented 5 years ago

I dont got much experience with java. You said "re-registering the same event listener multiple times". Do you mean: Load.api.addTS3Listeners(new TS3Listener()

or

Load.api.registerAllEvents();

or

Load.api.addTS3Listeners(new TS3EventAdapter()

I've registered all events now in my main file, but the error is still there.

rogermb commented 5 years ago

The first and third one. A TS3EventAdapter is just a TS3Listener with all methods being empty, so you don't have to override every single one.

Anyway - if you're new to Java, it would probably be a good idea if we could see the entirety of your code, as the error is most likely hiding in there somewhere ๐Ÿ˜„

PeXArtZ commented 5 years ago

Alright. Heres my code of the online counter:

`package de.frostiqz.cloudbot.events;

import com.github.theholywaffle.teamspeak3.api.ChannelProperty; import com.github.theholywaffle.teamspeak3.api.event.ClientJoinEvent; import com.github.theholywaffle.teamspeak3.api.event.ClientLeaveEvent; import com.github.theholywaffle.teamspeak3.api.event.TS3EventAdapter; import com.github.theholywaffle.teamspeak3.api.exception.TS3CommandFailedException; import de.frostiqz.cloudbot.Load; import de.frostiqz.cloudbot.storage.Data;

public class OnlineCount {

public static void start() {

    Load.api.registerEvent(TS3EventType.SERVER);

    Load.api.addTS3Listeners(new TS3EventAdapter() {
        @Override
        public void onClientJoin(ClientJoinEvent e) {
            try {
                Load.api.editChannel(5, ChannelProperty.CHANNEL_NAME , "[cspacer]ยป Aktuell Online: " + (Load.api.getHostInfo().getTotalClientsOnline()) + "/32 ยซ");
            } catch (TS3CommandFailedException ex) {
                System.out.println(Data.getPrefix() + "Fehler beim OnlineCounter (ClientJoin)");
                System.err.println(ex.getMessage());
            }
        }

        @Override
        public void onClientLeave(ClientLeaveEvent e) {
            // Online Count
            try {
                Load.api.editChannel(5, ChannelProperty.CHANNEL_NAME , "[cspacer]ยป Aktuell Online: " + (Load.api.getHostInfo().getTotalClientsOnline()) + "/32 ยซ");
            } catch (TS3CommandFailedException ex) {
                System.out.println(Data.getPrefix() + "Fehler beim OnlineCounter (ClientLeave)");
                System.err.println(ex.getMessage());
            }
        }
    });

}

}`

rogermb commented 5 years ago

That's pretty much still the same code shown above. I'm much more interested in the code that calls this OnlineCount.start() method of yours. And the code that calls that method. And so on, until we land in main(String[] args) ๐Ÿ˜„

(Also, use triple-backticks ( ` *3) for a proper multi-line code block)

PeXArtZ commented 5 years ago

Its my main method, which starts the OnlineCount. Heres the code (I deleted some stuff like passwords, etc):

package de.frostiqz.cloudbot;

import com.github.theholywaffle.teamspeak3.TS3Api;
import com.github.theholywaffle.teamspeak3.TS3Config;
import com.github.theholywaffle.teamspeak3.TS3Query;
import com.github.theholywaffle.teamspeak3.api.wrapper.Client;
import de.frostiqz.cloudbot.console.Console;
import de.frostiqz.cloudbot.events.*;
import de.frostiqz.cloudbot.storage.Data;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;

public class Load {

    public static final TS3Config config = new TS3Config();
    public static final TS3Query query = new TS3Query(config);
    public static final TS3Api api = query.getApi();
    public static final ArrayList<String> words = new ArrayList<>();

    public static void main(String[] args) throws IOException, URISyntaxException {

        // Start Screen
        System.out.println("");
        System.out.println("   _____ _                 _ _           _     _             _____    __   __");
        System.out.println("  / ____| |               | | |         | |   | |           |  __ \\   \\ \\ / /");
        System.out.println(" | |    | | ___  _   _  __| | |__   ___ | |_  | |__  _   _  | |__) |__ \\ V / ");
        System.out.println(" | |    | |/ _ \\| | | |/ _` | '_ \\ / _ \\| __| | '_ \\| | | | |  ___/ _ \\ > < ");
        System.out.println(" | |____| | (_) | |_| | (_| | |_) | (_) | |_  | |_) | |_| | | |  |  __// . \\ ");
        System.out.println("  \\_____|_|\\___/ \\__,_|\\__,_|_.__/ \\___/ \\__| |_.__/ \\__, | |_|   \\___/_/ \\_\\");
        System.out.println("                                                      __/ |                  ");
        System.out.println("                                                     |___/                   ");
        System.out.println("");
        System.out.println(Data.getPrefix() + "Cloudbot by Tobias - PeX");
        System.out.println("");

        // Starting the app
        System.out.println(Data.getPrefix() + "Cloudbot starting ...");

        // Setting the Host IP
        config.setHost("127.0.0.1");
        System.out.println(Data.getPrefix() + "Host IP has been set to localhost ... ");

        // Query is connecting
        query.connect();
        System.out.println(Data.getPrefix() + "Query connected ...");

        // Setting the floodrate to unlimited
        config.setFloodRate(TS3Query.FloodRate.UNLIMITED);
        System.out.println(Data.getPrefix() + "Floodrate has been set to unlimited ...");

        // Query is logging in
        api.login("serveradmin", "password");
        System.out.println(Data.getPrefix() + "API logged in ...");

        // Selecting the virtual server
        api.selectVirtualServerByPort(9987);
        System.out.println(Data.getPrefix() + "Virtual Server selected ...");

        // Setting the nickname
        api.setNickname("Frostiqz");
        System.out.println(Data.getPrefix() + "Nickname set ...");

        // Loading the events
        Load.api.registerAllEvents();
        Events.loadEvents();
        System.out.println(Data.getPrefix() + "Events loaded ...");

        // Loading the AFKMover
        AFKMover.start();
        System.out.println(Data.getPrefix() + "AFKMover loaded ...");

        // Loading the forbidden words and nickname checker
        addWords();
        checkNickname.start();
        System.out.println(Data.getPrefix() + "Nickname Checker loaded ...");

        // Loading the chatbot
        ChatBot.start();
        System.out.println(Data.getPrefix() + "Chatbot loaded ...");

        // Loading OnlineCount
        OnlineCount.start();
        System.out.println(Data.getPrefix() + "OnlineCount loaded ...");

        // Loading Support
        Support.start();
        System.out.println(Data.getPrefix() + "Support loaded ...");

        // Loading the welcome message
        WelcomeMessage.start();
        System.out.println(Data.getPrefix() + "Welcome message loaded ...");

        System.out.println("");
        System.out.println(Data.getPrefix() + "Cloudbot sucessfully loaded!");

        // Starting the console manager
        System.out.println(Data.getPrefix() + "The console manager started. You can now start typing in commands.");
        Console.start();
    }

    // forbidden words
    public static void addWords() {
        // Some pretty bad words :(
    }

    // Nickname Checker
    public static void CheckClient(Client c) {
        String name = c.getNickname().toLowerCase();
        if (words.contains(name)) {
            api.kickClientFromServer("Der Nickname " + c.getNickname() + " ist auf diesem Server nicht erlaubt!", c);

            // Console Log
            System.out.println(Data.getPrefix() + c.getNickname() + " got kicked for bad Nickname");
        }
    }

    // Channel Checker
    public static void CheckChannel(String id, String name) {
        Client c = api.getClientByUId(id);
        if (words.contains(name.toLowerCase())) {
            api.kickClientFromServer("Dieser Channel-Name ist auf diesem Server nicht erlaubt!", c);

            // Console Log
            System.out.println(Data.getPrefix() + c.getNickname() + " got kicked for bad Channel name");
        }
    }
}

I got an events.java too, here is it:


package de.frostiqz.cloudbot.events;

import com.github.theholywaffle.teamspeak3.api.event.*;
import de.frostiqz.cloudbot.Load;
import de.frostiqz.cloudbot.storage.Data;

public class Events {

    public static void loadEvents() {
        Load.api.addTS3Listeners(new TS3Listener() {

            @Override
            public void onChannelCreate(ChannelCreateEvent e) {

                // Channel Checker
                Load.CheckChannel(e.getInvokerUniqueId(), Load.api.getChannelInfo(e.getChannelId()).getName());

                // Console Log
                System.out.println(Data.getPrefix() + e.getInvokerName() + " created a channel named " + Load.api.getChannelInfo(e.getChannelId()).getName());
            }

            @Override
            public void onChannelDeleted(ChannelDeletedEvent e) {

                //Console Log
                System.out.println(Data.getPrefix() + e.getInvokerName() + " deleted a channel named " + Load.api.getChannelInfo(e.getChannelId()).getName());
            }

            @Override
            public void onChannelDescriptionChanged(ChannelDescriptionEditedEvent e) {

                // Console Log
                System.out.println(Data.getPrefix() + e.getInvokerName() + " changed the channel description of " + Load.api.getChannelInfo(e.getChannelId()).getName());
            }

            @Override
            public void onChannelEdit(ChannelEditedEvent e) {

                // Console Log
                System.out.println(Data.getPrefix() + e.getInvokerName() + " edited the channel " +  Load.api.getChannelInfo(e.getChannelId()).getName());
            }

            @Override
            public void onChannelMoved(ChannelMovedEvent e) {

                // Console Log
                System.out.println(Data.getPrefix() + e.getInvokerName() + " moved the channel " + Load.api.getChannelInfo(e.getChannelId()).getName());
            }

            @Override
            public void onChannelPasswordChanged(ChannelPasswordChangedEvent e) {

                // Console Log
                System.out.println(Data.getPrefix() + e.getInvokerName() + " changed the password of channel " + Load.api.getChannelInfo(e.getChannelId()).getName());
            }

            @Override
            public void onClientJoin(ClientJoinEvent e) {
                }

            @Override
            public void onClientLeave(ClientLeaveEvent e) {

            }

            @Override
            public void onClientMoved(ClientMovedEvent e) {

            }

            @Override
            public void onPrivilegeKeyUsed(PrivilegeKeyUsedEvent e) {

                // Console Log
                System.out.println(Data.getPrefix() + e.getInvokerName() + " used a privilege key.");
            }

            @Override
            public void onServerEdit(ServerEditedEvent e) {

                // Console Log
                System.out.println(Data.getPrefix() + e.getInvokerName() + " edited the virtual server.");
            }

            @Override
            public void onTextMessage(TextMessageEvent e) {

            }

        });
    }
}

Im sorry if its too messy for you ๐Ÿ˜•

rogermb commented 5 years ago

So many people follow that YouTube tutorial, it's insane. And the guy doesn't even explain the core concepts correctly...


First of all, if you change the TS3Config object after passing it to a TS3Query object, nothing will happen.

public static final TS3Config config = new TS3Config();
public static final TS3Query query = new TS3Query(config);

is wrong and does the same thing as

public static final TS3Query query = new TS3Query(new TS3Config());

That isn't what you wanted.

You need to do things in the following order:

  1. Create a TS3Config object.
  2. Modify that TS3Config object.
  3. Pass the TS3Config object to the constructor of TS3Query.
  4. Forget that that TS3Config object ever existed.

(Or check out any of our examples)


Second, if you already call

api.registerAllEvents();

from your main method, you don't need to call registerEvent again from every single listener. Now I don't think that's what's causing the duplicate events, but it just results in unnecessary API calls.


Finally, I don't see anything in your code that would result in the even listener being added multiple times, at least not in these two classes. I wonder if the server is actually sending you the same event notification multiple times. (It really shouldn't!)

So what I'd like you to do is call config.setEnableCommunicationsLogging(true) before passing that TS3Config to the constructor of TS3Query. After setting that to true, all of the messages sent between the client and the server should be logged in your console.

If you then run your application again and get the same duplicated events, we can compare your application log with your TS3 server's log to determine what's really causing these issues ๐Ÿ˜„

PeXArtZ commented 5 years ago

Done. I dont know if thats right, but heres what im seeing in my console:

https://www.frostiqz.de/images/Screenshot_1.png

Heres my new Load.java:

package de.frostiqz.cloudbot;

import com.github.theholywaffle.teamspeak3.TS3Api;
import com.github.theholywaffle.teamspeak3.TS3Config;
import com.github.theholywaffle.teamspeak3.TS3Query;
import com.github.theholywaffle.teamspeak3.api.wrapper.Client;
import de.frostiqz.cloudbot.console.Console;
import de.frostiqz.cloudbot.events.*;
import de.frostiqz.cloudbot.storage.Data;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;

public class Load {

    public static TS3Api api;
    private static TS3Config config;

    public static final ArrayList<String> words = new ArrayList<>();

    public static void main(String[] args) throws IOException, URISyntaxException {

        // Start Screen
        System.out.println("");
        System.out.println("   _____ _                 _ _           _     _             _____    __   __");
        System.out.println("  / ____| |               | | |         | |   | |           |  __ \\   \\ \\ / /");
        System.out.println(" | |    | | ___  _   _  __| | |__   ___ | |_  | |__  _   _  | |__) |__ \\ V / ");
        System.out.println(" | |    | |/ _ \\| | | |/ _` | '_ \\ / _ \\| __| | '_ \\| | | | |  ___/ _ \\ > < ");
        System.out.println(" | |____| | (_) | |_| | (_| | |_) | (_) | |_  | |_) | |_| | | |  |  __// . \\ ");
        System.out.println("  \\_____|_|\\___/ \\__,_|\\__,_|_.__/ \\___/ \\__| |_.__/ \\__, | |_|   \\___/_/ \\_\\");
        System.out.println("                                                      __/ |                  ");
        System.out.println("                                                     |___/                   ");
        System.out.println("");
        System.out.println(Data.getPrefix() + "Cloudbot by Tobias - PeX");
        System.out.println("");

        System.out.println(Data.getPrefix() + "Starting App...");

        // Ts3 Config
        config = new TS3Config();
        config.setHost("127.0.0.1");
        System.out.println(Data.getPrefix() + "Host IP set...");
        config.setEnableCommunicationsLogging(true);
        System.out.println(Data.getPrefix() + "Logging enabled...");
        config.setFloodRate(TS3Query.FloodRate.UNLIMITED);
        System.out.println(Data.getPrefix() + "Floodrate set to unlimited...");

        // Query Connect
        final TS3Query query = new TS3Query(config);
        query.connect();
        System.out.println(Data.getPrefix() + "Query connected...");

        // Api
        api = query.getApi();
        api.login("serveradmin", "password");
        System.out.println(Data.getPrefix() + "Logged in...");
        api.selectVirtualServerById(1);
        api.setNickname("Frostiqz");
        System.out.println(Data.getPrefix() + "Nickname set...");
        api.sendChannelMessage("Frostiqz Bot - Online!");

        // forbidden words
        addWords();
        checkNickname.start();
        System.out.println(Data.getPrefix() + "Nickname & Channel loaded...");

        // Events
        api.registerAllEvents();
        Events.loadEvents();
        System.out.println(Data.getPrefix() + "Events loaded...");

        // AFK Mover
        AFKMover.start();
        System.out.println(Data.getPrefix() + "AFKMover loaded...");

        // ChatBot
        ChatBot.start();
        System.out.println(Data.getPrefix() + "Chatbot loaded...");

        // Online Counter
        OnlineCount.start();
        System.out.println(Data.getPrefix() + "OnlineCount loaded...");

        // Support
        Support.start();
        System.out.println(Data.getPrefix() + "Support loaded...");

        // Welcome Message
        WelcomeMessage.start();
        System.out.println(Data.getPrefix() + "Welcome message loaded...");

        System.out.println("");
        System.out.println(Data.getPrefix() + "App sucessfully loaded!");

        // Console
        System.out.println(Data.getPrefix() + "Console manager startet. You can now start typing in commands.");
        Console.start();
    }

    // forbidden words list
    public static void addWords() {
        // Some pretty bad words :(
    }

    // Nickname Checker
    public static void CheckClient(Client c) {
        String name = c.getNickname().toLowerCase();
        if (words.contains(name)) {
            api.kickClientFromServer("Der Nickname " + c.getNickname() + " ist auf diesem Server nicht erlaubt!", c);

            // Console Log
            System.out.println(Data.getPrefix() + c.getNickname() + " got kicked for bad Nickname");
        }
    }

    // Channel Checker
    public static void CheckChannel(String id, String name) {
        Client c = api.getClientByUId(id);
        if (words.contains(name.toLowerCase())) {
            api.kickClientFromServer("Dieser Channel-Name ist auf diesem Server nicht erlaubt!", c);

            // Console Log
            System.out.println(Data.getPrefix() + c.getNickname() + " got kicked for bad Channel name");
        }
    }

}

I updated from 1.1 to 1.2 also (Is this version much faster? In 1.1 i got a delay of about 1-3 secs, now it reacts instantly)

rogermb commented 5 years ago

Done. I dont know if thats right, but heres what im seeing in my console:

Yeah, that looks correct. What you should look for are lines about notifycliententerview events. The question is whether those get duplicated or not. Might be a bit easier if you just write the whole program output to a file ๐Ÿ˜„ (i.e. use java -jar File.jar > out.txt or something along those lines)

Heres my new Load.java

I think you got rid of the wrong field ๐Ÿ˜›

Keeping public static final TS3Query query; around for when you want to shut the query down (using query.exit()) is probably a good idea. You should get rid of that public static final TS3Config config; field though, and just use a local variable for that instead.

Either way, the current code should work ๐Ÿ˜ƒ

I updated from 1.1 to 1.2 also

Good idea ๐Ÿ˜„

(Is this version much faster? In 1.1 i got a delay of about 1-3 secs, now it reacts instantly)

Nah, that's because you were modifying TS3Config after using it in the TS3Query constructor, which you've now fixed. Before, you still only got the default flood rate that would wait 350ms between commands, and now you're actually using the UNLIMITED flood rate ๐Ÿ˜„

PeXArtZ commented 5 years ago

What you should look for are lines about notifycliententerview events. The question is whether those get duplicated or not.

Yeah. I got 8 of them in less than a minute. Nobody joined in this time

Nah, that's because you were modifying TS3Config after using it in the TS3Query constructor, which you've now fixed. Before, you still only got the default flood rate that would wait 350ms between commands, and now you're actually using the UNLIMITED flood rate ๐Ÿ˜„

Oh, thats nice. Thanks ๐Ÿ˜„

rogermb commented 5 years ago

Yeah. I got 8 of them in less than a minute. Nobody joined in this time

That seems like it should be impossible O.o

Could you send me the parts of that log file that contain those notifycliententerview events?

PeXArtZ commented 5 years ago
2019-01-09 23:30:17.713 [DEBUG] [event] < notifycliententerview cfid=0 ctid=8 reasonid=0 clid=35 client_unique_identifier=ServerQuery client_nickname=Unknown\sfrom\s37.187.252.194:59939 client_input_muted=0 client_output_muted=0 client_outputonly_muted=0 client_input_hardware=0 client_output_hardware=0 client_meta_data client_is_recording=0 client_database_id=5 client_channel_group_id=10 client_servergroups=1,10 client_away=0 client_away_message client_type=1 client_flag_avatar client_talk_power=10 client_talk_request=0 client_talk_request_msg client_description client_is_talker=0 client_is_priority_speaker=0 client_unread_messages=0 client_nickname_phonetic client_needed_serverquery_view_power=75 client_icon_id=0 client_is_channel_commander=0 client_country=FR client_channel_group_inherited_channel_id=8 client_badges client_myteamspeak_id client_integrations
2019-01-09 23:30:17.723 [DEBUG] [event] < notifycliententerview cfid=0 ctid=8 reasonid=0 clid=35 client_unique_identifier=ServerQuery client_nickname=Unknown\sfrom\s37.187.252.194:59939 client_input_muted=0 client_output_muted=0 client_outputonly_muted=0 client_input_hardware=0 client_output_hardware=0 client_meta_data client_is_recording=0 client_database_id=5 client_channel_group_id=10 client_servergroups=1,10 client_away=0 client_away_message client_type=1 client_flag_avatar client_talk_power=10 client_talk_request=0 client_talk_request_msg client_description client_is_talker=0 client_is_priority_speaker=0 client_unread_messages=0 client_nickname_phonetic client_needed_serverquery_view_power=75 client_icon_id=0 client_is_channel_commander=0 client_country=FR client_channel_group_inherited_channel_id=8 client_badges client_myteamspeak_id client_integrations
2019-01-09 23:30:17.748 [DEBUG] [event] < notifycliententerview cfid=0 ctid=8 reasonid=0 clid=36 client_unique_identifier=ServerQuery client_nickname=Unknown\sfrom\s37.187.252.194:60733 client_input_muted=0 client_output_muted=0 client_outputonly_muted=0 client_input_hardware=0 client_output_hardware=0 client_meta_data client_is_recording=0 client_database_id=5 client_channel_group_id=10 client_servergroups=1,10 client_away=0 client_away_message client_type=1 client_flag_avatar client_talk_power=10 client_talk_request=0 client_talk_request_msg client_description client_is_talker=0 client_is_priority_speaker=0 client_unread_messages=0 client_nickname_phonetic client_needed_serverquery_view_power=75 client_icon_id=0 client_is_channel_commander=0 client_country=FR client_channel_group_inherited_channel_id=8 client_badges client_myteamspeak_id client_integrations

That are the first 3 I got. For me, it looks like these 3 are just the same (Same IP-Adress).

api.getHostInfo().getTotalClientsOnline()

This shouldnt get server query clients as well, right?

rogermb commented 5 years ago

So you're getting events for server query clients, which you just can't see from your TS3 client (unless you check the checkbox in your bookmarks). In other words, no random / duplicated events after all. Phew!

And I didn't even notice that. You shouldn't be using getHostInfo(), but rather getServerInfo(). As far as I can see from my test with n=1, hostinfo's virtualservers_total_clients_online property doesn't seem to count server query clients, but only regular clients. Moreover, hostinfo includes information about the entire TS3 server instance, not just the virtual server you are on. I'm almost certain that's not the information you're looking for.

VirtualServer#getClientsOnline() is the number of both regular clients and server query clients on the current virtual server. There's also VirtualServer#getQueryClientsOnline(), so you can also figure out how many of those total clients are server queries and how many are regular clients ๐Ÿ˜ƒ

PeXArtZ commented 5 years ago

Thats possible. There are some server query clients from server lists. Maybe they are causing these errors.

Thanks you so much. I really appreciate your work in here.

rogermb commented 5 years ago

You're welcome ๐Ÿ˜„

Good luck with the development of your TS3 bot!