diff --git a/api/joinroom.go b/api/joinroom.go
deleted file mode 100644
index 5826d7844ae78f05eab07696df28ea2ff1cef7bb..0000000000000000000000000000000000000000
--- a/api/joinroom.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package api
-
-import (
-	"fmt"
-	"net/http"
-)
-
-func JoinRoom(token Token, roomId string) error {
-	request, err := http.NewRequest(
-		http.MethodPost,
-		fmt.Sprintf("https://matrix-client.matrix.org/_matrix/client/v3/rooms/%s/join", roomId),
-		nil,
-	)
-	if err != nil {
-		panic(err)
-	}
-	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
-	_, err = http.DefaultClient.Do(request)
-	return err
-}
diff --git a/api/login.go b/api/login.go
deleted file mode 100644
index 3cb9afff16b157878d605d2af1ad9a77b4881eb7..0000000000000000000000000000000000000000
--- a/api/login.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package api
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"net/http"
-)
-
-type Identifier struct {
-	Type string `json:"type"`
-	User string `json:"user"`
-}
-
-type LoginRequest struct {
-	Type                     string     `json:"type"`
-	Identifier               Identifier `json:"identifier"`
-	Password                 string     `json:"password"`
-	RefreshToken             bool       `json:"refresh_token"`
-	DeviceId                 string     `json:"device_id"`
-	InitialDeviceDisplayName string     `json:"initial_device_display_name"`
-}
-
-type LoginResponse struct {
-	AccessToken  string `json:"access_token"`
-	DeviceId     string `json:"device_id"`
-	ExpiresInMs  int    `json:"expires_in_ms"`
-	RefreshToken string `json:"refresh_token"`
-	UserId       string `json:"user_id"`
-}
-
-func Login(username string, password string, deviceId string) (LoginResponse, error) {
-	body, err := json.Marshal(LoginRequest{
-		Type: "m.login.password",
-		Identifier: Identifier{
-			Type: "m.id.user",
-			User: username,
-		},
-		Password:                 password,
-		RefreshToken:             true,
-		DeviceId:                 deviceId,
-		InitialDeviceDisplayName: deviceId,
-	})
-	if err != nil {
-		return LoginResponse{}, err
-	}
-	resp, err := http.Post("https://matrix-client.matrix.org/_matrix/client/v3/login", "application/json", bytes.NewReader(body))
-	if err != nil {
-		return LoginResponse{}, err
-	}
-	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
-		return LoginResponse{}, fmt.Errorf("request failed %d: %s", resp.StatusCode, resp.Status)
-	}
-	var loginResponse LoginResponse
-	err = json.NewDecoder(resp.Body).Decode(&loginResponse)
-	return loginResponse, err
-}
diff --git a/api/markread.go b/api/markread.go
deleted file mode 100644
index 7bbdaa7edf3707b4a5d0d97b0f272b040da9ba42..0000000000000000000000000000000000000000
--- a/api/markread.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package api
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-)
-
-type ReadReceiptRequest struct {
-	FullyRead   string `json:"m.fully.read"`
-	Read        string `json:"m.read"`
-	ReadPrivate string `json:"m.read.private"`
-}
-
-func SetReadReceipt(token Token, roomId string, messageId string) error {
-	body, err := json.Marshal(ReadReceiptRequest{
-		FullyRead:   messageId,
-		Read:        messageId,
-		ReadPrivate: messageId,
-	})
-	if err != nil {
-		return err
-	}
-	request, err := http.NewRequest(
-		http.MethodPost,
-		fmt.Sprintf("https://matrix-client.matrix.org/_matrix/client/v3/rooms/%s/read_markers", roomId),
-		bytes.NewReader(body),
-	)
-	if err != nil {
-		panic(err)
-	}
-	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
-	response, err := http.DefaultClient.Do(request)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode < 200 || response.StatusCode >= 300 {
-		_, _ = io.Copy(os.Stdout, response.Body)
-		return fmt.Errorf("request failed %d: %s", response.StatusCode, response.Status)
-	}
-	return err
-}
diff --git a/api/notification.go b/api/notification.go
deleted file mode 100644
index 8f7dd6537cdf5ea7373300cf412eafda4303f4f6..0000000000000000000000000000000000000000
--- a/api/notification.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package api
-
-import (
-	"encoding/json"
-	"io"
-)
-
-type MessageContent struct {
-	Body          string `json:"body"`
-	Format        string `json:"format"`
-	FormattedBody string `json:"formatted_body"`
-	MsgType       string `json:"msgtype"`
-}
-
-type NotificationRequest struct {
-	Notification Notification `json:"notification"`
-}
-
-type Notification struct {
-	EventId           string         `json:"event_id"`
-	Content           MessageContent `json:"content"`
-	Id                string         `json:"id"`
-	Prio              string         `json:"prio"`
-	RoomId            string         `json:"room_id"`
-	RoomName          string         `json:"room_name"`
-	Sender            string         `json:"sender"`
-	SenderDisplayName string         `json:"sender_display_name"`
-	Type              string         `json:"type"`
-}
-
-func ParseNotification(body io.ReadCloser) (Notification, error) {
-	var request NotificationRequest
-	err := json.NewDecoder(body).Decode(&request)
-	notification := request.Notification
-	if err != nil {
-		return notification, err
-	}
-	return notification, err
-}
diff --git a/api/refresh.go b/api/refresh.go
deleted file mode 100644
index 1dcc5b72cf6ddd4e8581eb12b0f356615c03e7f5..0000000000000000000000000000000000000000
--- a/api/refresh.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package api
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"net/http"
-)
-
-type RefreshRequest struct {
-	RefreshToken string `json:"refresh_token"`
-}
-
-type RefreshResponse struct {
-	AccessToken  string `json:"access_token"`
-	ExpiresInMs  int    `json:"expires_in_ms"`
-	RefreshToken string `json:"refresh_token"`
-}
-
-func Refresh(refreshToken string) (RefreshResponse, error) {
-	body, err := json.Marshal(RefreshRequest{
-		RefreshToken: refreshToken,
-	})
-	if err != nil {
-		return RefreshResponse{}, err
-	}
-	resp, err := http.Post("https://matrix-client.matrix.org/_matrix/client/v3/refresh", "application/json", bytes.NewReader(body))
-	if err != nil {
-		return RefreshResponse{}, err
-	}
-	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
-		return RefreshResponse{}, fmt.Errorf("request failed %d: %s", resp.StatusCode, resp.Status)
-	}
-	var responseBody RefreshResponse
-	err = json.NewDecoder(resp.Body).Decode(&responseBody)
-	return responseBody, err
-}
diff --git a/api/sendmessage.go b/api/sendmessage.go
deleted file mode 100644
index 70e480d43bd9453021140cea57419ce5c071ed74..0000000000000000000000000000000000000000
--- a/api/sendmessage.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package api
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"github.com/google/uuid"
-	"net/http"
-)
-
-func SendMessage(token Token, roomId string, content interface{}) error {
-	transactionId, err := uuid.NewRandom()
-	if err != nil {
-		return err
-	}
-	body, err := json.Marshal(content)
-	if err != nil {
-		return err
-	}
-	request, err := http.NewRequest(
-		http.MethodPut,
-		fmt.Sprintf("https://matrix-client.matrix.org/_matrix/client/v3/rooms/%s/send/%s/%s",
-			roomId,
-			"m.room.message",
-			transactionId.String(),
-		),
-		bytes.NewReader(body),
-	)
-	if err != nil {
-		panic(err)
-	}
-	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
-	response, err := http.DefaultClient.Do(request)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode < 200 || response.StatusCode >= 300 {
-		return fmt.Errorf("request failed %d: %s", response.StatusCode, response.Status)
-	}
-	return err
-}
diff --git a/api/setpusher.go b/api/setpusher.go
deleted file mode 100644
index c223f86c13876bc9c05474ca0f5d1b2c7c9fa5c3..0000000000000000000000000000000000000000
--- a/api/setpusher.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package api
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-)
-
-type PusherRequest struct {
-	AppDisplayName    string     `json:"app_display_name"`
-	AppId             string     `json:"app_id"`
-	Append            bool       `json:"append"`
-	Data              PusherData `json:"data"`
-	DeviceDisplayName string     `json:"device_display_name"`
-	Kind              string     `json:"kind"`
-	Lang              string     `json:"lang"`
-	ProfileTag        string     `json:"profile_tag"`
-	PushKey           string     `json:"pushkey"`
-}
-
-type PusherData struct {
-	Format string `json:"format,omitempty"`
-	Url    string `json:"url"`
-}
-
-func SetPusher(token Token, url string) error {
-	body, err := json.Marshal(PusherRequest{
-		AppDisplayName: "webhook",
-		AppId:          "de.justjanne.webhook",
-		Append:         false,
-		Data: PusherData{
-			Url: url,
-		},
-		DeviceDisplayName: "webhook",
-		Kind:              "http",
-		Lang:              "en",
-		ProfileTag:        "abcdef",
-		PushKey:           "webhook",
-	})
-	if err != nil {
-		return err
-	}
-	fmt.Println(string(body))
-	request, err := http.NewRequest(
-		http.MethodPost,
-		"https://matrix-client.matrix.org/_matrix/client/v3/pushers/set",
-		bytes.NewReader(body),
-	)
-	if err != nil {
-		panic(err)
-	}
-	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
-	response, err := http.DefaultClient.Do(request)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode < 200 || response.StatusCode >= 300 {
-		_, _ = io.Copy(os.Stdout, response.Body)
-		return fmt.Errorf("request failed %d: %s", response.StatusCode, response.Status)
-	}
-	return err
-}
diff --git a/api/token.go b/api/token.go
deleted file mode 100644
index fa9c55954fdde2f1da8ce67522ebd5ef519d5d4b..0000000000000000000000000000000000000000
--- a/api/token.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package api
-
-import "time"
-
-type Token struct {
-	AccessToken  string
-	Expires      time.Time
-	RefreshToken string
-}
diff --git a/go.mod b/go.mod
index 44a23d67472bde27cc99846352e0d19e04af04ed..e975bd9e965aeb6e8c88d9477c1c57acf66d67c7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,15 +1,16 @@
-module git.kuschku.de/justjanne/stateless-matrix-bot
+module git.kuschku.de/justJanne/stateless-matrix-bot
 
 go 1.22.2
 
 require (
 	git.kuschku.de/justJanne/bahn-api v0.0.0-20210606022125-173e9216d8a8
-	github.com/google/uuid v1.6.0
+	git.kuschku.de/justJanne/stateless-matrix-bot-framework v0.1.3
 )
 
 require (
 	github.com/andybalholm/cascadia v1.0.0 // indirect
 	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
+	github.com/google/uuid v1.6.0 // indirect
 	golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01 // indirect
 	golang.org/x/text v0.3.2 // indirect
 )
diff --git a/go.sum b/go.sum
index 0425cac525dfbe7398024a83fb691ffe627077fa..d9db7846c6dce0efd2eb7fb4aca3b141b399bcff 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 git.kuschku.de/justJanne/bahn-api v0.0.0-20210606022125-173e9216d8a8 h1:5VmfteMrWeABylH1lP46QLBEx8YWawIfw2WdfGWSv/I=
 git.kuschku.de/justJanne/bahn-api v0.0.0-20210606022125-173e9216d8a8/go.mod h1:9d+hDIsjtAxjb0FPo6DLNqf9Co7CX35IHScmo9wQGlo=
+git.kuschku.de/justJanne/stateless-matrix-bot-framework v0.1.3 h1:cwQyLajDRdRJYa5PK62BrGMbFZkVi0MIGiFFn1zA0H4=
+git.kuschku.de/justJanne/stateless-matrix-bot-framework v0.1.3/go.mod h1:RF+HfGNMtINYT/+JNQRo3c3Cjaw36evgAVOhYzE0oCo=
 github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
 github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
diff --git a/handle_8ball.go b/handle_8ball.go
new file mode 100644
index 0000000000000000000000000000000000000000..0c692fa34b47517144905c2a80a97ec1f3360b60
--- /dev/null
+++ b/handle_8ball.go
@@ -0,0 +1,54 @@
+package main
+
+import (
+	matrixbot "git.kuschku.de/justJanne/stateless-matrix-bot-framework"
+	"math/rand/v2"
+)
+
+func handle8ball(bot *matrixbot.MatrixBot, notification matrixbot.Notification) error {
+	positive := []string{
+		"It is certain",
+		"It is decidedly so",
+		"Without a doubt",
+		"Yes – definitely",
+		"You may rely on it",
+		"As I see it, yes",
+		"Most Likely",
+		"Outlook good",
+		"Yes",
+		"Signs point to yes.",
+	}
+	negative := []string{
+		"Don’t count on it",
+		"My reply is no",
+		"My sources say no",
+		"Outlook not so good",
+		"very doubtful",
+	}
+	neutral := []string{
+		"Reply hazy",
+		"try again",
+		"Ask again later",
+		"Better not tell you now",
+		"Cannot predict now",
+		"Concentrate and ask again",
+	}
+	var answers []string
+	switch rand.IntN(3) {
+	case 0:
+		answers = positive
+		break
+	case 1:
+		answers = negative
+		break
+	case 2:
+		answers = neutral
+		break
+	}
+	answer := answers[rand.IntN(len(answers))]
+
+	return matrixbot.SendMessage(*bot.Token, notification.RoomId, matrixbot.MessageContent{
+		Body:    answer,
+		MsgType: "m.text",
+	})
+}
diff --git a/handle_trains.go b/handle_trains.go
new file mode 100644
index 0000000000000000000000000000000000000000..03d066fbe14ec2eff337fa7dbdfcf32543f87ec6
--- /dev/null
+++ b/handle_trains.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"git.kuschku.de/justJanne/bahn-api"
+	matrixbot "git.kuschku.de/justJanne/stateless-matrix-bot-framework"
+	"html/template"
+	"net/http"
+	"strings"
+)
+
+var bahnTpl *template.Template
+
+func initTrainHandler() error {
+	var err error
+	bahnTpl, err = template.New("bahn").Parse(`{{- /*gotype: bahn.Timetable*/ -}}
+<b>{{.Station}}</b> Abfahrten
+{{- range .Stops -}}
+    {{- if .Departure -}}
+        {{- if .Departure.Line -}}
+        <li>{{- if .Departure.ChangedTime -}}{{.Departure.ChangedTime}}{{- else if .Departure.PlannedTime -}}{{.Departure.PlannedTime}}{{- end -}}
+            · <b>{{ .Departure.Line }}</b></li>
+        {{- else if .TripLabel.TripCategory -}}
+            {{- if .TripLabel.TripNumber -}}
+                <li>{{- if .Departure.ChangedTime -}}{{.Departure.ChangedTime}}{{- else if .Departure.PlannedTime -}}{{.Departure.PlannedTime}}{{- end -}}
+                    · <b>{{ .TripLabel.TripCategory }} {{ .TripLabel.TripNumber }}</b></li>
+            {{- end -}}
+        {{- end -}}
+    {{- end -}}
+{{- end -}}`)
+	return err
+}
+
+func handleTrains(bot *matrixbot.MatrixBot, notification matrixbot.Notification) error {
+	elements := strings.SplitN(strings.TrimSpace(notification.Content.Body), " ", 3)
+	response, err := http.Get(fmt.Sprintf("https://iris.noncd.db.de/iris-tts/timetable/fchg/%s", elements[1]))
+	if err != nil {
+		return err
+	}
+	timetable, err := bahn.TimetableFromReader(response.Body)
+	if err != nil {
+		return err
+	}
+	buf := new(bytes.Buffer)
+	err = bahnTpl.Execute(buf, timetable)
+	if err != nil {
+		return err
+	}
+	return matrixbot.SendMessage(*bot.Token, notification.RoomId, matrixbot.MessageContent{
+		FormattedBody: buf.String(),
+		Format:        "org.matrix.custom.html",
+		MsgType:       "m.text",
+	})
+}
diff --git a/main.go b/main.go
index 218ce4d7c9ac17247f694b73a4ce9e89fac34921..f19725697c421f0cf7ed5933c45415f02e85096a 100644
--- a/main.go
+++ b/main.go
@@ -1,16 +1,9 @@
 package main
 
 import (
-	"bytes"
-	"fmt"
-	"git.kuschku.de/justJanne/bahn-api"
-	"git.kuschku.de/justjanne/stateless-matrix-bot/api"
-	"html/template"
-	"math/rand/v2"
-	"net/http"
+	"git.kuschku.de/justJanne/stateless-matrix-bot-framework"
 	"net/url"
 	"os"
-	"strings"
 )
 
 func main() {
@@ -20,99 +13,14 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
-	bot := NewMatrixBot(pushUrl)
-
-	// !8ball handler
-	bot.HandleFunc("!8ball", func(bot *MatrixBot, notification api.Notification) error {
-		positive := []string{
-			"It is certain",
-			"It is decidedly so",
-			"Without a doubt",
-			"Yes – definitely",
-			"You may rely on it",
-			"As I see it, yes",
-			"Most Likely",
-			"Outlook good",
-			"Yes",
-			"Signs point to yes.",
-		}
-		negative := []string{
-			"Don’t count on it",
-			"My reply is no",
-			"My sources say no",
-			"Outlook not so good",
-			"very doubtful",
-		}
-		neutral := []string{
-			"Reply hazy",
-			"try again",
-			"Ask again later",
-			"Better not tell you now",
-			"Cannot predict now",
-			"Concentrate and ask again",
-		}
-		var answers []string
-		switch rand.IntN(3) {
-		case 0:
-			answers = positive
-			break
-		case 1:
-			answers = negative
-			break
-		case 2:
-			answers = neutral
-			break
-		}
-		answer := answers[rand.IntN(len(answers))]
-
-		err = api.SendMessage(*bot.token, notification.RoomId, api.MessageContent{
-			Body:    answer,
-			MsgType: "m.text",
-		})
-		return nil
-	})
-
-	// !trains handler
-	bahnTpl, err := template.New("bahn").Parse(`{{- /*gotype: bahn.Timetable*/ -}}
-<b>{{.Station}}</b> Abfahrten
-{{- range .Stops -}}
-    {{- if .Departure -}}
-        {{- if .Departure.Line -}}
-        <li>{{- if .Departure.ChangedTime -}}{{.Departure.ChangedTime}}{{- else if .Departure.PlannedTime -}}{{.Departure.PlannedTime}}{{- end -}}
-            · <b>{{ .Departure.Line }}</b></li>
-        {{- else if .TripLabel.TripCategory -}}
-            {{- if .TripLabel.TripNumber -}}
-                <li>{{- if .Departure.ChangedTime -}}{{.Departure.ChangedTime}}{{- else if .Departure.PlannedTime -}}{{.Departure.PlannedTime}}{{- end -}}
-                    · <b>{{ .TripLabel.TripCategory }} {{ .TripLabel.TripNumber }}</b></li>
-            {{- end -}}
-        {{- end -}}
-    {{- end -}}
-{{- end -}}`)
+	bot := matrixbot.NewMatrixBot(pushUrl)
+	bot.HandleFunc("!8ball", handle8ball)
+	bot.HandleFunc("!trains", handleTrains)
+	err = initTrainHandler()
 	if err != nil {
 		panic(err)
 	}
-	bot.HandleFunc("!trains", func(bot *MatrixBot, notification api.Notification) error {
-		elements := strings.SplitN(strings.TrimSpace(notification.Content.Body), " ", 3)
-		response, err := http.Get(fmt.Sprintf("https://iris.noncd.db.de/iris-tts/timetable/fchg/%s", elements[1]))
-		if err != nil {
-			return err
-		}
-		timetable, err := bahn.TimetableFromReader(response.Body)
-		if err != nil {
-			return err
-		}
-		buf := new(bytes.Buffer)
-		err = bahnTpl.Execute(buf, timetable)
-		if err != nil {
-			return err
-		}
-		err = api.SendMessage(*bot.token, notification.RoomId, api.MessageContent{
-			FormattedBody: buf.String(),
-			Format:        "org.matrix.custom.html",
-			MsgType:       "m.text",
-		})
-		return nil
-	})
+
 	err = bot.Login(os.Getenv("BOT_USERNAME"), os.Getenv("BOT_PASSWORD"), os.Getenv("BOT_DEVICEID"))
 	if err != nil {
 		panic(err)
diff --git a/matrixbot.go b/matrixbot.go
deleted file mode 100644
index 2604f8fcd5c59bee008f4d1be4a2e24595531342..0000000000000000000000000000000000000000
--- a/matrixbot.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"git.kuschku.de/justjanne/stateless-matrix-bot/api"
-	"io"
-	"log"
-	"net/http"
-	"net/url"
-	"strings"
-	"time"
-)
-
-type MatrixBot struct {
-	token    *api.Token
-	pushUrl  *url.URL
-	handlers map[string]func(bot *MatrixBot, notification api.Notification) error
-}
-
-func NewMatrixBot(pushUrl *url.URL) *MatrixBot {
-	return &MatrixBot{
-		token:    nil,
-		pushUrl:  pushUrl,
-		handlers: make(map[string]func(bot *MatrixBot, notification api.Notification) error),
-	}
-}
-
-func (bot *MatrixBot) RefreshToken() error {
-	if bot.token == nil {
-		return fmt.Errorf("no refresh token available")
-	}
-	userData, err := api.Refresh(bot.token.RefreshToken)
-	if err != nil {
-		return err
-	}
-	bot.token = &api.Token{
-		AccessToken:  userData.AccessToken,
-		Expires:      time.Now().Add(time.Duration(userData.ExpiresInMs) / 2 * time.Millisecond),
-		RefreshToken: userData.RefreshToken,
-	}
-	return nil
-}
-
-func (bot *MatrixBot) Login(username string, password string, deviceId string) error {
-	userData, err := api.Login(username, password, deviceId)
-	if err != nil {
-		return err
-	}
-	bot.token = &api.Token{
-		AccessToken:  userData.AccessToken,
-		Expires:      time.Now().Add(time.Duration(userData.ExpiresInMs) / 2 * time.Millisecond),
-		RefreshToken: userData.RefreshToken,
-	}
-	return nil
-}
-
-func (bot *MatrixBot) RefreshTask() {
-	for true {
-		if bot.token != nil && time.Now().After(bot.token.Expires) {
-			if err := bot.RefreshToken(); err != nil {
-				log.Printf("error refresh token: %s\n", err.Error())
-			}
-		}
-		time.Sleep(1 * time.Second)
-	}
-}
-
-func (bot *MatrixBot) RegisterPusher() error {
-	return api.SetPusher(*bot.token, bot.pushUrl.String())
-}
-
-func (bot *MatrixBot) HandleFunc(command string, handler func(bot *MatrixBot, notification api.Notification) error) {
-	bot.handlers[command] = handler
-}
-
-func (bot *MatrixBot) Serve(endpoint string) {
-	http.HandleFunc("/healthz", func(writer http.ResponseWriter, request *http.Request) {
-		_, _ = io.WriteString(writer, "OK\n")
-	})
-	log.Printf("listening for push notifications on %s\n", bot.pushUrl.Path)
-	http.HandleFunc(bot.pushUrl.Path, func(writer http.ResponseWriter, request *http.Request) {
-		log.Println("Received push notification")
-		notification, err := api.ParseNotification(request.Body)
-		if err != nil {
-			log.Println(err.Error())
-			return
-		}
-		if notification.EventId == "" {
-			return
-		}
-		err = api.SetReadReceipt(*bot.token, notification.RoomId, notification.EventId)
-		if err != nil {
-			log.Println(err.Error())
-			return
-		}
-		command := strings.SplitN(notification.Content.Body, " ", 2)[0]
-		handler, ok := bot.handlers[command]
-		if !ok {
-			log.Printf("could not find handler for '%s'\n", command)
-			return
-		}
-		err = handler(bot, notification)
-		if err != nil {
-			log.Println(err.Error())
-			return
-		}
-		writer.WriteHeader(204)
-	})
-	fmt.Printf("listening for requests on %s\n", endpoint)
-	log.Fatal(http.ListenAndServe(endpoint, nil))
-}