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

initial commit

parents
No related branches found
No related tags found
No related merge requests found
/.idea/
!/.idea/.name
FROM golang:1.24-alpine AS go_builder
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go build -o app
FROM scratch
COPY --from=go_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=go_builder /src/app /app
ENTRYPOINT ["/app"]
package api
import (
"fmt"
"net/http"
)
func JoinRoom(userData LoginResponse, 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", userData.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(userData LoginResponse, 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", userData.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"
"github.com/google/uuid"
"net/http"
)
func SendMessage(userData LoginResponse, 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", userData.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(userData LoginResponse, 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", userData.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
}
go.mod 0 → 100644
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
)
require (
github.com/andybalholm/cascadia v1.0.0 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01 // indirect
golang.org/x/text v0.3.2 // indirect
)
go.sum 0 → 100644
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=
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=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01 h1:po1f06KS05FvIQQA2pMuOWZAUXiy1KYdIf0ElUU2Hhc=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
main.go 0 → 100644
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"
"os"
"strings"
)
func main() {
bot, err := NewMatrixBot(
os.Getenv("BOT_USERNAME"),
os.Getenv("BOT_PASSWORD"),
os.Getenv("BOT_DEVICEID"),
os.Getenv("BOT_PUSHURL"),
)
if err != nil {
panic(err)
}
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(2) {
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.userData, notification.RoomId, api.MessageContent{
Body: answer,
MsgType: "m.text",
})
return nil
})
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 -}}`)
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.userData, notification.RoomId, api.MessageContent{
FormattedBody: buf.String(),
Format: "org.matrix.custom.html",
MsgType: "m.text",
})
return nil
})
bot.Serve(":8080")
}
package main
import (
"git.kuschku.de/justjanne/stateless-matrix-bot/api"
"io"
"log"
"net/http"
"strings"
)
type MatrixBot struct {
userData api.LoginResponse
handlers map[string]func(bot *MatrixBot, notification api.Notification) error
}
func NewMatrixBot(
username string,
password string,
deviceId string,
url string,
) (*MatrixBot, error) {
userData, err := api.Login(username, password, deviceId)
if err != nil {
return nil, err
}
err = api.SetPusher(userData, url)
if err != nil {
return nil, err
}
return &MatrixBot{
userData: userData,
handlers: make(map[string]func(bot *MatrixBot, notification api.Notification) error),
}, nil
}
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")
})
http.HandleFunc("/_matrix/push/v1/notify", func(writer http.ResponseWriter, request *http.Request) {
notification, err := api.ParseNotification(request.Body)
if err != nil {
log.Println(err.Error())
return
}
if notification.EventId == "" {
return
}
err = api.SetReadReceipt(bot.userData, 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
}
})
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