First commit
This commit is contained in:
commit
0cef3e0a90
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Dockerfile for Matrix Sports Bot
|
||||||
|
|
||||||
|
# --- Build Stage ---
|
||||||
|
FROM golang:1.20-alpine AS builder
|
||||||
|
|
||||||
|
# Install git for module downloads
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build binary
|
||||||
|
RUN go build -o matrix-scores-bot ./main.go
|
||||||
|
|
||||||
|
# --- Final Stage ---
|
||||||
|
FROM alpine:3.18
|
||||||
|
|
||||||
|
# Install certificates
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# Copy the built binary
|
||||||
|
COPY --from=builder /app/matrix-scores-bot .
|
||||||
|
|
||||||
|
# Expose any port if needed (Matrix client doesn't listen)
|
||||||
|
# EXPOSE 0
|
||||||
|
|
||||||
|
# Set entrypoint
|
||||||
|
ENTRYPOINT ["./matrix-scores-bot"]
|
||||||
|
|
100
README.md
Normal file
100
README.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Matrix Sports Bot
|
||||||
|
|
||||||
|
A Matrix bot written in Go that retrieves and displays college football (CFB) and basketball (CBB) scores using the `ncaa.ewnix.net` API. It listens for `!cfb` and `!cbb` commands in Matrix rooms and replies with formatted game information.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **CFB Scores**: `!cfb <teamName>`
|
||||||
|
- **CBB Scores**: `!cbb <teamName> [MM/DD]`
|
||||||
|
- Automatic conversion of dates to Central Time
|
||||||
|
- Custom team name aliases via configuration
|
||||||
|
- HTML formatting for bold scores in Matrix
|
||||||
|
- Dockerized for easy deployment to Kubernetes (K3s)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go 1.20+ (for local development)
|
||||||
|
- Docker (for building the container)
|
||||||
|
- Access to a Matrix homeserver (e.g., Synapse)
|
||||||
|
- A bot user created on your Matrix server with an access token
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. Open `config/config.go`.
|
||||||
|
2. Add your Matrix credentials:
|
||||||
|
|
||||||
|
```go
|
||||||
|
return &Config{
|
||||||
|
Homeserver: "https://matrix.your-server.net",
|
||||||
|
UserID: "@your-bot:your-server.net",
|
||||||
|
AccessToken: "YOUR_ACCESS_TOKEN",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. (Optional) Define custom team name aliases:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var CustomTeamNames = map[string]string{
|
||||||
|
"bama": "alabama",
|
||||||
|
"osu": "ohio state",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
1. Clone the repository and navigate to its root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-org/matrix-scores-bot.git
|
||||||
|
cd matrix-scores-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialize Go modules and download dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod init matrix-scores-bot
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Invite the bot to a Matrix room and send commands:
|
||||||
|
|
||||||
|
```text
|
||||||
|
!cfb alabama
|
||||||
|
!cbb duke 12/05
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
1. Build the Docker image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t matrix-scores-bot:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name matrix-scores-bot matrix-scores-bot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** The bot’s credentials are hardcoded in `config/config.go`. Rebuild the image after updating credentials.
|
||||||
|
|
||||||
|
## Kubernetes (K3s)
|
||||||
|
|
||||||
|
You can deploy the bot as a Deployment in K3s. Create a Secret for your access token and mount it, or update `config/config.go` before building. Follow standard practices for containerized services.
|
||||||
|
|
||||||
|
## Command Reference
|
||||||
|
|
||||||
|
- `!cfb <teamName>`: Retrieves the latest college football game info for the specified team.
|
||||||
|
- `!cbb <teamName> [MM/DD]`: Retrieves college basketball game info for the specified team on the given date (defaults to today in Central Time).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © Kevin M. Thompson
|
||||||
|
|
124
cbb/cbb_client.go
Normal file
124
cbb/cbb_client.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package cbb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Game represents the nested game structure from the API response.
|
||||||
|
type Game struct {
|
||||||
|
Game struct {
|
||||||
|
GameID string `json:"gameID"`
|
||||||
|
StartDate string `json:"startDate"`
|
||||||
|
StartTime string `json:"startTime"`
|
||||||
|
GameState string `json:"gameState"`
|
||||||
|
CurrentPeriod string `json:"currentPeriod"`
|
||||||
|
ContestClock string `json:"contestClock"`
|
||||||
|
Home Team `json:"home"`
|
||||||
|
Away Team `json:"away"`
|
||||||
|
FinalMessage string `json:"finalMessage"`
|
||||||
|
} `json:"game"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Team holds score and name details.
|
||||||
|
type Team struct {
|
||||||
|
Score string `json:"score"`
|
||||||
|
Names struct {
|
||||||
|
Short string `json:"short"`
|
||||||
|
Full string `json:"full"`
|
||||||
|
} `json:"names"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGameInfo fetches and returns basketball game info for a team on a given date.
|
||||||
|
func GetGameInfo(teamName, date string) string {
|
||||||
|
teamKey := strings.ToLower(strings.TrimSpace(teamName))
|
||||||
|
|
||||||
|
// Determine date: today in Central Time if none provided
|
||||||
|
if date == "" {
|
||||||
|
loc, err := time.LoadLocation("America/Chicago")
|
||||||
|
if err != nil {
|
||||||
|
loc = time.FixedZone("CST", -6*60*60)
|
||||||
|
}
|
||||||
|
date = time.Now().In(loc).Format("2006/01/02")
|
||||||
|
} else {
|
||||||
|
// Accept MM/DD and convert
|
||||||
|
r := []rune(date)
|
||||||
|
if len(r) == 5 && unicode.IsDigit(r[0]) && unicode.IsDigit(r[1]) && r[2] == '/' && unicode.IsDigit(r[3]) && unicode.IsDigit(r[4]) {
|
||||||
|
date = fmt.Sprintf("2025/%s", date)
|
||||||
|
} else {
|
||||||
|
return "Invalid date format. Please use MM/DD."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := fmt.Sprintf("https://ncaa.ewnix.net/scoreboard/basketball-men/d1/%s", date)
|
||||||
|
log.Printf("Fetching CBB data from API: %s", apiURL)
|
||||||
|
|
||||||
|
resp, err := http.Get(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Failed to reach the scoreboard API: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Sprintf("API error: status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Error reading API response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResponse struct {
|
||||||
|
Games []Game `json:"games"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
||||||
|
return "Error parsing the API response."
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []string{}
|
||||||
|
for _, g := range apiResponse.Games {
|
||||||
|
h := strings.ToLower(g.Game.Home.Names.Short)
|
||||||
|
a := strings.ToLower(g.Game.Away.Names.Short)
|
||||||
|
if h == teamKey || a == teamKey {
|
||||||
|
s := g.Game
|
||||||
|
var info string
|
||||||
|
if s.GameState == "live" {
|
||||||
|
period := s.CurrentPeriod
|
||||||
|
if period == "" {
|
||||||
|
period = "HALFTIME"
|
||||||
|
}
|
||||||
|
info = fmt.Sprintf("Live: %s: </strong>%s</strong> %s: <strong>%s</strong> | Half: %s | Time Remaining: %s",
|
||||||
|
s.Away.Names.Short, s.Away.Score,
|
||||||
|
s.Home.Names.Short, s.Home.Score,
|
||||||
|
period, s.ContestClock)
|
||||||
|
} else if s.GameState == "pre" {
|
||||||
|
startTime, err := time.Parse("03:04PM ET", s.StartTime)
|
||||||
|
if err == nil {
|
||||||
|
startTime = startTime.Add(-1 * time.Hour)
|
||||||
|
info = fmt.Sprintf("Upcoming: %s @ %s on %s at %s CT",
|
||||||
|
s.Away.Names.Short, s.Home.Names.Short, s.StartDate, startTime.Format("03:04 PM CT"))
|
||||||
|
} else {
|
||||||
|
info = fmt.Sprintf("Upcoming: %s @ %s on %s at %s",
|
||||||
|
s.Away.Names.Short, s.Home.Names.Short, s.StartDate, s.StartTime)
|
||||||
|
}
|
||||||
|
} else if s.GameState == "final" {
|
||||||
|
info = fmt.Sprintf("Final: %s: <strong>%s</strong> %s: <strong>%s</strong>",
|
||||||
|
s.Away.Names.Short, s.Away.Score,
|
||||||
|
s.Home.Names.Short, s.Home.Score)
|
||||||
|
}
|
||||||
|
results = append(results, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 0 {
|
||||||
|
return strings.Join(results, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "No game found for the specified team."
|
||||||
|
}
|
146
cfb/cfb_client.go
Normal file
146
cfb/cfb_client.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package cfb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"matrix-scores-bot/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Game holds the nested game data structure from the API.
|
||||||
|
type Game struct {
|
||||||
|
Game struct {
|
||||||
|
GameID string `json:"gameID"`
|
||||||
|
StartDate string `json:"startDate"`
|
||||||
|
StartTime string `json:"startTime"`
|
||||||
|
StartTimeEpoch string `json:"startTimeEpoch"`
|
||||||
|
GameState string `json:"gameState"`
|
||||||
|
CurrentPeriod string `json:"currentPeriod"`
|
||||||
|
ContestClock string `json:"contestClock"`
|
||||||
|
Home Team `json:"home"`
|
||||||
|
Away Team `json:"away"`
|
||||||
|
FinalMessage string `json:"finalMessage"`
|
||||||
|
} `json:"game"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Team holds the team's score and names.
|
||||||
|
type Team struct {
|
||||||
|
Score string `json:"score"`
|
||||||
|
Names struct {
|
||||||
|
Short string `json:"short"`
|
||||||
|
Full string `json:"full"`
|
||||||
|
} `json:"names"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGameInfo fetches and returns information for a specific team or week.
|
||||||
|
func GetGameInfo(input string) string {
|
||||||
|
teamName := strings.TrimSpace(strings.ToLower(input))
|
||||||
|
week := ""
|
||||||
|
|
||||||
|
// Replace custom team name if configured
|
||||||
|
if custom, ok := config.CustomTeamNames[teamName]; ok {
|
||||||
|
log.Printf("Custom team name detected: %s -> %s", teamName, custom)
|
||||||
|
teamName = custom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect week if last two chars are digits
|
||||||
|
if len(teamName) >= 2 {
|
||||||
|
last2 := teamName[len(teamName)-2:]
|
||||||
|
if unicode.IsDigit(rune(last2[0])) && unicode.IsDigit(rune(last2[1])) {
|
||||||
|
week = last2
|
||||||
|
teamName = strings.TrimSpace(teamName[:len(teamName)-2])
|
||||||
|
log.Printf("Week detected: %s", week)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build URL
|
||||||
|
url := fmt.Sprintf("https://ncaa.ewnix.net/scoreboard/football/fbs/2024/%s/all-conf", week)
|
||||||
|
if week == "" {
|
||||||
|
url = "https://ncaa.ewnix.net/scoreboard/football/fbs/2024/current/all-conf"
|
||||||
|
}
|
||||||
|
log.Printf("Fetching CFB data from API: %s", url)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Failed to reach the scoreboard API: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Sprintf("API error: status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Error reading API response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResponse struct {
|
||||||
|
Games []Game `json:"games"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
||||||
|
return "Error parsing the API response."
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []string{}
|
||||||
|
teamKey := strings.ToLower(teamName)
|
||||||
|
|
||||||
|
for _, g := range apiResponse.Games {
|
||||||
|
h := strings.ToLower(g.Game.Home.Names.Short)
|
||||||
|
a := strings.ToLower(g.Game.Away.Names.Short)
|
||||||
|
|
||||||
|
if h == teamKey || a == teamKey {
|
||||||
|
var info string
|
||||||
|
s := g.Game
|
||||||
|
if s.GameState == "live" {
|
||||||
|
period := s.CurrentPeriod
|
||||||
|
if period == "" {
|
||||||
|
period = "HALFTIME"
|
||||||
|
}
|
||||||
|
info = fmt.Sprintf("%s: <strong>%s</strong> %s: <strong>%s</strong> | Quarter: %s | Time Remaining: %s",
|
||||||
|
s.Away.Names.Short, s.Away.Score,
|
||||||
|
s.Home.Names.Short, s.Home.Score,
|
||||||
|
period, s.ContestClock)
|
||||||
|
} else if s.GameState == "pre" {
|
||||||
|
timeCT, err := convertEpochToCentralTime(s.StartTimeEpoch)
|
||||||
|
if err != nil {
|
||||||
|
timeCT = s.StartTime
|
||||||
|
}
|
||||||
|
info = fmt.Sprintf("Upcoming: %s @ %s on %s at %s CT",
|
||||||
|
s.Away.Names.Short, s.Home.Names.Short, s.StartDate, timeCT)
|
||||||
|
} else if s.GameState == "final" {
|
||||||
|
info = fmt.Sprintf("Final: %s: <strong>%s</strong> %s: <strong>%s</strong>",
|
||||||
|
s.Away.Names.Short, s.Away.Score,
|
||||||
|
s.Home.Names.Short, s.Home.Score)
|
||||||
|
}
|
||||||
|
results = append(results, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 0 {
|
||||||
|
return strings.Join(results, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "No game found for the specified team."
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertEpochToCentralTime(epochStr string) (string, error) {
|
||||||
|
epoch, err := strconv.ParseInt(epochStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error parsing epoch: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Unix(epoch, 0).UTC()
|
||||||
|
loc, err := time.LoadLocation("America/Chicago")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error loading location: %w", err)
|
||||||
|
}
|
||||||
|
return t.In(loc).Format("03:04 PM"), nil
|
||||||
|
}
|
24
config/config.go
Normal file
24
config/config.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// CustomTeamNames maps lowercase custom inputs to API-recognized team codes.
|
||||||
|
// Add aliases here, e.g.: "bama": "alabama", "osu": "ohio state"
|
||||||
|
var CustomTeamNames = map[string]string{
|
||||||
|
"bama": "alabama",
|
||||||
|
"barn": "auburn",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds the Matrix homeserver URL, bot user ID, and access token.
|
||||||
|
type Config struct {
|
||||||
|
Homeserver string
|
||||||
|
UserID string
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load returns configuration with hardcoded values; replace with your bot's credentials.
|
||||||
|
func Load() *Config {
|
||||||
|
return &Config{
|
||||||
|
Homeserver: "", // the URL to your Homeserver
|
||||||
|
UserID: "", // your bot's user ID
|
||||||
|
AccessToken: "", // your bot's access token
|
||||||
|
}
|
||||||
|
}
|
20
go.mod
Normal file
20
go.mod
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module matrix-scores-bot
|
||||||
|
|
||||||
|
go 1.24.2
|
||||||
|
|
||||||
|
require maunium.net/go/mautrix v0.23.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
go.mau.fi/util v0.8.6 // indirect
|
||||||
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
|
)
|
46
go.sum
Normal file
46
go.sum
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
|
||||||
|
go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
|
||||||
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
maunium.net/go/mautrix v0.23.3 h1:U+fzdcLhFKLUm5gf2+Q0hEUqWkwDMRfvE+paUH9ogSk=
|
||||||
|
maunium.net/go/mautrix v0.23.3/go.mod h1:LX+3evXVKSvh/b43BVC3rkvN2qV7b0bkIV4fY7Snn/4=
|
26
main.go
Normal file
26
main.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"matrix-scores-bot/config"
|
||||||
|
"matrix-scores-bot/matrixbot"
|
||||||
|
|
||||||
|
mautrix "maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
// Initialize Matrix client with homeserver URL, bot user ID, and access token
|
||||||
|
client, err := mautrix.NewClient(cfg.Homeserver, id.UserID(cfg.UserID), cfg.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create Matrix client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot := matrixbot.NewBot(client)
|
||||||
|
log.Println("Matrix sports bot started...")
|
||||||
|
if err := bot.Start(); err != nil {
|
||||||
|
log.Fatalf("Bot failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
37
matrixbot/bot.go
Normal file
37
matrixbot/bot.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package matrixbot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mautrix "maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bot encapsulates a Matrix client.
|
||||||
|
type Bot struct {
|
||||||
|
Client *mautrix.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBot creates a new Bot instance.
|
||||||
|
func NewBot(client *mautrix.Client) *Bot {
|
||||||
|
return &Bot{Client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins syncing with the Matrix server and sets up event handlers.
|
||||||
|
// It waits 30 seconds after connecting before registering the message handler
|
||||||
|
// to allow historical messages to be skipped.
|
||||||
|
func (b *Bot) Start() error {
|
||||||
|
syncer := b.Client.Syncer.(*mautrix.DefaultSyncer)
|
||||||
|
|
||||||
|
// Pause for 30 seconds to skip over historical messages
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
|
// Register message handler for new events only
|
||||||
|
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
|
||||||
|
go HandleMessage(evt, b.Client)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the sync loop (blocks until error).
|
||||||
|
return b.Client.Sync()
|
||||||
|
}
|
70
matrixbot/handler.go
Normal file
70
matrixbot/handler.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package matrixbot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"matrix-scores-bot/cbb"
|
||||||
|
"matrix-scores-bot/cfb"
|
||||||
|
|
||||||
|
mautrix "maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleMessage processes incoming Matrix messages and dispatches commands with HTML formatting.
|
||||||
|
func HandleMessage(evt *event.Event, client *mautrix.Client) {
|
||||||
|
msg := evt.Content.AsMessage()
|
||||||
|
if msg.MsgType != event.MsgText {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fields := strings.Fields(msg.Body)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := fields[0]
|
||||||
|
var plain, html string
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "!cfb":
|
||||||
|
if len(fields) < 2 {
|
||||||
|
plain = "Usage: !cfb <teamName>"
|
||||||
|
html = plain
|
||||||
|
} else {
|
||||||
|
team := strings.Join(fields[1:], " ")
|
||||||
|
html = cfb.GetGameInfo(team)
|
||||||
|
plain = strings.ReplaceAll(html, "<strong>", "")
|
||||||
|
plain = strings.ReplaceAll(plain, "</strong>", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "!cbb":
|
||||||
|
if len(fields) < 2 {
|
||||||
|
plain = "Usage: !cbb <teamName> [MM/DD]"
|
||||||
|
html = plain
|
||||||
|
} else {
|
||||||
|
team := fields[1]
|
||||||
|
date := ""
|
||||||
|
if len(fields) >= 3 {
|
||||||
|
date = fields[2]
|
||||||
|
}
|
||||||
|
html = cbb.GetGameInfo(team, date)
|
||||||
|
plain = strings.ReplaceAll(html, "<strong>", "")
|
||||||
|
plain = strings.ReplaceAll(plain, "</strong>", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := event.MessageEventContent{
|
||||||
|
MsgType: event.MsgText,
|
||||||
|
Body: plain,
|
||||||
|
Format: "org.matrix.custom.html",
|
||||||
|
FormattedBody: html,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.SendMessageEvent(context.Background(), evt.RoomID, event.EventMessage, content); err != nil {
|
||||||
|
// Log failure but do not panic
|
||||||
|
println("Failed to send formatted message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user