|
|
@ -17,6 +17,7 @@ import (
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
|
|
|
|
|
|
|
"go.wperron.io/themis"
|
|
|
|
"go.wperron.io/themis"
|
|
|
@ -27,7 +28,8 @@ const (
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
var (
|
|
|
|
dbFile = flag.String("db", "", "SQlite database file path")
|
|
|
|
dbFile = flag.String("db", "", "SQlite database file path.")
|
|
|
|
|
|
|
|
debug = flag.Bool("debug", false, "Set log level to DEBUG.")
|
|
|
|
|
|
|
|
|
|
|
|
store *themis.Store
|
|
|
|
store *themis.Store
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -40,6 +42,21 @@ func main() {
|
|
|
|
|
|
|
|
|
|
|
|
flag.Parse()
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
|
|
|
|
|
|
|
if *debug {
|
|
|
|
|
|
|
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log.Info().Msg("startup.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
|
|
if err := serve(":8080"); err != nil {
|
|
|
|
|
|
|
|
log.Error().Err(err).Msg("failed to serve requests")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
err := touchDbFile(*dbFile)
|
|
|
|
err := touchDbFile(*dbFile)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("failed to touch database file")
|
|
|
|
log.Fatal().Err(err).Msg("failed to touch database file")
|
|
|
@ -185,6 +202,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
handlers := map[string]Handler{
|
|
|
|
handlers := map[string]Handler{
|
|
|
|
"info": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"info": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("info", s, i)
|
|
|
|
uptime, err := themis.Uptime()
|
|
|
|
uptime, err := themis.Uptime()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("failed to get server uptime")
|
|
|
|
log.Error().Err(err).Msg("failed to get server uptime")
|
|
|
@ -224,6 +242,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"list-claims": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"list-claims": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("list-claims", s, i)
|
|
|
|
claims, err := store.ListClaims(ctx)
|
|
|
|
claims, err := store.ListClaims(ctx)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("failed to list claims")
|
|
|
|
log.Error().Err(err).Msg("failed to list claims")
|
|
|
@ -255,6 +274,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"claim": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"claim": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("claim", s, i)
|
|
|
|
if i.Type == discordgo.InteractionApplicationCommandAutocomplete {
|
|
|
|
if i.Type == discordgo.InteractionApplicationCommandAutocomplete {
|
|
|
|
handleClaimAutocomplete(ctx, store, s, i)
|
|
|
|
handleClaimAutocomplete(ctx, store, s, i)
|
|
|
|
return
|
|
|
|
return
|
|
|
@ -344,6 +364,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"describe-claim": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"describe-claim": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("describe-claim", s, i)
|
|
|
|
id := i.ApplicationCommandData().Options[0]
|
|
|
|
id := i.ApplicationCommandData().Options[0]
|
|
|
|
detail, err := store.DescribeClaim(ctx, int(id.IntValue()))
|
|
|
|
detail, err := store.DescribeClaim(ctx, int(id.IntValue()))
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
@ -376,6 +397,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"delete-claim": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"delete-claim": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("delete-claim", s, i)
|
|
|
|
id := i.ApplicationCommandData().Options[0]
|
|
|
|
id := i.ApplicationCommandData().Options[0]
|
|
|
|
userId := i.Member.User.ID
|
|
|
|
userId := i.Member.User.ID
|
|
|
|
err := store.DeleteClaim(ctx, int(id.IntValue()), userId)
|
|
|
|
err := store.DeleteClaim(ctx, int(id.IntValue()), userId)
|
|
|
@ -407,6 +429,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"flush": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"flush": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("flush", s, i)
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
Type: discordgo.InteractionResponseModal,
|
|
|
|
Type: discordgo.InteractionResponseModal,
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
@ -434,6 +457,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"query": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"query": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("query", s, i)
|
|
|
|
roDB, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=private&mode=ro", *dbFile))
|
|
|
|
roDB, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=private&mode=ro", *dbFile))
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("failed to open read-only copy of databse")
|
|
|
|
log.Error().Err(err).Msg("failed to open read-only copy of databse")
|
|
|
@ -468,6 +492,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"schedule": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"schedule": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("schedule", s, i)
|
|
|
|
// get schedule from now to 4 mondays into the future
|
|
|
|
// 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))
|
|
|
|
sched, err := store.GetSchedule(ctx, themis.NextMonday(), themis.NextMonday().Add(4*7*24*time.Hour))
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
@ -512,6 +537,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"absent": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
"absent": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
logCommandInvocation("absent", s, i)
|
|
|
|
var rawDate string
|
|
|
|
var rawDate string
|
|
|
|
if len(i.ApplicationCommandData().Options) == 0 {
|
|
|
|
if len(i.ApplicationCommandData().Options) == 0 {
|
|
|
|
rawDate = themis.NextMonday().Format(time.DateOnly)
|
|
|
|
rawDate = themis.NextMonday().Format(time.DateOnly)
|
|
|
@ -586,8 +612,11 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
defer discord.Close()
|
|
|
|
defer discord.Close()
|
|
|
|
|
|
|
|
|
|
|
|
registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
|
|
|
|
total := len(commands)
|
|
|
|
|
|
|
|
registeredCommands := make([]*discordgo.ApplicationCommand, total)
|
|
|
|
|
|
|
|
log.Debug().Int("total", total).Msg("registering commands with Discord")
|
|
|
|
for i, c := range commands {
|
|
|
|
for i, c := range commands {
|
|
|
|
|
|
|
|
log.Debug().Msg(fmt.Sprintf("registering command %d of %d", i+1, total))
|
|
|
|
command, err := discord.ApplicationCommandCreate(appId, guildId, c)
|
|
|
|
command, err := discord.ApplicationCommandCreate(appId, guildId, c)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("failed to register command")
|
|
|
|
log.Fatal().Err(err).Msg("failed to register command")
|
|
|
@ -597,14 +626,8 @@ func main() {
|
|
|
|
|
|
|
|
|
|
|
|
log.Info().Int("count", len(registeredCommands)).Msg("registered commands")
|
|
|
|
log.Info().Int("count", len(registeredCommands)).Msg("registered commands")
|
|
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
|
|
if err := serve(":8080"); err != nil {
|
|
|
|
|
|
|
|
log.Error().Err(err).Msg("failed to serve requests")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go notifier.NotifyFunc(ctx, func() {
|
|
|
|
go notifier.NotifyFunc(ctx, func() {
|
|
|
|
|
|
|
|
log.Info().Msg("sending weekly reminder")
|
|
|
|
absentees, err := store.GetAbsentees(ctx, themis.NextMonday())
|
|
|
|
absentees, err := store.GetAbsentees(ctx, themis.NextMonday())
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("failed to get absentees for next session")
|
|
|
|
log.Error().Err(err).Msg("failed to get absentees for next session")
|
|
|
@ -632,7 +655,9 @@ func main() {
|
|
|
|
<-ctx.Done()
|
|
|
|
<-ctx.Done()
|
|
|
|
log.Info().Msg("context cancelled, exiting")
|
|
|
|
log.Info().Msg("context cancelled, exiting")
|
|
|
|
|
|
|
|
|
|
|
|
for _, c := range registeredCommands {
|
|
|
|
log.Debug().Int("total", total).Msg("deregistering commands with Discord")
|
|
|
|
|
|
|
|
for i, c := range registeredCommands {
|
|
|
|
|
|
|
|
log.Debug().Msg(fmt.Sprintf("registering command %d of %d", i+1, total))
|
|
|
|
err = discord.ApplicationCommandDelete(appId, guildId, c.ID)
|
|
|
|
err = discord.ApplicationCommandDelete(appId, guildId, c.ID)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("failed to deregister commands")
|
|
|
|
log.Error().Err(err).Msg("failed to deregister commands")
|
|
|
@ -643,6 +668,7 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func touchDbFile(path string) error {
|
|
|
|
func touchDbFile(path string) error {
|
|
|
|
|
|
|
|
log.Debug().Str("path", path).Msg("touching database file")
|
|
|
|
f, err := os.Open(path)
|
|
|
|
f, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
@ -674,6 +700,7 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
|
|
|
|
if strings.HasPrefix(i.ModalSubmitData().CustomID, "modals_flush_") {
|
|
|
|
if strings.HasPrefix(i.ModalSubmitData().CustomID, "modals_flush_") {
|
|
|
|
sub := i.ModalSubmitData().Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
|
|
|
|
sub := i.ModalSubmitData().Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
|
|
|
|
sub = strings.ToLower(sub)
|
|
|
|
sub = strings.ToLower(sub)
|
|
|
|
|
|
|
|
log.Debug().Str("value", sub).Msg("flush modal submitted")
|
|
|
|
if sub == "y" || sub == "ye" || sub == "yes" {
|
|
|
|
if sub == "y" || sub == "ye" || sub == "yes" {
|
|
|
|
err := store.Flush(context.Background(), i.Member.User.ID)
|
|
|
|
err := store.Flush(context.Background(), i.Member.User.ID)
|
|
|
|
msg := "Flushed all claims!"
|
|
|
|
msg := "Flushed all claims!"
|
|
|
@ -741,6 +768,7 @@ func formatClaimsTable(claims []themis.Claim) string {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func handleClaimAutocomplete(ctx context.Context, store *themis.Store, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
func handleClaimAutocomplete(ctx context.Context, store *themis.Store, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
log.Debug().Msg("getting autocomplete data for claim")
|
|
|
|
opts := i.ApplicationCommandData().Options
|
|
|
|
opts := i.ApplicationCommandData().Options
|
|
|
|
claimType, err := themis.ClaimTypeFromString(opts[0].StringValue())
|
|
|
|
claimType, err := themis.ClaimTypeFromString(opts[0].StringValue())
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
@ -762,6 +790,8 @@ func handleClaimAutocomplete(ctx context.Context, store *themis.Store, s *discor
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log.Debug().Int("len", len(choices)).Msg("found autocomplete suggestions")
|
|
|
|
|
|
|
|
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
Type: discordgo.InteractionApplicationCommandAutocompleteResult,
|
|
|
|
Type: discordgo.InteractionApplicationCommandAutocompleteResult,
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
@ -780,6 +810,21 @@ func serve(address string) error {
|
|
|
|
return http.ListenAndServe(address, nil)
|
|
|
|
return http.ListenAndServe(address, nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func logCommandInvocation(name string, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
|
|
|
log.Info().
|
|
|
|
|
|
|
|
Str("userid", i.Member.User.ID).
|
|
|
|
|
|
|
|
Str("username", i.Member.User.Username).
|
|
|
|
|
|
|
|
Str("command", name).
|
|
|
|
|
|
|
|
Str("params", func() string {
|
|
|
|
|
|
|
|
p := make([]string, 0, len(i.ApplicationCommandData().Options))
|
|
|
|
|
|
|
|
for _, o := range i.ApplicationCommandData().Options {
|
|
|
|
|
|
|
|
p = append(p, o.Name+"="+o.StringValue())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings.Join(p, ", ")
|
|
|
|
|
|
|
|
}()).
|
|
|
|
|
|
|
|
Msg("command invoked")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func min(a, b int) int {
|
|
|
|
func min(a, b int) int {
|
|
|
|
if a < b {
|
|
|
|
if a < b {
|
|
|
|
return a
|
|
|
|
return a
|
|
|
|