Connecting to Twitch IRC

Hi,

I’m currently looking into integrating Twitch into my game. I’d like to parse a Twitch chat (which can be accessed via IRC: https://github.com/justintv/Twitch-API/blob/master/IRC.md ) for commands and stuff.

Now, I don’t really have any experience with C++ networking. I found this tutorial on socket connection: A new, community-hosted Unreal Engine Wiki - Announcements and Releases - Unreal Engine Forums Can this be used to connect to the IRC server? But even if this works, I wouldn’t even know where to start with the OAuth token for the password.

Before I delve deeper into this, I wanted to ask if there are any resources on this topic or maybe someone has done something like this before. Can’t really search for “IRC” in the forums/answerhub as this always returns links to the UE4 IRC channel.

Thanks!

I was interested in this specific task as well so I thought I’d give it a try and I just got it working, thanks to the tutorial you have linked, Ramas Socket-Listener-Tutorial (awesome as always!) and thistutorial, using the following code. There might be a better way to do it, but it works fine as far as I have tested it (Especially the parsing is not very optimized, since I don’t know the IRC format very well, I just did a quick google search and tried some things to get it working).
I put it into GameMode as this was the easiest way for me to test it but it should work in any other class as well.



#pragma once

#include "GameFramework/GameMode.h"
#include "Networking.h"

#include "TwitchTestGameMode.generated.h"

/**
 * 
 */
UCLASS()
class TWITCH_TEST_API ATwitchTestGameMode : public AGameMode
{
	GENERATED_BODY()
	
public:
	virtual void BeginPlay() override;

private:
	FSocket* ListenerSocket;
	FSocket* ConnectionSocket;
	FTimerHandle timerHandle;

	void SocketListener();

	void SendLogin();

	bool SendString(FString msg);

        void ParseMessage(FString msg);

	void ReceivedChatMessage(FString UserName, FString message);
};





#include "TwitchTest.h"
#include <string>

#include "TwitchTestGameMode.h"

void ATwitchTestGameMode::BeginPlay()
{
	Super::BeginPlay();

	FIPv4Endpoint Endpoint(FIPv4Address(127, 0, 0, 1), 6667);
	FSocket* ListenerSocket = FTcpSocketBuilder(TEXT("TwitchListener"))
		.AsReusable()
		.BoundToEndpoint(Endpoint)
		.Listening(8);

	//Set Buffer Size
	int32 NewSize = 0;
	ListenerSocket->SetReceiveBufferSize(2 * 1024 * 1024, NewSize);

	GetWorldTimerManager().SetTimer(timerHandle, this, &ATwitchTestGameMode::SocketListener, 0.01, true);

	SendLogin();
}

void ATwitchTestGameMode::SocketListener()
{
	TArray<uint8> ReceivedData;
	uint32 Size;
	bool Received = false;
	while (ListenerSocket->HasPendingData(Size))
	{
		Received = true;
		ReceivedData.SetNumUninitialized(FMath::Min(Size, 65507u));

		int32 Read = 0;
		ListenerSocket->Recv(ReceivedData.GetData(), ReceivedData.Num(), Read);
	}
	if (Received)
	{
		const std::string cstr(reinterpret_cast<const char*>(ReceivedData.GetData()), ReceivedData.Num());
		FString fs(cstr.c_str());

		ParseMessage(fs);
	}
}

void ATwitchTestGameMode::ParseMessage(FString msg)
{
	TArray<FString> lines;
	msg.ParseIntoArrayLines(lines);
	for (FString fs : lines)
	{
		TArray<FString> parts;
		fs.ParseIntoArray(parts, TEXT(":"));
		TArray<FString> meta;
		parts[0].ParseIntoArrayWS(meta);
		if (parts.Num() >= 2)
		{
			if (meta[0] == TEXT("PING"))
			{
				SendString(TEXT("PONG :tmi.twitch.tv"));
			}
			else if (meta.Num() == 3 && meta[1] == TEXT("PRIVMSG"))
			{
				FString message=parts[1];
				if (parts.Num() > 2)
				{
					for (int i = 2; i < parts.Num(); i++)
					{
						message += TEXT(":") + parts*;
					}
				}
				FString username;
				FString tmp;
				meta[0].Split(TEXT("!"), &username, &tmp);
				ReceivedChatMessage(username, message);
				continue;
			}
		}
	}
}


void ATwitchTestGameMode::ReceivedChatMessage(FString UserName, FString message)
{
	UE_LOG(LogTemp, Warning, TEXT("%s: %s"), *UserName, *message);
}

void ATwitchTestGameMode::SendLogin()
{
	auto ResolveInfo = ISocketSubsystem::Get()->GetHostByName("irc.twitch.tv");
	while (!ResolveInfo->IsComplete());
	if (ResolveInfo->GetErrorCode() != 0)
	{
		UE_LOG(LogTemp, Warning, TEXT("Couldn't resolve hostname."));
		return;
	}

	const FInternetAddr* Addr = &ResolveInfo->GetResolvedAddress();
	uint32 OutIP = 0;
	Addr->GetIp(OutIP);
	int32 port = 6667;

	TSharedRef<FInternetAddr> addr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr();
	addr->SetIp(OutIP);
	addr->SetPort(port);

	ListenerSocket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("default"), false);

	bool connected = ListenerSocket->Connect(*addr);
	if (!connected)
	{
		UE_LOG(LogTemp, Warning, TEXT("Failed to connect."));
		if (ListenerSocket)
			ListenerSocket->Close();
		return;
	}

	SendString(TEXT("PASS [oauth]"));
	SendString(TEXT("NICK [twitch_name]"));
	//SendString(TEXT("JOIN #[channel_name]"));
}

bool ATwitchTestGameMode::SendString(FString msg)
{
	FString serialized = msg + TEXT("
");
	TCHAR *serializedChar = serialized.GetCharArray().GetData();
	int32 size = FCString::Strlen(serializedChar);
	int32 sent = 0;

	return ListenerSocket->Send((uint8*)TCHAR_TO_UTF8(serializedChar), size, sent);
}


And you need to add Sockets and Networking to your Build.cs file.



PublicDependencyModuleNames.AddRange(new string] { "Core", "CoreUObject", "Engine", "InputCore", "Sockets", "Networking" });


Of course you have to replace [oauth] and [twitch_name] with your corresponding oauth and twitch_name, without the square brackets.

Woah, thanks so much! I’ll try it out later this week and will report back. :slight_smile:

Works flawlessly, thanks again! :slight_smile: Only had to make minor changes to the ParseIntoArray calls, maybe because I’m still on 4.7.

Now I can let the ball explode via chat commands. Best day ever.

Hey guys,

I’m really interested in using this Chat feature and don’t want to switch to “Lumberyard” to do it,

How is it going getting information from the Chat?

I read through the code and i seem to understand the general idea, however my understanding of the Syntax is limited as i much prefer visual scripting.

Would it be possible to have a discussion and a demonstration from one of you about the current system you have going?

Thanks,

The code as is above there simply connects to any twitch chat you want it to (as long as you enter a valid username/ouath combination and a channel to join) and then parses every data that comes in. Amongst some other things the client receives, Twitch sends a Ping every 5 minutes to check if you are stilly online, to which the code above automatically replies and of course also all chat messages. I created a seperate method that gets called upon receiving a chat message and am passing the username of the sender and the message itself to the method. You could also make that a BlueprintImplementableEvent so that you can handle the parsing of the actual chat message yourself or do anything you wish to do with it. You could also expose SendString to BP so that you can join a channel from Blueprints, send a chat message etc.

@Taces

Ahh yes I can see it now,

I setup a C++ Project for a quick test and followed your instructions, the only difference i believe being my project was called TWITCHCHAT_API when yours was called TWITCH_TEST_API

However when run the code from VS it opens the editor but when I play there are no messages in the output log regarding the codes actions.

I made sure to override the Gamemode with my new test gamemode file that I created.
I Changed the OAUTH and TwitchName for my Pass and Name respectively, do i need to remove the words that say PASS and NICK?

ib9ikun.png


I should mention this is happening as well, I am unsure why its unhappy with these lines and others similar to it.

Is there something silly i forgot?

Like I said i’m very new to this, but when I read through the code I understand the functions and what they do, they just don’t seem to be happening.

Thanks guys,

In the for-each-loop in ParseMessage you could try to log every received line “UE_LOG(LogTemp, Warning, TEXT(”%s"), *fs);", as it currently only logs the chat messages and not the “welcome message” by twitch on a succesful login and some other stuff.
Did you also join a channel?

No, PASS and NICK have to be part of the message (as can be read here: https://github.com/justintv/Twitch-API/blob/master/IRC.md - I’d really recommend reading that page if you want to include twitch in your game, at least the upper few paragraphs). Have you included the “oauth:” as part of the oauth key? Might be a bit confusing, but the oauth key is e.g. “oauth:1a1b2g5g3” and not only “1a1b2g5g3”.

As long as everything compiles fine you can safely ignore the misleading red squiggles, at least I have them as well and works flawlessly for me.

I read through and now i understand OAuth - I have created my own Oauth password using the website you mentioned.

I added in the Log you recommended I now receive this from Twitch in UE4’s Output log:


LogTemp:Warning: :tmi.twitch.tv 001 nemecys :Welcome, GLHF!
LogTemp:Warning: :tmi.twitch.tv 002 nemecys :Your host is tmi.twitch.tv
LogTemp:Warning: :tmi.twitch.tv 003 nemecys :This server is rather new
LogTemp:Warning: :tmi.twitch.tv 004 nemecys :-
LogTemp:Warning: :tmi.twitch.tv 375 nemecys :-
LogTemp:Warning: :tmi.twitch.tv 372 nemecys :You are in a maze of twisty passages, all alike.
LogTemp:Warning: :tmi.twitch.tv 376 nemecys :>

**EDIT:
**
I now have the correct functionality working, I understand it all completely now, I wondered how it would get the information from just my Username and Password.

I should of read a bit more before I posted but yes that page and your code was a great success, I’m having fun dissecting it as we speak.

Now it all makes sense,
Thank you so much for your help, this was a great tutorial and the website you linked me to cleared this all up.

This should be a fun little C++ Project for me to try for the first time, I will be interested in making functions and information Blueprint accessible, I think that’s my next step.

Thanks!!

Gald it helped :slight_smile:

I just noticed that the connection fails when I try it in standalone mode or as a packaged game (Development configuration) - ResolveInfo (from “ISocketSubsystem::Get()->GetHostByName(“irc.twitch.tv”);”) gives me an error code 21. Both PIE and Shipping builds work though, so it’s not that big of a problem. Anyways, does anyone know why this is happening?

I’m going to start to look in to doing this too and I have a question. When the game is finished and say multiple people are playing it. Would using my oauth key and user work for all of them? Or should the players generate and use their own.

They can generate their own token with Twitch Chat Password Generator and you’ll need to create a menu or something where they can type (or copy-paste) it in (+ their username).

If you are generating OAuths with Twitch Chat Password Generator and need to customize the scopes of the OAuth you can use this:
http://tizzyt-archive.blogspot.com/2014/05/custom-oauth.html
URL=http://tizzyt-archive.blogspot.com/2014/05/custom-oauth.html

Please could somebody tell me (and explain to me) how to fix the compile error that has just started happening with 4.13.2 as I don’t understand why it would suddenly start appearing, or (in truth) what it actually means:

warning C4458: declaration of ‘ListenerSocket’ hides class member. / see declaration in .h file - for the line: FSocket* ListenerSocket = FTcpSocketBuilder(TEXT(“TwitchListener”)).AsReusable().BoundToEndpoint(Endpoint).Listening(8);

Thanks.

It’s only a warning, right? It should work nonetheless. To fix it, I believe you just have to rename the ListenerSocket variable, as there seems to be a variable in a parent class with the same name.

Edit: Thanks for the reply anteevy, sadly changing the variable name just changed the error to the new variable name.

The good news is that I have just found what was crashing the game so figured it a good idea to post here as information for others. Originally I had been using the spawn actor from class to create a chat actor in BeginPlay (worked fine in versions before 4.13). Now I’ve set it up as a child actor within the player controller (which I then set a variable to within the controllers’ BeginPlay), and it’s all working again.