Bot doesn't see the server responses to its slash commands?

I’m working on a bot in C# and would like to get the list of vips and moderators. It seemed like a simple idea to just send /vips /mods or .vips .mods and parse the response. But the bot does not actually see the server replies for any slash or dot commands it sends.

Is there something I need to do to enable that? Or is it a limitation with how I’m sending/receiving the messages?

Searching this forum I do see people asking about their bot not seeing its own messages, and it sounds like you need to establish a second connection to do that. Is that also necessary to see the server responses for commands?

What I’m using is built on the minimalist sample in this tutorial:

It uses a single TcpClient and a single SslStream to establish a StreamReader and StreamWriter.

It works great to connect, send and receive messages, respond to PINGS and everything. The only issue I have is that it does not see replies to slash commands it sends.

Here’s the connection routine:

        const string ip = "irc.chat.twitch.tv";
        const int port = 6697;

        var tcpClient = new TcpClient();
        await tcpClient.ConnectAsync(ip, port);
        SslStream sslStream = new SslStream(
            tcpClient.GetStream(),
            false,
            new RemoteCertificateValidationCallback(ValidateServerCertificate),
            null
        );
        await sslStream.AuthenticateAsClientAsync(ip);
        streamReader = new StreamReader(sslStream);
        streamWriter = new StreamWriter(sslStream) { NewLine = "\r\n", AutoFlush = true };

        await streamWriter.WriteLineAsync($"PASS oauth:{chatToken}");
        await streamWriter.WriteLineAsync($"NICK {chatUsername}");
        await streamWriter.WriteLineAsync($"JOIN #{channel}");

That does connect and join.

Then I’m reading and monitoring the messages coming back from the server like so:

                line = await streamReader.ReadLineAsync();
                Console.WriteLine($"Read: {line}");

The oauth token I’m using to login looks like it has all the scopes that would be involved:
https://id.twitch.tv/oauth2/validate
{“client_id”:“XXX”,“login”:“pitgirl”,“scopes”:[“channel:moderate”,“channel_editor”,“chat:edit”,“chat:read”,“whispers:edit”,“whispers:read”],“user_id”:“552557131”,“expires_in”:0}

Any insight or suggestions, or more details I can provide from the code?

Do you have an example of how you are sending/requesting a slash command to be run?

You can try instead of /command using .command in case something is being misunderstood somewhere.

You should also request some capabilties

To help track information on response/error messaging

I’m sending the commands as PRIVMSGs as described in the documentation:

IRC Guide | Twitch Developers : Twitch slash commands are sent via PRIVMSG . The following table lists these commands and required scopes…

So in the code, the commands are sent just like any other message the bot sends.

        if (twitchChatMessage.Message.StartsWith("?slash",StringComparison.OrdinalIgnoreCase))
        {
            await _twitchBot.SendMessage("Sending slash / dot . commands: /vips .vips /mods .mods");
            await _twitchBot.SendMessage("/vips");
            await _twitchBot.SendMessage(".vips");
            await _twitchBot.SendMessage("/mods");
            await _twitchBot.SendMessage(".mods");
            await _twitchBot.SendMessage("Commands sent.");
        }

The above messages are formatted in SendMessage:

    public async Task SendMessage(string message, string channel=chatChannel)
    {
        await TryWriteLineAsync($"PRIVMSG #{channel} :{message}");
    }

And sent with the streamWriter:

            Console.WriteLine($"<{line}");
            try
            { 
                await streamWriter.WriteLineAsync(line);
                //throw(new Exception("Some f'cking thing in TryWriteLineAsync"));
            }
            catch (Exception ex)
            {

                Console.WriteLine("TwitchBot Exception! in TryWriteLineAsync", "red");
            ...

I’ve added those messages/commands to be sent when it joins, and also set a ?slash command to send them later just in case there was some race condition or whatever sending them too fast on connect. In both cases, the messages before/after the commands do show up in the chat for others to see - but the commands are not echoed, nor is their response seen by the bot on its streamReader.

So here’s what the console output looks like showing all the messages that are send/read on the streams:

And here’s what it looks like on the chat output:
Bot_Cmds_Chat_01

I’ll try adding the capabilities requests now and see if that changes anything.

Probably not the easiest commands to test with since they could trip for a variety of issues.

Your first send is sending before GLHF so you ran too soon probably should wait for the JOIN to complete.

If you send a second ?slash after the second command sent. does yoru script log and do the thing.

wondering if your reader isn’t reading right

Good questions, and thank you for your help. Are there other commands you suggest that would be easier to demonstrate the send/receive is working properly? I did try adding /help and .help but that also doesnt show anything.

I did add timestamps (just to the Console.WriteLine output - they are not included in whats being sent) to try and show more clearly when things happen.

Yes, the first send is right after the connect. But you will notice the “PitGirl is love.4/22/2022 9:05:08 AM” message is sent before the commands, and that does show up properly in the chat.

In this run, I wait until 9:06 so the ?slash command tries them again well after the connect/join. You can see in both cases the “Sending slash…” and “Commands sent” messages that wrap the commands are showing up in chat as expected.

Yes, I wonder if the reader is reading correctly as well - but it does get everything else: the messages on connect from :tmi.twitch.tv, the PRIVMSG from robertsmania to call the ?slash command, the subsequent PING, etc…

help isn’t a command.

But that should result in an “invalid command” error.

I don’t see anything obviously wrong here.

I do notice you didn’t get a ROOMSTATE on room join. But thats a CAP commands thing iirc

I ran my own test to confirm. It is working as expected here

init
opening
PONG :tmi.twitch.tv
:tmi.twitch.tv CAP * ACK :twitch.tv/commands
:tmi.twitch.tv CAP * ACK :twitch.tv/tags
:tmi.twitch.tv 001 barrycarlyon :Welcome, GLHF!
:tmi.twitch.tv 002 barrycarlyon :Your host is tmi.twitch.tv
:tmi.twitch.tv 003 barrycarlyon :This server is rather new
:tmi.twitch.tv 004 barrycarlyon :-
:tmi.twitch.tv 375 barrycarlyon :-
:tmi.twitch.tv 372 barrycarlyon :You are in a maze of twisty passages, all alike.
:tmi.twitch.tv 376 barrycarlyon :>
@badge-info=;badges=ambassador/1;color=#033700;display-name=BarryCarlyon;emote-sets=0,2,26,42,46,59,75,112,237,322,365,376,572,615,934,1057,3543,4163,4739,9318,10699,13338,13968,14755,19194,33563,197870,343806,400795,647181,739425,758802,300206296,300374282,300548756,300563799,300981539,301590448,301592777,301850800,301919687,302696035,302696036,302696037,302696038,302696039,302696040,302760317,303434564,318285583,318939165,339395276,343525774,368962233,371408320,381822777,384508024,395764043,401295080,411996111,418301196,420758870,436284473,436820331,437343514,443761993,453335100,463212988,466997024,472873131,477339272,477942371,483832563,488737509,490221517,492125462,498223190,537206155,564265402,592920959,610186276,1962979319,2001998065;user-id=15185913;user-type= :tmi.twitch.tv GLOBALUSERSTATE
:barrycarlyon!barrycarlyon@barrycarlyon.tmi.twitch.tv JOIN #barrycarlyon
join {
  tags: {},
  command: 'JOIN',
  message: undefined,
  raw: ':barrycarlyon!barrycarlyon@barrycarlyon.tmi.twitch.tv JOIN #barrycarlyon',
  user: 'barrycarlyon',
  room: '#barrycarlyon',
  action: false
}
Connected to Channel
:barrycarlyon.tmi.twitch.tv 353 barrycarlyon = #barrycarlyon :barrycarlyon
:barrycarlyon.tmi.twitch.tv 366 barrycarlyon #barrycarlyon :End of /NAMES list
@badge-info=;badges=broadcaster/1,ambassador/1;color=#033700;display-name=BarryCarlyon;emote-sets=0,2,26,42,46,59,75,112,237,322,365,376,572,615,934,1057,3543,4163,4739,9318,10699,13338,13968,14755,19194,33563,197870,343806,400795,647181,739425,758802,300206296,300374282,300548756,300563799,300981539,301590448,301592777,301850800,301919687,302696035,302696036,302696037,302696038,302696039,302696040,302760317,303434564,318285583,318939165,339395276,343525774,368962233,371408320,381822777,384508024,395764043,401295080,411996111,418301196,420758870,436284473,436820331,437343514,443761993,453335100,463212988,466997024,472873131,477339272,477942371,483832563,488737509,490221517,492125462,498223190,537206155,564265402,592920959,610186276,1962979319,2001998065;mod=0;subscriber=0;user-type= :tmi.twitch.tv USERSTATE #barrycarlyon
@emote-only=0;followers-only=-1;r9k=0;rituals=0;room-id=15185913;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #barrycarlyon
@badge-info=;badges=broadcaster/1,ambassador/1;client-nonce=a394e01dc161ed1dccd5eb89e71999d0;color=#033700;display-name=BarryCarlyon;emotes=;first-msg=0;flags=;id=0e7647c1-8089-429a-b37f-49d973a71686;mod=0;room-id=15185913;subscriber=0;tmi-sent-ts=1650644745599;turbo=0;user-id=15185913;user-type= :barrycarlyon!barrycarlyon@barrycarlyon.tmi.twitch.tv PRIVMSG #barrycarlyon :!ehco test
@badge-info=;badges=broadcaster/1,ambassador/1;client-nonce=6404bc5b2ac5cec2f764ef765fae4b84;color=#033700;display-name=BarryCarlyon;emotes=;first-msg=0;flags=;id=88403d6d-e8f0-4d52-ae3a-561bfea92f1c;mod=0;room-id=15185913;subscriber=0;tmi-sent-ts=1650644751786;turbo=0;user-id=15185913;user-type= :barrycarlyon!barrycarlyon@barrycarlyon.tmi.twitch.tv PRIVMSG #barrycarlyon :!echo test
Request test
@badge-info=;badges=broadcaster/1,ambassador/1;color=#033700;display-name=BarryCarlyon;emote-sets=0,2,26,42,46,59,75,112,237,322,365,376,572,615,934,1057,3543,4163,4739,9318,10699,13338,13968,14755,19194,33563,197870,343806,400795,647181,739425,758802,300206296,300374282,300548756,300563799,300981539,301590448,301592777,301850800,301919687,302696035,302696036,302696037,302696038,302696039,302696040,302760317,303434564,318285583,318939165,339395276,343525774,368962233,371408320,381822777,384508024,395764043,401295080,411996111,418301196,420758870,436284473,436820331,437343514,443761993,453335100,463212988,466997024,472873131,477339272,477942371,483832563,488737509,490221517,492125462,498223190,537206155,564265402,592920959,610186276,1962979319,2001998065;mod=0;subscriber=0;user-type= :tmi.twitch.tv USERSTATE #barrycarlyon
@badge-info=;badges=broadcaster/1,ambassador/1;client-nonce=e1ec59737ba5ab7ae56ce61258b41246;color=#033700;display-name=BarryCarlyon;emotes=;first-msg=0;flags=;id=587f4c33-fab0-4777-9558-f3ac588c5700;mod=0;room-id=15185913;subscriber=0;tmi-sent-ts=1650644756293;turbo=0;user-id=15185913;user-type= :barrycarlyon!barrycarlyon@barrycarlyon.tmi.twitch.tv PRIVMSG #barrycarlyon :!echo .vips
Request .vips
@msg-id=vips_success :tmi.twitch.tv NOTICE #barrycarlyon :The VIPs of this channel are: 42ndActual, BarryCarlyonBotTest.
@badge-info=;badges=broadcaster/1,ambassador/1;client-nonce=fdc123ea3f4fe046aeed34e017595c24;color=#033700;display-name=BarryCarlyon;emotes=;first-msg=0;flags=;id=b582a87d-42c1-40c4-b4f1-0d5b661a1f90;mod=0;room-id=15185913;subscriber=0;tmi-sent-ts=1650644764838;turbo=0;user-id=15185913;user-type= :barrycarlyon!barrycarlyon@barrycarlyon.tmi.twitch.tv PRIVMSG #barrycarlyon :!echo .mods
Request .mods
@msg-id=room_mods :tmi.twitch.tv NOTICE #barrycarlyon :The moderators of this channel are: 42ndbot, barrycarlyonbot, cohhilitionbot, cohhkittenbot, emilyelectronic1, eschaap, gecky, ghostcommanderbot, nuuriell

Bit more noise due to my emotesets and tags enabled.

Retesting without caps I get no response. So you need the COMMANDS CAPability

So after PASS but before JOIN

send

CAP REQ :twitch.tv/commands

And then .mods and .vips should work as expected

YES! The key was the commands capability request:

        await _twitchBot.SendCommand("CAP REQ :twitch.tv/commands");

After that, the commands work as expected.

Even the /help and .help do show the response as NOTICE messages.

Also, I find it makes no difference experimentally if the CAP REQ happens before or after the JOIN. That kind of makes sense since those requests are not channel specific?

Thank you for your help!!!

Just to wrap this up if anyone stumbles on this thread in the future trying to get /vips and /mods from chat, here’s what I ended up with to process the output.

Here line is the string that came back from streamReader.ReadLineAsync. There are two List<string> objects: vips and mods that get populated with the data from the NOTICEs.

                string[] split = line.Split(' ');
                //PING :tmi.twitch.tv
                //Respond with PONG :tmi.twitch.tv
                if (line.StartsWith("PING"))
                {
                    await TryWriteLineAsync($"PONG {split[1]}");
                }
                else if (split.Length > 2 && split[1] == "NOTICE")
                {
                    //:tmi.twitch.tv NOTICE #robertsmania :The moderators of this channel are: mod1, mod2, mod3, ... modX 
                    //:tmi.twitch.tv NOTICE #robertsmania :The VIPs of this channel are: vip1, vip2, vip3, .. vipX.
                    //                       ^^^^^^^^
                    //Grab the channel name here
                    int secondColonPosition = line.IndexOf(':', 1);//the 1 here is what skips the first character
                    string message = line.Substring(secondColonPosition + 1);//Everything past the second colon
                    string channel = split[2].TrimStart('#');

                    if (message.StartsWith("The VIPs of this channel are:"))
                    {
                        int thirdColonPosition = line.IndexOf(':', secondColonPosition + 1); 
                        string vipStr = line.Substring(thirdColonPosition + 2); //2 skips the space after the colon, should be list of VIPs
                        vipStr = vipStr.TrimEnd('.'); //VIPs do apper to have a period?
                        vips = vipStr.Split(new string[] {", "}, StringSplitOptions.None).ToList();
                    }
                    else if (message.StartsWith("The moderators of this channel are:"))
                    {
                        int thirdColonPosition = line.IndexOf(':', secondColonPosition + 1); 
                        string modsStr = line.Substring(thirdColonPosition + 2); //2 skips the space afte rthe colon, should be list of mods
                        modsStr = modsStr.TrimEnd('.'); //mods doesnt look like it has a period?
                        mods = modsStr.Split(new string[] {", "}, StringSplitOptions.None).ToList();
                    }
                } ...

And then goes on to process other types of messages.

I see that this topic is already marked as solved, but I see some things that are probably worth pointint/clearing out.

Also, I find it makes no difference experimentally if the CAP REQ happens before or after the JOIN. That kind of makes sense since those requests are not channel specific?

Just to add on top of that, it actually makes some difference but you won’t make much use of it - simply, sending CAPs before you send PASS message is the only way to receive GLOBALUSERSTATE message (which btw can be only sent once and only right after authenticating). But maybe this could turn out useful for you in the future, who knows.
P.S.: All channels on twitch have -n mode, which means you can send PRIVMSGs without joining any channel - same goes for messages like PRIVMSG #channel :/mods and you should also get your NOTICE back, even if you’re not JOINed the channel. Which also means that if you only care about getting lists of mods/vips you might not even need to JOIN any channels at all :slight_smile:

Just to wrap this up if anyone stumbles on this thread in the future trying to get /vips and /mods from chat, here’s what I ended up with to process the output.

@robertsmania In the snippet you’ve posted, you rely on the message’s text itself which could always change, so a way better solution would be making use of msg-id tags. For these, it’s necessary to also request twitch.tv/tags CAP, which btw is shown in Barry’s example (but instead of doing that in 2 separate outgoing CAP REQ ... messages, both CAPs can be all requested with just one: CAP REQ :twitch.tv/tags twitch.tv/commands).
With both capabilities, you should get more verbose but also way more reliable messages, like so:

# outgoing messages: >
# incoming messages: <

> PRIVMSG #zneixbot :.vips
< @msg-id=no_vips :tmi.twitch.tv NOTICE #zneixbot :This channel does not have any VIPs.
> PRIVMSG #zneix :.vips
< @msg-id=vips_success :tmi.twitch.tv NOTICE #zneix :The VIPs of this channel are: ALazyMeme, Amayii, CollapsingWave, Eltefan, IceCreamDataBase, Leppunen, NotKarar, PepegePaul, Smaczny, Somso2e, Supinic, TETYYS, TeoTheParty, TranRed, Tsoding, WiFeSeN, YOSEFSA7, YOSEFSAA7, YUNG_RANDD, YungLPR, apa420, boring_nick, cbdg, fabZeef, flex3rs, foUrtf, jammehcow, matthewde, muskatico, nourylul, pajlada, randers, ravi0li_, rl_grey, romyDank, sbus, talenq, teischEnte, tolekk, xVeroNy, ツーツーツーツーツーツーツ
ーツーツーツ.
> PRIVMSG #cinipus :.mods
< @msg-id=no_mods :tmi.twitch.tv NOTICE #cinipus :There are no moderators of this channel.
> PRIVMSG #zneix :.mods
< @msg-id=room_mods :tmi.twitch.tv NOTICE #zneix :The moderators of this channel are: heryin, mm2pl, mm_sutilitybot, senderak, testaccount_420, zneixbot

and that makes it just a matter of parsing incoming NOTICE messages and accessing its msg-id tags. Keep in mind that a NOTICE message should pretty much always include said tag telling you which kind of message it is, so you can just check for no_vips, vips_success, no_mods or room_mods (as shown above).
Last but not least is a little warning that vips_success shows display names instead of login names, so you might get a username that could contain non-ascii localized display names (just like in the example of PRIVMSG #zneix :.vips.

sorry if I was too verbose, I’ve tried to share as much of my knowledge as possible ;p

Thank you so much for the reply and additional information. I’m pretty new to this, so everything helps.

The original issue of not getting the response to the commands was indeed solved, and I do have a functional method for getting the data I wanted. But as you point out, its not totally bullet proof. I want to try and be cautious and not get too clever right away, particularly since I’m not familiar with the syntax of all the possible NOTICE message response formats. But experimenting with adding the CAP for tags and adjusting which splits have which parts of the data, this is what I’m using now:

                else if (split.Length > 2 && split[2] == "NOTICE")
                {
                    _vaProxy.WriteToLog($"TwitchBot NOTICE: {line}", "yellow");
                    //Not sure about the format of all the NOTICE messages, but we want to handle mods and vips_success
                    if (split[0].StartsWith("@msg-id=room_mods") || split[0].StartsWith("@msg-id=vips_success"))
                    {
                        //@msg-id=room_mods :tmi.twitch.tv NOTICE #robertsmania :The moderators of this channel are: mod1, mod2, mod3, ... modX
                        //@msg-id=vips_success :tmi.twitch.tv NOTICE #robertsmania :The VIPs of this channel are: vip1, vip2, vip3, ... vipX.
                        //                                            ^^^^^^^^
                        //Grab the channel name here
                        int secondColonPosition = line.IndexOf(':');
                        int thirdColonPosition = line.IndexOf(':', secondColonPosition + 1);
                        int fourthColonPosition = line.IndexOf(':', thirdColonPosition + 1);
                        string message = line.Substring(thirdColonPosition + 1);//Everything past the third colon
                        string channel = split[3].TrimStart('#');
                        string userList = line.Substring(fourthColonPosition + 2); //2 skips the space after the colon, should be list of users
                        userList = userList.TrimEnd('.'); //VIP lists do appear to have a period at the end?
                        if (split[0].StartsWith("@msg-id=vips_success"))
                        {
                            vips = userList.Split(new string[] { ", " }, StringSplitOptions.None).ToList();
                        }
                        else if (split[0].StartsWith("@msg-id=room_mods"))
                        {
                            mods = userList.Split(new string[] { ", " }, StringSplitOptions.None).ToList();
                        }
                    }
                }

I guess I am a little worried that its depending on things in the message being a certain number of colons in, or that the NOTICE/JOIN/PING/PRVMSG identifiers are always in the same segment when split by spaces. Seems kind of risky.

Is there some kind of documentation with best practice or established framework for reliably parsing and handing the messages?

Only what is in the Docs is really “official”, but @BarryCarlyon 's Examples on GitHub are generally really high-quality and I can vouch for them.

Generally, it may also be worth to note the existance of some API Endpoints that Mirror Information available via chat - like GET moderators - do note the requirement for a broadcaster/moderator token on the Endpoints, though. Also EventSub and its soon™ to be available WebSocket Transfer.

That said, chat is still a very good general source for most things - just be aware of the other potential sources in case this changes in the future.

As Twitch is “standard” IRC when it comes to the format of a line.

Any IRCv3 compatible tokeniser/parser will do the job.

Twitch doc’s don’t describe parsing/tokenisation as Twitch manages to follow the specificiation for the formatting of an IRC line. So any existing IRCv3 paser/tokeniser will do the job. Or roll your own as per the IRC RFC’s/specifications

RFC 1459 - Internet Relay Chat Protocol (see also RFC2812 and RFC7194)
IRCv3 Specifications - IRCv3

Just note that Twitch only supports two CAP’s tags/commands (we don’t talk about membership since it’s basically useless)