Skip to content
Snippets Groups Projects
Verified Commit 5d9ef4c5 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

feat: split framework from prototype

parent cb985aef
No related branches found
No related tags found
No related merge requests found
Pipeline #3041 passed
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
}
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
}
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
}
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
}
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
}
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
}
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
}
package api
import "time"
type Token struct {
AccessToken string
Expires time.Time
RefreshToken string
}
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
)
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",
})
}
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",
})
}
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)
......
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))
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment