Acid Bot – Ein Mulitplattform-Bot

TwitchBitch war ein Bot, den ich 2018 programmiert hatte. Er war gut, er war modular, er hat seine Aufgabe erfüllt. Aber sind wir mal ehrlich: Ein Bot der nur eine einzelne Plattform bedient, ist nicht mehr zeitgemäß. Mit meinem neuen Projekt Acid Bot möchte ich grenzen überschreiten. Ich möchte Funktionen verschiedener Plattformen vereinen. Und ich möchte auch weiterhin das Social-Media leben von Twitch-Streamern vereinfachen.

Aber auch ein Multiplattform-Bot benötigt eine Basis. Einen Ort, an dem er heimisch ist, wo er konfiguriert und gewartet werden kann. Ich habe mich als Basisplattform für Discord entschieden. Acid Bot wird also in erster Linie auf Discord laufen und dann weitere Plattformen wie Twitch und Twitter mit einbinden. Im aktuellen Stand (version 1.1) unterstützt Acid Bot bereits mehrere Funktionen um Informationen von Twitch zu beziehen. Beispielsweise ob ein Streamer gerade live ist, welches Spiel er spielt, wieviele Zuschauer er hat, wieviele Follower und Views er insgesamt hat, usw. Geplant sind aber auch ein vollständiger Chat-Bot für Twitch. Acid Bot soll also parallel in Discord und Twitch agieren können. Jedoch soll die Konfiguration nur über Discord erfolgen.

Konfiguration über Discord?

Um Acid Bot zu konfigurieren, werden die altbekannten !commands verwendet. Beispielsweise kann ein Discord-Admin mit dem Befehl !add-twitch DamianRyse meinen Twitch-Kanal zu einer Überwachungsliste hinzufügen. Und Acid Bot wird sich melden, sobald ich live gehe. Mit !remove-twitch DamianRyse kann er mich wieder von seiner Liste entfernen. So einfach wird die Konfiguration in allen Bereichen sein. Ob nun der Twitch-Watchdog, das setzen eines Promotion-Channels für die Werbung oder das einstellen einer Zeitzone (für bestimmte Funktionen benötigt), alles lässt sich über solche einfachen Kommandos konfigurieren.
Das ist eine der erheblichen Änderungen und Verbesserungen zu TwitchBitch. Denn dort musste man noch umständlich eine config-Datei bearbeiten, um die Bot-Settings zu verändern. Das ist natürlich bei Acid Bot noch immer möglich, aber nicht mehr nötig.

Für die Discord-API nutze ich eine 3rd-Party Library die sich Discord.Net nennt. Ich war am überlegen, ob ich meine eigene API dafür entwickle, aber warum das Rad neu erfinden, wenn jemand anders schon großartige Arbeit abgeliefert hat? Allerdings möchte ich für Twitch weiterhin meine eigene Codebasis verwenden, wie ich sie auch schon für TwitchBitch verwendet habe. Der gesamte Twitch-Code wird in eine eigenständige Library ausgegliedert, so dass sie für andere Projekte ebenfalls nutzbar ist.

Code: Der Anfang

Der Anfang des Programmes ist recht unspektakulär.

using Discord.WebSocket;
using System;
using System.Threading.Tasks;
using System.ServiceProcess;

namespace Acid_Bot
{
    class Program
    {
        static void Main(string[] args)
        => new AcidBot().StartAsync().GetAwaiter().GetResult();
    }

    class AcidBot : ServiceBase
    {
        public DiscordSocketClient _client;
        public CommandHandler _handler;
        private Initialize init;
        private System.Timers.Timer scheduleTimer = new System.Timers.Timer(60000);
        
        

        public async Task StartAsync()
        {
            if (String.IsNullOrWhiteSpace(Configuration.GetGeneralConfiguration().Token)) return;
            _client = new DiscordSocketClient(new DiscordSocketConfig
            {
                LogLevel = Discord.LogSeverity.Verbose
            });
            _client.Log += Log;
            init = new Initialize(null, _client);
            _handler = new CommandHandler(init.BuildServiceProvider(), _client);

            await _client.LoginAsync(Discord.TokenType.Bot, Configuration.GetGeneralConfiguration().Token);
            await _client.StartAsync();
            await _handler.InitializeAsync();
            await Task.Delay(-1);         
        }
    }
}

Direkt beim Programmstart rufen wir die Klasse AcidBot und den darin enthaltenen Task StartAsync() asynchron auf und „warten“ auf das Ergebnis. Das Ergebnis werden wir im günstigsten Fall natürlich nie erhalten, da die Klasse AcidBot unendlich lange laufen soll. Einzig eine beabsichtigte Methode zum herunterfahren des Bots könnte GetResult() auslösen.
StartAsync() tut jetzt folgende Dinge: Zu allererst lädt er sich aus einer Konfiguration (dazu kommen wir in einem anderen Beitrag) einen Token, den wir für Discord benötigen. Anschließend wird ein neues DiscordSocketClient-Objekt erstellt, welcher unsere Basis für die Datenverbindung zu Discord ist. Wir sagen dem DiscordSocketClient noch, das er bitte alles mögliche protokollieren soll, anschließend erstellen wir eine neue Instanz einer Initialize-Klasse (Code gleich im Anschluß).
Die Erstellung der CommandHandler-Klasse dient uns dazu, Kommandos die über Discord gesendet werden zu verarbeiten.
Die Methoden LoginAsync(), StartAsync() und InitializeAsync() tun im prinzip genau das, was die Namen bereits aussagen: Sie loggen sich ein, starten und initialisieren alle nötigen Klassen und Funktionen, um das Programm lauf- und kommunikationsfähig zu machen.

public class Initialize
{
    private readonly CommandService _commands;
    private readonly DiscordSocketClient _client;

    public Initialize(CommandService commands = null, DiscordSocketClient client = null)
    {
        _commands = commands ?? new CommandService();
        _client = client ?? new DiscordSocketClient();
    }

    public IServiceProvider BuildServiceProvider() => new ServiceCollection()
        .AddSingleton(_client)
        .AddSingleton(_commands)
        .AddSingleton<CommandHandler>()
        .BuildServiceProvider();
}

Und hier noch die CommandHandler-Klasse

class CommandHandler
{
    private readonly DiscordSocketClient _client;
    private readonly CommandService _commands;
    private readonly IServiceProvider _services;

    public CommandHandler(IServiceProvider serviceProvider, DiscordSocketClient client)
    {
        _services = serviceProvider;
        _commands = new CommandService();
        _client = client;
    }

    public async Task InitializeAsync()
    {
        Console.Title = "Acid Bot";
        await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(), services: _services);
        _client.MessageReceived += HandleCommandAsync;
    }

    private async Task HandleCommandAsync(SocketMessage arg)
    {
        var msg = arg as SocketUserMessage;
        if (msg == null) return;
        var context = new SocketCommandContext(_client, msg);
        int argPos = 0;
        if (msg.HasStringPrefix("!", ref argPos) || msg.HasMentionPrefix(_client.CurrentUser, ref argPos))
        {
            var result = await _commands.ExecuteAsync(context, argPos, _services);
            if (!result.IsSuccess && result.Error != CommandError.UnknownCommand)
            {
                Console.WriteLine(result.ErrorReason);
            }
        }
    }
}

Im nächsten Blogpost werden wir die Klasse schreiben, die Kommandos verarbeitet.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert