Ein Matrix Bot in Go

Einleitung

In der Vergangenheit habe ich schon öfter mal Chat-Bots mit Python für Matrix geschrieben, aus Interresse wollte ich es auch mal mit go versuchen.

Für den folgenden Projekt verwende ich Mautrix-Go um nicht alles from scratch neu schreiben zu müssen. Mautrix-Go bietet viele Grundlegende funktionen, welche die Kommunikation mit dem Matrix Server wesentlich einfacher machen.

Wichtig ist hier das nicht der “legacy” branch verwendet wird, sondern der Master.

Konfiguration

Da ich keine Kennwörter oder Auth-Tokens in meinem Quell-Code mag, lege ich mir zu erst immer eine Konfigdatei an.

Ich habe mich der einfacheit halber in diesem Fall für eine einfache json Datei entschieden.

{
  "matrix": {
    "homeserver": "https://matrix.org",
    "username": "Bot-User",
    "password": "Bot-Passwort"
  }
}

config.json

Diese Daten müssen noch von unserem go Programm verarbeitet werden können, dazu habe ich eine config.go angelegt, in dieser befindet sich ein “struct”, in dem ich die Felder der Config.json definiert habe.

Anschließend habe ich eine kleine Funktion geschrieben, welche den Pfad zur Konfigurations-Datei entgegennimmt, und daraufhin versucht, die Datei aufzurufen.

package main

import (
	"encoding/json"
	"io/ioutil"
)

type Config struct {
	Matrix struct {
		Homeserver    string `json:"homeserver"`
		Username      string `json:"username"`
		Password      string `json:"password"`
	}
}

var config Config

func loadConfig(path string) error {
	data, err := ioutil.ReadFile(path)
	if err != nil {
		return err
	}
	return json.Unmarshal(data, &config)
}

config.go

Nach dem ich mich um die Konfiguration gekümmert haben, legen ich die main.go an.

In der main.go fügen ich zunächst eine init() Funktion hinzu. In dieser Funktion rufen nun die loadConfig() Funktion auf.

package main

import (
	"log"
	"os"
)

func init() {
	err = loadConfig("config.json")
	if err != nil {
		log.Fatal(err.Error())
		os.Exit(0)
	}
	log.Println("Config loading successful")
}

func main() {
	println("foo")
}

main.go

Login

Das Mautrix-Go Paket stellt uns die Möglichkeit zur Verfügung, uns am Server zu Authentifizieren:

...
func loginToServer() (error, *mautrix.Client) {
	log.Printf("Log in to %v as %v", config.Matrix.Homeserver, config.Matrix.Username)

	client, err := mautrix.NewClient(config.Matrix.Homeserver, "", "")
	if err != nil {
		return err, nil
	}

	resp, err := client.Login(&mautrix.ReqLogin{Type: "m.login.password", User: config.Matrix.Username, Password: config.Matrix.Password})
	if err != nil {
		return err, nil
	}

	client.SetCredentials(resp.UserID, resp.AccessToken)
	log.Println("Login successful")

	return err, client
}

main.go

Syncer

Laut Dokumentation ist der Syncer das Teil, das sich um den API-Endpunkt /sync kümmert (// The thing which can process /sync responses).

Der Syncer kommt aus dem Client und kann folgendermaßen erstellt werden: syncer := client.Syncer.(*mautrix.DefaultSyncer).

Über syncer.OnEventType(mautrix.EventMessage, func(matrixEvent *mautrix.Event){}) erstelle ich einen Listener der auf den Event Type “EventMessage” lauscht, der zweite Parameter den “OnEventType” entgegennimmt ist vom Type “callback”, an diesen Parameter kann eine Funktion übergeben werden.

Ein Beispiel:

syncer := client.Syncer.(*mautrix.DefaultSyncer)
syncer.OnEventType(mautrix.EventMessage, func(matrixEvent *mautrix.Event) {
	log.Println(matrixEvent.Content.Body)
})

In diesem Beispiel wird der im Chat geschriebene Text im Terminal ausgegeben.

Ping!

Um das an einem praktischen Beispiel fest zu machen, baue ich den bisherigen Komponeten zu einem “Ping-Bot” zusammen. Aufgabe des Bots ist es, zu prüfen ob !ping im Chat geschrieben wurde um anschließend Pong zu antworten.

package main

import (
	"fmt"
	"log"
	"maunium.net/go/mautrix"
	"os"
	"strings"
)

func init() {
	err = loadConfig("config.json")
	if err != nil {
		log.Fatal(err.Error())
		os.Exit(0)
	}
	log.Println("Config loading successful")
}

func main() {
	ping()
}

func loginToServer() (error, *mautrix.Client) {
	log.Printf("Log in to %v as %v", config.Matrix.Homeserver, config.Matrix.Username)

	client, err := mautrix.NewClient(config.Matrix.Homeserver, "", "")
	if err != nil {
		return err, nil
	}

	resp, err := client.Login(&mautrix.ReqLogin{Type: "m.login.password", User: config.Matrix.Username, Password: config.Matrix.Password})
	if err != nil {
		return err, nil
	}

	client.SetCredentials(resp.UserID, resp.AccessToken)
	log.Println("Login successful")

	return err, client
}

func ping() {
	client, err := loginToServer()
	if err != nil {
		log.Println(err)
		os.Exit(0)
	}

	syncer := client.Syncer.(*mautrix.DefaultSyncer)
	syncer.OnEventType(mautrix.EventMessage, func(matrixEvent *mautrix.Event) {
		if strings.HasPrefix(matrixEvent.Content.Body, "!ping") {
			client.SendText(matrixEvent.RoomID, "Pong!")
		}
	})
	err = client.Sync()
	if err != nil {
		fmt.Println(err)
	}
}

main.go

Im Beispiel oben haben ich alles einmal zusammen gebaut, was ich bisher geschrieben haben, mit einer kleinen Neuerung. In der Funktion die an an OnEventType übergeben wird, habe ich einen Check hinzugefügt.

Hier wird geprüft ob die entsprechende Nachricht mit !ping beginnt, trifft dieser Fall ein, wird über client.SendText() eine Nachricht an den Raum aus dem die Nachricht kommt gesendet.

Letzte Worte

Vielen Dank für deine Aufmerksamkeit!

Falls du Anmerkungen zu dem Post hast, freue ich mich jederzeit über E-Mails an feedback@lino.dev.

Quellen

Ich habe mich für dieses kleine Tutorial an dem Code von https://github.com/maubot/gitlab orrientiert, der allerdings noch die legacy version von Mautrix-Go verwendet. Darüberhinaus sind auch Teile aus https://github.com/tulir/mautrix-go/pull/1/commits/7311b6b428d0112ef6cc3343130c2e1047ee475f verwendet worden.