Telegram Bot auf Kotlin: Command / Sudo Null IT News

Vorheriger Teil: Telegram Bot auf Kotlin: Einführung

Dies ist ein Zwischenteil des Tutorials zum Erstellen von Telegramm-Bots basierend auf Bibliotheken Plagubot und tgbotapi. Insbesondere werden wir in diesem Teil über einen ziemlich einfachen Vergleich mit dem Geplanten sprechen Plugin für die Befehlsregistrierung beim Start und deren Installation/Reinigung weiter zur Laufzeit.

Es ist erwähnenswert, dass dieser Artikel einen vereinfachten Code enthält. Mit seiner Hilfe wird es möglich sein, ein Analogon des Plugins zu erstellen, das sich am Ende herausgestellt hat, und dennoch wird es in dem Artikel etwas oberflächlicher sein. Wenn möglich, habe ich versucht, Spoiler hinzuzufügen, wo der Code am Ende anders war, also sollte es keine Probleme geben.

Also die Aufgabe

Wie oben erwähnt, ist die Aufgabe dieses Plugins sehr einfach – die Bot-Befehle beim Start zu registrieren und dabei den Befehlssatz ändern zu können. Natürlich möchte ich für jedes Team die volle Bandbreite an Parametern spezifizieren können, nämlich:

Basislösung

Da wir irgendwo anfangen müssen, um die Befehle zu übernehmen, die der Bot anfangs setzt, wäre die einfachste Option, DI zu verwenden, um alle Befehle von anderen Plugins und Teilen der Anwendung zu erhalten. Gleichzeitig reicht es in diesen ganz anderen Plugins und Teilen der Anwendung aus, einen Befehl mit einer Bindung an den gewünschten Typ und einer zufälligen Kennung zu registrieren (damit DI nicht auf einen Typkonflikt schwört):

// So holen wir Befehle innerhalb des Plugins mit den Befehlen koin.getAll().distinct()… // Und so passen die Befehle in DI in anderen Plugins single(named(uuid4 ().toString() )) { /* Erstellen von CommandType */ }

Darüber hinaus muss die Möglichkeit bereitgestellt werden, aktuelle Bot-Befehle hinzuzufügen / zu entfernen. Zu diesem Zweck ist es möglich, eine einfache Set / Unset-Schnittstelle zu verwenden, wie zum Beispiel:

Schnittstelle CommandsKeeper { Spaß aussetzen addCommand (Befehl: CommandType) Spaß aussetzen removeCommand (Befehl: CommandType) }

Wenn man sich das Finale anschaut CommandsKeeper, werden Sie viel internes sehen. Dies ist die Fähigkeit der Sprache, die Sichtbarkeit von Elementen innerhalb eines bestimmten Moduls einzuschränken. Kurz bspw. onScopeChanged wird nur für Plugin-Teile verfügbar sein

Nicht zuletzt das Plugin selbst. Tatsächlich muss er zu Beginn alle in DI registrierten Befehle sammeln, sie selbst in CommandsKeeper einfügen und irgendwie auf Änderungen in Befehlen hören und sie aktualisieren.

Puzzle Stücke

Denn im Telegramm-Bot-API Es gibt keine Entität, die sowohl einen Befehl mit einer Beschreibung und seinem Geltungsbereich als auch einen Sprachcode enthalten würde, wir müssen selbst eine solche Entität erstellen. Aus offensichtlichen Gründen wird dies eine ziemlich einfache Datumsklasse mit drei Feldern sein:

Datenklasse BotCommandFullInfo( val command: BotCommand, val scope: BotCommandScope = BotCommandScope.Default, val languageCode: String? = null ) { val key: Pair? = if (scope == BotCommandScope.Default && languageCode == null) { null } else { Pair(scope, languageCode) } }

musste ich hinzufügen Wertklasse CommandsKeeperKey für Schlüssel, die nur alle notwendigen Informationen für den Befehl enthält: BotCommandScope und Sprachcode. Infolgedessen laufen im Plugin und seiner API alle Aufrufe auf Aufrufe mit CommandsKeeperKey hinaus

Für CommandsKeeper können Sie die einfachste Implementierung basierend auf der Karte vornehmen, in der die Schlüssel der Befehlssatzkontext sind – Befehlsbereich und Sprachcode. Ein Beispiel für eine grundlegende Implementierung basierend auf der oben vorgestellten Schnittstelle:

class CommandsKeeper( // Abrufen der beim Start festzulegenden Befehle im Konstruktor val preset: List ) { // Dieser Flow kann verwendet werden, um Befehlssatzaktualisierungen für jeden Schlüsselsatz abzurufen internal val onScopeChanged = MutableSharedFlow?>() // Hier können Sie über groupBy lesen: // Erstellt eine Map mit Schlüsseln aus Bereich und Sprachcode und Werten – Listen von Befehlen mit diesen Schlüsseln private val scopesCommands: MutableMap?, MutableSet > = preset.groupBy { it.key }.mapValues ​​​​{ (_, v) -> // Ersetzen Sie die Werte in der Karte // Erstellen Sie eine Liste von Befehlen, indem Sie sie abrufen BotCommandFullInfo und verwandle sie in einen Satz, um Wiederholungen zu vermeiden v.map { it.command }.toMutableSet() }.toMutableMap() // Umwandlung in eine veränderliche Karte // Dieser Mutex wird verwendet, um den gleichzeitigen Zugriff auf private Werte von scopesCommands zu verhindern mutationsMutext = Mutex() // Informationen zum Suspend-Befehl hinzufügen fun addCommand (Befehl: BotCommandFullInfo) { val added = mutationsMutex.withLock { // Befehlssatzänderung sperren // Holen Sie sich einen bestehenden Satz per Schlüssel ODER erstellen Sie einen neuen Satz, fügen Sie ihn in die Karte ein und verwenden Sie diesen Satz val set = scopesCommands.getOrPut(command.key) { mutableSetOf( ) } // Befehl zum Satz hinzufügen, add gibt booleschen Wert zurück set.add(command.command) } if (added) { // Benachrichtigen, dass sich der Befehlssatz für die Taste geändert hat onScopeChanged.emit(command.key ) } } suspend fun removeCommand (command : BotCommandFullInfo) { val entfernt = mutationsMutex.withLock { // Sperren der Änderung des Befehlssatzes // Holen Sie sich den vorhandenen Satz per Schlüssel // ODER berücksichtigen Sie, dass der Befehl nicht gelöscht werden kann, und geben Sie “false” zurück withLock, // die auf die entfernte Variable gesetzt wird val set = scopesCommands.get(command.key) ?: return@withLock false // Entferne den Befehl aus der Menge, entferne gibt booleschen Wert zurück set.remove(command.command) } if (removed) { // Benachrichtigen, dass sich der Befehlssatz für die Taste geändert hat onScopeChanged.emit (Befehlstaste) } } int ernal fun getKeys(): List?> { // Rückgabe der Tasten return scopesCommands.keys.toList() } internal fun get(key: Pair?): List< BotCommand > { // Abrufen der bekannten Befehle, Konvertieren in eine Liste ODER Zurückgeben einer leeren Liste return scopesCommands.get(key) ?.toList() ?: emptyList() } }

Tatsächlich haben wir im Wesentlichen eine ziemlich einfache Klasse: Wir können einen Befehl in anderen Plugins hinzufügen (addCommand) oder entfernen (removeCommand), und innerhalb des Team-Plugin-Projekts können wir eine Reihe von Befehlen für den Kontext abrufen und den Befehl abonnieren Änderungen. All dies wird mit der Synchronisierung zum Zeitpunkt der Installation / Löschung eines Teams gewürzt

Stimmt, es ist nicht gerade idiomatisch 🙁

Und es wäre idiomatisch, eine versiegelte Schnittstelle für den Aufgabentyp und ein paar Datenklassen zum Hinzufügen / Löschen von Befehlen zu erstellen und alles mit einem verzögerten Anhang an einen Kanal zu senden, von dem wir auf das Ergebnis warten. Wie Sie anhand der Nachteile dieses Ansatzes sehen können, ist er sehr umständlich. Von den Pluspunkten – wir werden keine Synchronisationen mehr haben und möglicherweise wird solcher Code für Coroutinen leichter zu verdauen sein und folglich für ihre Arbeit im Allgemeinen

Das dickste Stück

Wie Sie sich vorstellen können, wird das fetteste Stück auf diesem Kuchen das Plugin selbst sein. Im Wesentlichen ist es jedoch sehr einfach: einen CommandsKeeper erstellen und bei DI registrieren, die Bot-Befehle zum Zeitpunkt der Installation festlegen und sie verfolgen, während der Bot läuft. Hier ist das Skelett unseres Plugins:

@Serializable object CommandsPlugin : Plugin { override fun Module.setupDI(database: Database, params: JsonObject) {} override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {} }

Das heißt, im Plugin benötigt das Plugin auf der Basisebene nichts anderes als das Überschreiben der Plugin-Methoden. Und jetzt fügen wir die Erstellung und Registrierung von CommandsKeeper zu setupDI hinzu:

override fun Module.setupDI(database: Database, params: JsonObject) { // Eine einzelne CommandsKeeper-Instanz registrieren single { // Eine CommandsKeeperImpl-Instanz erstellen CommandsKeeper( // Alle registrierten BotCommandFullInfo-Instanzen abrufen und Duplikate vermeiden getAll().distinct () ) } }

Wir werden nichts anderes in DI registrieren. Da wir die Aktualisierung von Befehlen an zwei Stellen angegeben haben, nämlich beim Initialisieren des Bots und beim Ändern des Befehlssatzes, wäre es angebracht, das Installieren von Befehlen und das Entfernen von Befehlen bei deren Abwesenheit in eine separate Funktion zu trennen:

private suspend fun BehaviourContext.setScopeCommands(scope: BotCommandScope, languageCode: String?, commands: List) { if (commands.isEmpty()) { // Befehle für Scope und languageCode löschen deleteMyCommands( scope, languageCode ) } else { // Befehle für Geltungsbereich und Sprachcode setzen setMyCommands( // Nur eindeutige Befehle nehmen und die ersten 100 Befehle nehmen, wenn es mehr Befehle gibt. distinctBy { it.command }.take(botCommandsLimit.last + 1), Geltungsbereich, Sprachcode ) } }Like Es stellt sich tatsächlich heraus, dass alles komplizierter ist, obwohl die Essenz dieselbe ist

Es gibt mehrere Nuancen in der endgültigen Implementierung:

  • Es ist wünschenswert, solchen Code in runCatching / trycatch einzurahmen

  • Die Liste der Eingabebefehle ist nullable, da in der CommandsKeeper-Implementierung von Github die get-Methode null zurückgibt, wenn kein Befehlssatz vorhanden ist

Arbeiten Sie im Bot:

override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { // CommandsKeeper in DI oben registrieren lassen val commandsKeeper = koin.get() // Befehlssatzänderungen abonnieren. es hier – Pair? commandsKeeper.onScopeChanged.subscribeSafelyWithoutExceptions(scope) { // Eine Reihe von Befehlen nach Schlüsseln abrufen val commands = commandsKeeper.getCommands(it) // Befehle festlegen setScopeCommands(it, commands) } // Die zum Zeitpunkt des Bot-Starts bekannten Tasten abrufen (Bereichs- und Sprachcode-Paare) und den Befehlssatz für jeden aktualisieren commandsKeeper.getKeys().forEach { // Den Befehlssatz nach Schlüsseln abrufen val commands = commandsKeeper.getCommands(it) // Die Befehle festlegen setScopeCommands(it, commands ) } }

Alles in allem war es das 🙂

Ergebnisse

Als Ergebnis haben wir ein ziemlich einfaches Plugin erstellt, mit dem Sie Bot-Befehle zentral verwalten können. Der vollständige Code, Verbindungsanweisungen und andere nützliche Informationen sind enthalten Github-Repositories. Viel Spaß beim Benutzen!

Similar Posts

Leave a Reply

Your email address will not be published.