Advice for programming structure for a chatbot

Goodday, I apologize if this is not the right section to post this in, but here goes:

I currently have a program written in C#, a network which consists of several bots (handpicked by specific streamers) which it hosts.

Each bot has 1 socket (SmartIrc4Net) and can handle multiple channels.

It can:

  1. parse messages
  2. maintain channel metadata
  3. communicate with a database
  4. manage viewers as users / add / save / edit
  5. commands

Can I do these things better/more efficient/??:

  1. parse every message and determine if its a command
  2. add user (viewer) to db if it doesnt exist on every message/join/part
  3. load user in message event
  4. (re)load channel data every 10 minutes (game, title, followers, viewers)
  5. sub chat notif (parses twitchnotify message on message event)

Screenshot of what I have so far:

http://i.gyazo.com/08b2a0a6dc8160f76fcefebe9c0c5031.png

Even if some users on this forum have experience with how to do these things, it is impossible to answer these questions without showing some code you wish to be improved upon.

Fair enough, how about some general directions/suggestions on the best approach?

I believe the lack of responses is again attributed to your lack of specificity. Your optimal solution will always depend on the way your current system is set up, but I will try to give some pointers.

Parsing messages is best done with a tokenizer. I made my own, but there should be libraries in your language you can use. When the message is isolated, you simply match the message to your command structure. If the structure has complex parameters (e.g. !addcom -ul=mod !hello hello @user@!) then simple string matching might need to be replaced by regular expressions.

Grabbing users from messages are also done with tokenizers, and regular expressions in regard to JOIN/PART.

Channel data is loaded through the API, there is a lot of documentation and discussions on this in this forum. For parsing twitchnotify messages, I suggest trying out @night’s regular expression.

Thanks for your reply.
How can I be more specific?

Edit:

  1. How should I handle multiple channels for each bot?
    Do I have one connection for every channel or just one connection for all of them.

  2. What exactly is the use of these chat servers for bots?
    I will have to rewrite my code if it works how I think it does (seperate IRC servers). So instead of having a Bot as parent and Channel as child I’d have to reverse that.

I’m just lookiing for guidance on these things, I wonder if I’m doing it correctly, where can things be improved etc.

This is the main code that I use for connecting to IRC and joining channel(s):

    public void DoMainWork(object sender, DoWorkEventArgs e)
    {
        Irc.SendDelay = 200;
        Irc.Encoding = Encoding.UTF8;
        Irc.AutoRetry = true;
        Irc.AutoReconnect = true;
        Irc.AutoRelogin = true;
        Irc.AutoRejoin = true;
        Irc.ActiveChannelSyncing = true;

        Irc.OnOp += OnOp;
        Irc.OnDeop += OnDeop;
        Irc.OnConnected += OnConnected;
        Irc.OnConnecting += OnConnecting;
        Irc.OnDisconnected += OnDisconnected;
        Irc.OnError += OnError;
        Irc.OnRawMessage += OnRawMessage;
        Irc.OnJoin += OnJoin;
        Irc.OnPart += OnPart;
        Irc.OnChannelMessage += OnChannelMessage; 

        Irc.Connect(Config.IrcHost, Config.IrcPort);

        AddLogMessage("Authenticating", "");
        Irc.Login(Nick, "AccursedNetwork Bot", 0, Nick, "oauth:" + Password);

        foreach (var channel in Channels)
        {
            AddLogMessage("[{0}] joining {1}.".InjectWith(Nick, channel.NamePrefixed));
            Irc.RfcJoin(channel.NamePrefixed);
        }

        while (!_mainWorker.CancellationPending)
        {
            if (Working)
            {
                try
                {
                    Irc.ListenOnce(false);
                }
                catch (ArgumentException)
                {
                }

                Uptime = Uptime + TimeSpan.FromMilliseconds(50);
                Iterations++;
            }

            Thread.Sleep(50);
        }
        Irc.Disconnect();
    }

So basically it hooks all of the events and then processes mostly everything with string comparisons.

OnChannelMessage

private void OnChannelMessage(object s, IrcEventArgs e)
{
    if (!Working) 
        return;

    var channel = TwitchChannels.Find(c => c.NamePrefixed == e.Data.Channel);
    if (channel == null)
        return;

    var user = channel.Users.Find(u => u.Name == e.Data.Nick) ?? new User(channel.Id, e.Data.Nick);

    if (channel.Mods.Contains(user.Name.Lc()) || Irc.GetChannel(channel.NamePrefixed).Ops.ContainsKey(user.Name.Lc()))
        user.AccessLevel = AccessLevel.Operator;

    user.AccessLevel = user.Name == channel.Name ? AccessLevel.Owner : user.AccessLevel;
    user.MessagesSent++;
    user.NeedSave = true;

    if (channel.StealthMode)
        return;

    var message = e.Data.Message;
    if (message.IsNullOrEmpty())
        return;

    string[] parts;
    if (channel.Live && (e.Data.Nick == "twitchnotify" && message.Contains("subscribed")))
    {
        parts = e.Data.Message.Split(' ');
        var nick = parts[0];
        if (message.Contains("months in a row!"))
        {
            var months = parts[3];
            SendMessage(
                "{0} - thank you so much for the continued support with a subscription ({1} months)! <3"
                    .InjectWith(nick.UcFirst(), months), channel.NamePrefixed);
            return;
        }

        SendMessage("{0} - thank you so much for subscribing! <3".InjectWith(nick.UcFirst()),
            channel.NamePrefixed);
        return;
    }

    var isCommand = message.StartsWith("!");
    if (!isCommand) 
        return;

    parts = e.Data.MessageArray;
    var commandName = parts[0].Lc();

    if (commandName == "!{0}".InjectWith(channel.CurrencyName.Lc()))
    {
        SendMessage("{0} -> you have [{1}] {2}!".InjectWith(user.Name, user.Currency, channel.CurrencyName.UcFirst()), channel.NamePrefixed);
        return;
    }

    if (commandName == "!reload")
    {
        channel.Load();
        SendMessage("{0} -> channel configuration reloaded.".InjectWith(user.Name), channel.NamePrefixed);
        return;
    }

    var command = channel.Commands.Find(cmd => cmd.Name == commandName);
    if (command == null)
        return;

    if (user.AccessLevel < command.AccessLevel)
    {
        SendMessage("{0} -> you do not have access to this command.".InjectWith(user.Name), channel.NamePrefixed);
        return;
    }
    var commandText = command.Text;
    if (commandText.IsNullOrEmpty())
        return;

    var commandArgsCount = Regex.Matches(commandText, @"\{[0-9]+\}", RegexOptions.Singleline | RegexOptions.IgnoreCase).Count;
    var messageArgsCount = 0;

    var temp = new string[message.Split(' ').Count()-1];
    for (var i = 0; i < temp.Count(); i++)
    {
        temp[i] = message.Split(' ')[i+1];
    }

    var messageArgs = temp;
    messageArgsCount = messageArgs.Count();
 
    if (messageArgsCount < commandArgsCount)
    {
        SendMessage("{0} -> incorrect syntax for command {1}.".InjectWith(user.Name, commandName), channel.NamePrefixed);
        return;
    }

    // ReSharper disable once CoVariantArrayConversion
    commandText = commandText.InjectWith(messageArgs);
    SendMessage(commandText, channel.NamePrefixed);
}

As you can see, every channel is a Channel and every bot is a TwitchBot and every new/existing user is a User.

This is what happens on JOIN/PART (for part in reverse):

private void OnJoin(object s, JoinEventArgs e)
{
    var channel = TwitchChannels.Find(c => c.NamePrefixed == e.Data.Channel);
    if (channel == null)
        return;

    var user = channel.Users.Find(u => u.Name == e.Data.Nick);
    if (user != null) 
        return;

    user = new User(channel.Id, e.Data.Nick);

    AddLogMessage("user {0} joined the channel".InjectWith(user.Name));
    channel.Users.Add(user);
}

This depends on three things:

  1. If you need to receive the JOIN- and PART messages you need a connection in default or “TWITCHCLIENT 1” mode (which you already have). Read more in this thread for example. If you also need user metadata from jtv, you need a separate connection for the same channel in “TWITCHCLIENT 3” mode. I recommend using IRCv3 instead. This way you also don’t need to parse MODE messages and keep a record of current mods.
  2. If you want to send messages beyond the limit of 100 messages every 30 seconds (for modded users, per connection), you need additional connections so that you don’t get IP banned. Even if this is not something you want, I recommend that you guard yourself by either rate limiting or automating more connections.
  3. There is also a remote possibility that you will be disconnected from a connection if it receives too many messages. To be safe you can spread it evenly so that channels with a heavy load are on a single connection and several channels with a light load are on another.

Because of this complicated nature, I think the best approach would not be a parent-child relationship, but the current configuration of connections should be hidden from the bots and their channels in a separate Connection Manager of sorts.

My guess is that you would be fine with irc.twitch.tv, as this works as a domain name that would be translated into a random available IP address. If that server goes down, reconnecting to the same domain should give you another address. I don’t see how this affects your class hierarchy.

1 Like

To clarify, what I have in mind is a class that abstracts the multiple connections your bots need to communicate with the server:

  • At the beginning, the manager opens a connection/socket for every mode that you require (see previous post).
  • At any time one of your bots wants to join a new channel, it joins within a connection that is below a certain incoming load that you have specified. If there is none available, it creates a new one.
  • Every time a bot wants to send a message it distributes this to a connection that is within the outgoing limit. I monitor the message rate like this. This does not have to be through a connection that has joined the target channel, since you can send messages to channels you haven’t joined. If none is available you create a new connection, but to minimize this delay you can do this some time before reaching the cap.
  • A timed method removes unnecessary connections and/or redistributes joined channels over connections.
  • Incoming messages are distributed to the bots that listens to this channel.

I’ve recently started developing within Twitch, and currently don’t need multiple connections. These are just some ideas that I might implement in the future.

1 Like

Wow! that’s alot of information to process, it sounds very difficult to implement though.
Thank you.

This was just assuming you have a huge system that needs careful regulation :smile:
Until your bot reaches a scale that creates these problems, you will be fine with a single connection for your entire system.

Edit: the only problem I imagine smaller bots encounter is the outgoing message limit for PRIVMSG. This could easily be reached if a large channel finds it entertaining to spam a certain command for a respone. A simple safe guard would be to limit the time between sent messages, like three every second or less.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.