My extensible, modular C# stream bot development framework (for integration with Twitch) .
At the moment, if you aren't comfortable getting your hands dirty with the source or at least json config files, this may not be the project for you.
But once configured, the project should be very simple to use. It has a number of modular features, and it should be fast and easy to develop new ones.
I've also developed a StreamDeck plugin for interacting with the REST endpoints hosted by the bot.
To start with, get the appropriate NetCore 6 SDK (With AspNet). It will also be useful to install the .NET Entity Framework CLI tools by running:
dotnet tool install --global dotnet-ef
dotnet tool update --global dotnet-ef
Clone the TASagentTwitchBotDemos project and initialize and update the git submodule (this project):
git clone https://github.com/TASagent/TASagentTwitchBotDemos
cd TASagentTwitchBotDemos
git submodule init
git submodule update
If you're using this as a twitch bot, you're going to need to make a new Twitch account for that bot. You also need to go to The Twitch Dev Console and register an application to receive a ClientID. Enter any name, use http://localhost:5000/TASagentBotAPI/OAuth/BotCode
and http://localhost:5000/TASagentBotAPI/OAuth/BroadcasterCode
as the OAuth Redirect URLs, and choose "Chat Bot" as the category.
If the project you're building uses the database, navigate to the project directory via command line. If the migration files already exist for it (and are up-to-date), then just run dotnet ef database update
. If they do not exist yet, you'll have to create them first with dotnet ef migrations add InitialDBCreation
, followed by dotnet ef database update
.
The first time you run a demo program, you'll be prompted for several values, and it will prepare some configuration files in your Documents/TASagentBotDemo
directory.
There are now two methods of using TTS.
The first is to head to My Webserver and register a new account using the Twitch OAuth option (make sure to log in as the Broadcaster). Then contact me at TASagent.Streams@gmail.com and give me your Twitch username and ask me to authorize you to connect and increase your TTS Character Limit. Be sure to include whether you'd like to use the Neural voices, but understand that the Neural TTS Voices consume 4x the character limit (because of the increased cost). You'll be given some values to insert into TASagentBotDemo/Config/ServerConfig.json
in order to be able to connect.
I have made sure to keep The Hard Way available because I don't want to keep a stranglehold on how the bot is used. If you wish to use TTS the hard way, you'll have to create a GoogleCloud account with TTS enabled and put the credentials in TASagentBotDemo/Config/googleCloudCredentials.json
, an AWSPolly account with its credentials stored TASagentBotDemo/Config/awsPollyCredentials.json
, and/or a Microsoft Azure account with its credentials stored in TASagentBotDemo/Config/azureSpeechSynthesisCredentials.json
. Otherwise, you'll have to use a configuration of the bot that doesn't support TTS. Read and understand the pricing schemes of each service. It's unlikely one streamer would be able to use enough TTS in a single month to result leave the free tier of any service (while it lasts), but it's your responsibility to understand how the pricing works.
Example googleCloudCredentials.json
file:
{
"type": "service_account",
"project_id": "TheProjectsName",
"private_key_id": "LONGPRIVATEID",
"private_key": "-----BEGIN PRIVATE KEY-----\nLOTS OF PRIVATE STUFF HERE\n-----END PRIVATE KEY-----\n",
"client_email": "examplettssystem@theprojectsname.iam.gserviceaccount.com",
"client_id": "CLIENTID",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/examplettssystem%theprojectsname.iam.gserviceaccount.com"
}
Example awsPollyCredentials.json
file:
{
"AccessKey": "AN ACCESS KEY HERE",
"SecretKey": "A SECRET KEY HERE"
}
Example azureSpeechSynthesisCredentials.json
file:
{
"AccessKey": "AN ACCESS KEY HERE",
"Region": "westus2"
}
Depending on which Demo you use, you may need to change the ITTSRenderer
provider to the Local one in the Startup
class like so:
services.UnregisterImplementation<Core.TTS.TTSWebRenderer>()
.AddSingleton<Core.TTS.ITTSRenderer, Core.TTS.TTSRenderer>();
Navigate to the directory of the Bot project (where the .proj
file lives) and create the initial database with dotnet ef database update
.
To apply model changes as a new database migration, navigate to the directory of the project and use dotnet ef migrations add <MigrationName>
where <MigrationName>
is what you'd like to name this model update, for example, dotnet ef migrations add AddingNewUserField
. Then use dotnet ef database update
to apply the model updates to the database.
If everything has been set up properly, you should be able to launch the bot and have it connect to Chat.
Open a web browser to http://localhost:5000/API/ControlPage.html to see the control page, and enter the admin password.
Voila!
There are several major subsystems to help streamline customization, and it's simple to create new ones. A lot of the action takes place in the in the Dependency Injection system that's part of ASP.NET. Classes are registered in Program.cs
, and when an instance of a class is required, it is instantiated (along with all of its constructor pre-requisites). As a result, no cycles are allowed (where Class A and Class B both require one-another).
When multiple classes are registered for an interface or class, only the last one to be registered will be instantiated.
If you wish to register a single class to handle multiple interfaces, be careful how you register it.
This block would instantiate only one instance of Notifications.FullActivityProvider
and provide it for every listed interface:
builder.Services
.AddSingleton<Notifications.FullActivityProvider>()
.AddSingleton<Notifications.ISubscriptionHandler>(x => x.GetRequiredService<Notifications.FullActivityProvider>())
.AddSingleton<Notifications.ICheerHandler>(x => x.GetRequiredService<Notifications.FullActivityProvider>())
.AddSingleton<Notifications.IRaidHandler>(x => x.GetRequiredService<Notifications.FullActivityProvider>())
.AddSingleton<Notifications.IGiftSubHandler>(x => x.GetRequiredService<Notifications.FullActivityProvider>())
.AddSingleton<Notifications.IFollowerHandler>(x => x.GetRequiredService<Notifications.FullActivityProvider>())
.AddSingleton<Notifications.ITTSHandler>(x => x.GetRequiredService<Notifications.FullActivityProvider>());
Whereas this block would instantiate an independent copy of the class as the handler for each interface:
builder.Services
.AddSingleton<Notifications.FullActivityProvider>()
.AddSingleton<Notifications.ISubscriptionHandler, Notifications.FullActivityProvider>()
.AddSingleton<Notifications.ICheerHandler, Notifications.FullActivityProvider>()
.AddSingleton<Notifications.IRaidHandler, Notifications.FullActivityProvider>()
.AddSingleton<Notifications.IGiftSubHandler, Notifications.FullActivityProvider>()
.AddSingleton<Notifications.IFollowerHandler, Notifications.FullActivityProvider>()
.AddSingleton<Notifications.ITTSHandler, Notifications.FullActivityProvider>();
However, when a class accepts an IEnumerable
as a constructor argument, it receives an instance of every class that's been registered as a handler. For example, the CommandSystem has the following constructor:
public CommandSystem(
ICommunication communication,
ErrorHandler errorHandler,
IEnumerable<ICommandContainer> commandContainers)
So it recieves and instance of every class registered as an ICommandContainer
:
builder.Services
.AddSingleton<Commands.ICommandContainer, Commands.CustomSimpleCommands>()
.AddSingleton<Commands.ICommandContainer, Commands.SystemCommandSystem>()
.AddSingleton<Commands.ICommandContainer, Commands.PermissionSystem>()
.AddSingleton<Commands.ICommandContainer, Commands.ShoutOutSystem>()
.AddSingleton<Commands.ICommandContainer, Commands.NotificationSystem>()
.AddSingleton<Commands.ICommandContainer, Quotes.QuoteSystem>()
.AddSingleton<Commands.ICommandContainer, TTS.TTSSystem>()
.AddSingleton<Commands.ICommandContainer, Commands.TestCommandSystem>();
However, classes are (deliberately) not automatically registered as the handler for all of their interfaces. If you created a class called ExampleClass
that implements Commands.ICommandContainer
, and in the Startup
class you called services.AddSingleton<ExampleClass>()
, then ExampleClass
would not be passed to the CommandSystem
.
New commands just need to extend the ICommandSystem
interface, and get registered to that interface in the Startup
class.
A Scripting subsystem is utilized by several modules, like the ScriptedActivityProvider
and ScriptedCommands
. The scripts can be edited on the ControlPage.
Each script has a predefined entry-point. You can best conceptualize each script as living inside of an implicit anonymous class, with syntax similar to C#. Other methods and script-instanced variables can be defined.
A variable or container defined outside of a function is persistent across all invocations of the script, but is scoped to only the script. If the variable is declared with the global
keyword, then it can be accessed in all scripts that declare it with the global
keyword, and only the assignment in the first declaration to run is executed. So a dozen scripts that all say global int exampleInt = -1;
will all access the same exampleInt
, but it will only be set to -1
the first time one is run.
For the time being, these are the supported containers: List<T>
, Queue<T>
, Stack<T>
, RingBuffer<T>
, DepletableList<T>
, DepletableBag<T>
, Dictionary<TKey,TValue>
, HashSet<T>
.
New classes can be registered for access in the scripting language with the ClassRegistrar
, see the ScriptedActivityProvider
for examples.
In OBS, add the following BrowserSources
http://localhost:5000/BrowserSource/overlay.html
- Image notifications (example size: 800px wide x 600px tall)
http://localhost:5000/BrowserSource/controllerSpy.html
- SNES NintendoSpy Overlay (example size: 400px wide x 100px tall)
http://localhost:5000/BrowserSource/timer.html
- Timer overlay (example size: 450px wide x 150px tall)
http://localhost:5000/BrowserSource/ttsmarquee.html
- Scrolling TTS Marquee (example size: 1920px wide x 60px tall)
http://localhost:5000/BrowserSource/emoteRain.html
- Raining Emote effect (example size: 1920px wide x 1080px tall)