From 5d9ef4c5332d0f1e39313c115818429da2d2e13f Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <janne@kuschku.de> Date: Fri, 28 Feb 2025 18:51:41 +0100 Subject: [PATCH] feat: split framework from prototype --- api/joinroom.go | 20 -------- api/login.go | 57 ----------------------- api/markread.go | 45 ------------------ api/notification.go | 39 ---------------- api/refresh.go | 37 --------------- api/sendmessage.go | 41 ---------------- api/setpusher.go | 65 -------------------------- api/token.go | 9 ---- go.mod | 5 +- go.sum | 2 + handle_8ball.go | 54 +++++++++++++++++++++ handle_trains.go | 55 ++++++++++++++++++++++ main.go | 104 +++-------------------------------------- matrixbot.go | 111 -------------------------------------------- 14 files changed, 120 insertions(+), 524 deletions(-) delete mode 100644 api/joinroom.go delete mode 100644 api/login.go delete mode 100644 api/markread.go delete mode 100644 api/notification.go delete mode 100644 api/refresh.go delete mode 100644 api/sendmessage.go delete mode 100644 api/setpusher.go delete mode 100644 api/token.go create mode 100644 handle_8ball.go create mode 100644 handle_trains.go delete mode 100644 matrixbot.go diff --git a/api/joinroom.go b/api/joinroom.go deleted file mode 100644 index 5826d78..0000000 --- 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 3cb9aff..0000000 --- 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 7bbdaa7..0000000 --- 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 8f7dd65..0000000 --- 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 1dcc5b7..0000000 --- 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 70e480d..0000000 --- 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 c223f86..0000000 --- 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 fa9c559..0000000 --- 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 44a23d6..e975bd9 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 0425cac..d9db784 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 0000000..0c692fa --- /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 0000000..03d066f --- /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 218ce4d..f197256 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 2604f8f..0000000 --- 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)) -} -- GitLab