Add /schedule command

Adds the Discord slash command to get the schedule for the next few
weeks.

Also updates the required Go version to 1.21 to benefit from the new
`time.DateOnly` format that's gonna be used in the absences table.
William Perron 1 year ago
parent e766208a95
commit 12ddb567e3
Signed by: wperron
GPG Key ID: BFDB4EF72D73C5F2

@ -1,4 +1,4 @@
FROM golang:1.19-buster as builder FROM golang:1.21-bullseye as builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN mkdir ./bin; go build -buildvcs=false -o ./bin ./cmd/... RUN mkdir ./bin; go build -buildvcs=false -o ./bin ./cmd/...

@ -22,7 +22,7 @@ func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string
return fmt.Errorf("failed to prepare absence query: %w", err) return fmt.Errorf("failed to prepare absence query: %w", err)
} }
_, err = stmt.ExecContext(ctx, session.Format("2006-01-02"), userId) _, err = stmt.ExecContext(ctx, session.Format(time.DateOnly), userId)
if err != nil { if err != nil {
return fmt.Errorf("failed to insert absence: %w", err) return fmt.Errorf("failed to insert absence: %w", err)
} }
@ -42,7 +42,7 @@ func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string,
return nil, fmt.Errorf("failed to prepare query: %w", err) return nil, fmt.Errorf("failed to prepare query: %w", err)
} }
rows, err := stmt.QueryContext(ctx, session.Format("2006-01-02")) rows, err := stmt.QueryContext(ctx, session.Format(time.DateOnly))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to execute query: %w", err) return nil, fmt.Errorf("failed to execute query: %w", err)
} }
@ -61,12 +61,13 @@ func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string,
return absentees, nil return absentees, nil
} }
type Foo struct { // map session_date -> list of absentees
session_date string type Schedule map[string][]string
userid string
} func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) (Schedule, error) {
schedule := make(Schedule)
initSchedule(schedule, from, to)
func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) ([]Foo, error) {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err) return nil, fmt.Errorf("failed to begin transaction: %w", err)
@ -78,21 +79,32 @@ func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) ([]Foo, err
return nil, fmt.Errorf("failed to prepare query: %w", err) return nil, fmt.Errorf("failed to prepare query: %w", err)
} }
rows, err := stmt.QueryContext(ctx, from.Format("2006-01-02"), to.Format("2006-01-02")) rows, err := stmt.QueryContext(ctx, from.Format(time.DateOnly), to.Format(time.DateOnly))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to execute query: %w", err) return nil, fmt.Errorf("failed to execute query: %w", err)
} }
schedule := make([]Foo, 0)
for rows.Next() { for rows.Next() {
var foo Foo var date string
err = rows.Scan(&foo.session_date, &foo.userid) var user string
err = rows.Scan(&date, &user)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err) return nil, fmt.Errorf("failed to scan row: %w", err)
} }
schedule = append(schedule, foo) if _, ok := schedule[date]; ok {
schedule[date] = append(schedule[date], user)
} else {
schedule[date] = []string{user}
}
} }
return schedule, nil return schedule, nil
} }
func initSchedule(schedule Schedule, from, to time.Time) {
for from.Before(to) || from.Equal(to) {
schedule[from.Format(time.DateOnly)] = []string{}
from = from.AddDate(0, 0, 7)
}
}

@ -35,7 +35,15 @@ func TestGetSchedule(t *testing.T) {
_ = store.AddAbsence(context.TODO(), now.Add(7*24*time.Hour), "foobar") _ = store.AddAbsence(context.TODO(), now.Add(7*24*time.Hour), "foobar")
schedule, err := store.GetSchedule(context.TODO(), now, now.Add(2*7*24*time.Hour)) schedule, err := store.GetSchedule(context.TODO(), now, now.AddDate(0, 0, 14))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, len(schedule)) // reason being, the schedule should initialize to the desired time range
assert.Equal(t, 3, len(schedule))
for d, a := range schedule {
if d == now.Add(7*24*time.Hour).Format(time.DateOnly) {
assert.Equal(t, 1, len(a))
} else {
assert.Equal(t, 0, len(a))
}
}
} }

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"sort"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -152,6 +153,11 @@ func main() {
}, },
}, },
}, },
{
Name: "schedule",
Description: "Get the schedule for the following weeks.",
Type: discordgo.ChatApplicationCommand,
},
} }
handlers := map[string]Handler{ handlers := map[string]Handler{
"info": func(s *discordgo.Session, i *discordgo.InteractionCreate) { "info": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
@ -438,7 +444,44 @@ func main() {
} }
}, },
"schedule": func(s *discordgo.Session, i *discordgo.InteractionCreate) { "schedule": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
store.GetSchedule(ctx, time.Now(), time.Now().Add(4*7*24*time.Hour)) // get schedule from now to 4 mondays into the future
sched, err := store.GetSchedule(ctx, themis.NextMonday(), themis.NextMonday().Add(4*7*24*time.Hour))
if err != nil {
log.Error().Err(err).Msg("failed to get schedule")
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "failed to get schedule, check logs for more info.",
},
}); err != nil {
log.Error().Err(err).Msg("failed to respond to interaction")
}
}
sb := strings.Builder{}
keys := make([]string, 0, len(sched))
for k := range sched {
keys = append(keys, k)
}
sort.Strings(keys)
for _, d := range keys {
sb.WriteString(d + ": ")
if len(sched[d]) == 0 {
sb.WriteString("Everyone is available!\n")
} else {
sb.WriteString(strings.Join(sched[d], ", ") + " won't be able to make it")
}
}
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: sb.String(),
},
}); err != nil {
log.Error().Err(err).Msg("failed to respond to interaction")
}
}, },
} }

@ -2,7 +2,7 @@
app = "themis" app = "themis"
kill_signal = "SIGINT" kill_signal = "SIGINT"
kill_timeout = 5 kill_timeout = 30
processes = [] processes = []
[env] [env]

@ -1,6 +1,6 @@
module go.wperron.io/themis module go.wperron.io/themis
go 1.19 go 1.21
require ( require (
github.com/bwmarrin/discordgo v0.26.1 github.com/bwmarrin/discordgo v0.26.1

@ -15,6 +15,7 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=

Loading…
Cancel
Save