|
|
|
@ -10,7 +10,6 @@ import (
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"runtime/pprof"
|
|
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
@ -21,19 +20,10 @@ import (
|
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
"go.opentelemetry.io/otel"
|
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
|
|
|
"go.opentelemetry.io/otel/codes"
|
|
|
|
|
"go.opentelemetry.io/otel/propagation"
|
|
|
|
|
"go.opentelemetry.io/otel/sdk/resource"
|
|
|
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
|
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
|
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
|
"go.wperron.io/flydetector"
|
|
|
|
|
"go.wperron.io/sqliteexporter"
|
|
|
|
|
|
|
|
|
|
"go.wperron.io/themis"
|
|
|
|
|
"go.wperron.io/themis/correlation"
|
|
|
|
|
zerologcompat "go.wperron.io/themis/correlation/compat/zerolog"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
@ -43,14 +33,13 @@ const (
|
|
|
|
|
var (
|
|
|
|
|
dbFile = flag.String("db", "", "SQlite database file path.")
|
|
|
|
|
debug = flag.Bool("debug", false, "Set log level to DEBUG.")
|
|
|
|
|
cpuProfile = flag.String("cpuprofile", "", "Output file for pprof profiling.")
|
|
|
|
|
|
|
|
|
|
store *themis.Store
|
|
|
|
|
tracer trace.Tracer
|
|
|
|
|
propagator = propagation.TraceContext{}
|
|
|
|
|
seq = &correlation.CryptoRandSequencer{}
|
|
|
|
|
gen = correlation.NewGenerator(seq)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Handler func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error
|
|
|
|
|
type Handler func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate)
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
log.Info().Msg("startup.")
|
|
|
|
@ -66,20 +55,9 @@ func main() {
|
|
|
|
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
|
|
|
}
|
|
|
|
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
|
|
|
|
|
log.Logger = log.Logger.Hook(correlation.TraceContextHook{})
|
|
|
|
|
log.Logger = log.Logger.Hook(zerologcompat.CorrelationHook{})
|
|
|
|
|
zerolog.DurationFieldUnit = time.Millisecond
|
|
|
|
|
|
|
|
|
|
if *cpuProfile != "" && os.Getenv("ENV") != "production" {
|
|
|
|
|
log.Info().Str("file", *cpuProfile).Msg("starting profiler")
|
|
|
|
|
|
|
|
|
|
f, err := os.Create(*cpuProfile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal().Err(err).Msg("failed to create cpu profile output file")
|
|
|
|
|
}
|
|
|
|
|
_ = pprof.StartCPUProfile(f)
|
|
|
|
|
defer pprof.StopCPUProfile()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
if err := serve(":8080"); err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("failed to serve requests")
|
|
|
|
@ -94,23 +72,13 @@ func main() {
|
|
|
|
|
|
|
|
|
|
connString := fmt.Sprintf(CONN_STRING_PATTERN, *dbFile)
|
|
|
|
|
|
|
|
|
|
log.Debug().Str("connection_string", connString).Msg("opening sqlite3 database")
|
|
|
|
|
db, err := sql.Open("sqlite3", connString)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal().Err(err).Msg("failed to open database")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
store, err = themis.NewStore(db, log.Logger)
|
|
|
|
|
store, err = themis.NewStore(connString)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal().Err(err).Msg("failed to initialize database")
|
|
|
|
|
}
|
|
|
|
|
defer store.Close()
|
|
|
|
|
|
|
|
|
|
if err := initTracing(ctx, db); err != nil {
|
|
|
|
|
log.Fatal().Err(err).Msg("failed to initialize tracing")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
notifChan := make(chan context.Context)
|
|
|
|
|
notifChan := make(chan struct{})
|
|
|
|
|
notifier := themis.NewNotifier(notifChan)
|
|
|
|
|
go notifier.Start(ctx)
|
|
|
|
|
|
|
|
|
@ -247,7 +215,7 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
handlers := map[string]Handler{
|
|
|
|
|
"info": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"info": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
uptime, err := themis.Uptime(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
@ -257,9 +225,10 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to get server uptime: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to get server uptime")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
claimCount, uniquePlayers, err := store.CountClaims(ctx)
|
|
|
|
@ -271,9 +240,10 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to count claims: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to count claims")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ev, err := store.LastOf(ctx, themis.EventFlush)
|
|
|
|
@ -287,9 +257,10 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed get last flush event: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed get last flush event")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
lastFlush = "never"
|
|
|
|
|
} else {
|
|
|
|
@ -303,11 +274,10 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"list-claims": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"list-claims": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
claims, err := store.ListClaims(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
@ -317,9 +287,10 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to list claims: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to list claims")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb := strings.Builder{}
|
|
|
|
@ -335,16 +306,14 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
if i.Type == discordgo.InteractionApplicationCommandAutocomplete {
|
|
|
|
|
log.Debug().Ctx(ctx).Msg("command type interaction autocomplete")
|
|
|
|
|
// TODO(wperron) fix this
|
|
|
|
|
handleClaimAutocomplete(ctx, store, s, i)
|
|
|
|
|
return nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opts := i.ApplicationCommandData().Options
|
|
|
|
@ -352,14 +321,13 @@ func main() {
|
|
|
|
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "`claim-type` and `name` are mandatory parameters",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
claimType, err := themis.ClaimTypeFromString(opts[0].StringValue())
|
|
|
|
@ -367,14 +335,14 @@ func main() {
|
|
|
|
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "You can only take claims of types `area`, `region` or `trade`",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to parse claim: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Str("claim_type", opts[0].StringValue()).Msg("failed to parse claim")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
name := opts[1].StringValue()
|
|
|
|
|
|
|
|
|
@ -387,7 +355,6 @@ func main() {
|
|
|
|
|
|
|
|
|
|
_, err = store.Claim(ctx, userId, player, name, claimType)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// TODO(wperron) fix this error cast
|
|
|
|
|
conflict, ok := err.(themis.ErrConflict)
|
|
|
|
|
if ok {
|
|
|
|
|
sb := strings.Builder{}
|
|
|
|
@ -400,27 +367,26 @@ func main() {
|
|
|
|
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: sb.String(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction")
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "failed to acquire claim :(",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to acquire claim: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to acquire claim")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
@ -430,11 +396,10 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"describe-claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"describe-claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
id := i.ApplicationCommandData().Options[0]
|
|
|
|
|
detail, err := store.DescribeClaim(ctx, int(id.IntValue()))
|
|
|
|
|
if err != nil {
|
|
|
|
@ -445,9 +410,10 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to describe claim: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to describe claim")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb := strings.Builder{}
|
|
|
|
@ -463,16 +429,15 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"delete-claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"delete-claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
id := i.ApplicationCommandData().Options[0]
|
|
|
|
|
userId := i.Member.User.ID
|
|
|
|
|
err := store.DeleteClaim(ctx, int(id.IntValue()), userId)
|
|
|
|
|
if err != nil {
|
|
|
|
|
msg := "Oops, something went wrong :( blame <@wperron>"
|
|
|
|
|
msg := "Oops, something went wrong :( blame @wperron"
|
|
|
|
|
if errors.Is(err, themis.ErrNoSuchClaim) {
|
|
|
|
|
msg = fmt.Sprintf("Claim #%d not found for %s", id.IntValue(), i.Member.Nick)
|
|
|
|
|
}
|
|
|
|
@ -483,33 +448,34 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Errorf("failed to delete claim: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to delete claim")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "Got it chief.",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"flush": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"flush": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
cid := correlation.FromContext(ctx)
|
|
|
|
|
baggage := make(url.Values)
|
|
|
|
|
propagator.Inject(ctx, correlation.UrlValuesCarrier(baggage))
|
|
|
|
|
baggage.Set("correlation_id", cid.String())
|
|
|
|
|
state := baggage.Encode()
|
|
|
|
|
|
|
|
|
|
sb := strings.Builder{}
|
|
|
|
|
sb.WriteString("modal_flush")
|
|
|
|
|
if len(baggage) != 0 {
|
|
|
|
|
if state != "" {
|
|
|
|
|
sb.WriteRune(':')
|
|
|
|
|
sb.WriteString(baggage.Encode())
|
|
|
|
|
sb.WriteString(state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
@ -540,14 +506,14 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"query": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"query": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
roDB, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=private&mode=ro", *dbFile))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to open read-only copy of database: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to open read-only copy of database")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q := i.ApplicationCommandData().Options[0].StringValue()
|
|
|
|
@ -555,12 +521,14 @@ func main() {
|
|
|
|
|
defer cancelDeadline()
|
|
|
|
|
rows, err := roDB.QueryContext(deadlined, q)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("faied to exec user-provided query: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to exec user-provided query")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmtd, err := themis.FormatRows(ctx, rows)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to format rows: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to format rows")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2000 is a magic number here, it's the character limit for a discord
|
|
|
|
@ -575,11 +543,9 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"schedule": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"schedule": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
// 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 {
|
|
|
|
@ -589,9 +555,10 @@ func main() {
|
|
|
|
|
Content: "failed to get schedule, check logs for more info.",
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to get schedule: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to get schedule")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb := strings.Builder{}
|
|
|
|
@ -621,26 +588,21 @@ func main() {
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"send-schedule": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
notifier.Send(ctx)
|
|
|
|
|
"send-schedule": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
notifier.Send()
|
|
|
|
|
|
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "Done.",
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
"absent": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
"absent": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
var rawDate string
|
|
|
|
|
if len(i.ApplicationCommandData().Options) == 0 {
|
|
|
|
|
rawDate = themis.NextMonday().Format(time.DateOnly)
|
|
|
|
@ -653,40 +615,37 @@ func main() {
|
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "failed to parse provided date, make sure to use the YYYY-MM-DD format.",
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if date.Before(time.Now()) {
|
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "The date must be some time in the future.",
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if date.Weekday() != time.Monday {
|
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "The date you provided is not a Monday.",
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
// TODO(wperron) suggest Mondays before and after?
|
|
|
|
|
return nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userId := i.Member.User.ID
|
|
|
|
@ -697,22 +656,20 @@ func main() {
|
|
|
|
|
Content: "something went wrong recording your absence, check logs for more info.",
|
|
|
|
|
},
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to record absence: %w", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
|
|
|
Content: "Okey dokey.",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to respond to interaction: %w", err)
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -731,13 +688,7 @@ func main() {
|
|
|
|
|
}
|
|
|
|
|
log.Info().Int("count", len(created)).Dur("startup_latency_ms", time.Since(start)).Msg("registered commands, ready to operate")
|
|
|
|
|
|
|
|
|
|
go notifier.NotifyFunc(ctx, func(ctx context.Context) {
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
ctx, span := tracer.Start(ctx, "weekly_notification")
|
|
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
|
|
go notifier.NotifyFunc(ctx, func() {
|
|
|
|
|
log.Info().Msg("sending weekly reminder")
|
|
|
|
|
absentees, err := store.GetAbsentees(ctx, themis.NextMonday())
|
|
|
|
|
if err != nil {
|
|
|
|
@ -753,7 +704,7 @@ func main() {
|
|
|
|
|
var msg string
|
|
|
|
|
var components []discordgo.MessageComponent
|
|
|
|
|
if len(absentees) == 0 {
|
|
|
|
|
msg = "Everybody can make it next Monday, see you then! 🎉"
|
|
|
|
|
msg = "Everybody can make it next Monday, see you then!"
|
|
|
|
|
components = []discordgo.MessageComponent{
|
|
|
|
|
discordgo.ActionsRow{
|
|
|
|
|
Components: []discordgo.MessageComponent{
|
|
|
|
@ -761,16 +712,12 @@ func main() {
|
|
|
|
|
CustomID: "schedule-response",
|
|
|
|
|
Label: "I Can't Make It",
|
|
|
|
|
Style: discordgo.DangerButton,
|
|
|
|
|
Disabled: false,
|
|
|
|
|
Emoji: discordgo.ComponentEmoji{
|
|
|
|
|
Name: "🙁",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
msg = fmt.Sprintf("%s can't make it next Monday. 🙁", themis.FormatStringSlice(absentees))
|
|
|
|
|
msg = fmt.Sprintf("%s can't make it next Monday. :sad:", themis.FormatStringSlice(absentees))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = discord.ChannelMessageSendComplex(channelId, &discordgo.MessageSend{
|
|
|
|
@ -784,7 +731,7 @@ func main() {
|
|
|
|
|
|
|
|
|
|
<-ctx.Done()
|
|
|
|
|
log.Info().Msg("context cancelled, exiting")
|
|
|
|
|
store.Close()
|
|
|
|
|
os.Exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func touchDbFile(path string) error {
|
|
|
|
@ -815,9 +762,10 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
|
|
|
|
|
case discordgo.InteractionApplicationCommand:
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
ctx = context.WithValue(ctx, "correlation_id", gen.Next())
|
|
|
|
|
|
|
|
|
|
if h, ok := handlers[i.ApplicationCommandData().Name]; ok {
|
|
|
|
|
_ = inSpan(i.ApplicationCommandData().Name, withLogging(i.ApplicationCommandData().Name, h))(ctx, s, i)
|
|
|
|
|
withLogging(i.ApplicationCommandData().Name, h)(ctx, s, i)
|
|
|
|
|
}
|
|
|
|
|
case discordgo.InteractionModalSubmit:
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
@ -825,11 +773,16 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
|
|
|
|
|
|
|
|
|
|
state, err := parseCustomIDState(i.ModalSubmitData().CustomID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("unexpected error occurred while parsing state from custom id, returning early.")
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("unexpected error occured while parsing state from custom id, returning early.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx = propagator.Extract(ctx, correlation.UrlValuesCarrier(state))
|
|
|
|
|
cid := state.Get("correlation_id")
|
|
|
|
|
if cid != "" {
|
|
|
|
|
ctx = context.WithValue(ctx, "correlation_id", cid)
|
|
|
|
|
} else {
|
|
|
|
|
ctx = context.WithValue(ctx, "correlation_id", gen.Next())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.HasPrefix(i.ModalSubmitData().CustomID, "modal_flush") {
|
|
|
|
|
sub := i.ModalSubmitData().Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
|
|
|
|
@ -837,7 +790,7 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
|
|
|
|
|
log.Debug().Ctx(ctx).Str("value", sub).Msg("flush modal submitted")
|
|
|
|
|
|
|
|
|
|
if sub == "y" || sub == "ye" || sub == "yes" {
|
|
|
|
|
err := store.Flush(ctx, i.Member.User.ID)
|
|
|
|
|
err := store.Flush(context.Background(), i.Member.User.ID)
|
|
|
|
|
msg := "Flushed all claims!"
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Ctx(ctx).Err(err).Msg("failed to flush claims")
|
|
|
|
@ -877,14 +830,19 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx = propagator.Extract(ctx, correlation.UrlValuesCarrier(state))
|
|
|
|
|
cid := state.Get("correlation_id")
|
|
|
|
|
if cid != "" {
|
|
|
|
|
ctx = context.WithValue(ctx, "correlation_id", cid)
|
|
|
|
|
} else {
|
|
|
|
|
ctx = context.WithValue(ctx, "correlation_id", gen.Next())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch i.MessageComponentData().CustomID {
|
|
|
|
|
case "schedule-response":
|
|
|
|
|
userId := i.Member.User.ID
|
|
|
|
|
log.Info().Ctx(ctx).Str("message_component", "schedule-response").Str("userid", userId).Msg("handling message component interaction")
|
|
|
|
|
|
|
|
|
|
if err := store.AddAbsence(ctx, themis.NextMonday(), userId); err != nil {
|
|
|
|
|
if err := store.AddAbsence(context.TODO(), themis.NextMonday(), userId); err != nil {
|
|
|
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
|
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
|
|
|
Data: &discordgo.InteractionResponseData{
|
|
|
|
@ -984,57 +942,55 @@ func serve(address string) error {
|
|
|
|
|
return http.ListenAndServe(address, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func inSpan(name string, h Handler) Handler {
|
|
|
|
|
return func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
ctx, span := tracer.Start(ctx, fmt.Sprintf("discord_command %s", name))
|
|
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
|
|
return h(ctx, s, i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func withLogging(name string, h Handler) Handler {
|
|
|
|
|
return func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
|
|
|
|
return func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
start := time.Now()
|
|
|
|
|
logCommandInvocation(ctx, name, s, i)
|
|
|
|
|
err := h(ctx, s, i)
|
|
|
|
|
debugCommandCompletion(ctx, name, time.Since(start), err, s, i)
|
|
|
|
|
return nil
|
|
|
|
|
h(ctx, s, i)
|
|
|
|
|
debugCommandCompletion(ctx, name, time.Since(start), s, i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func logCommandInvocation(ctx context.Context, name string, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
span := trace.SpanFromContext(ctx)
|
|
|
|
|
span.SetAttributes(
|
|
|
|
|
attribute.String("user_id", i.Member.User.ID),
|
|
|
|
|
attribute.String("username", i.Member.User.Username),
|
|
|
|
|
attribute.String("command_name", name),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
log.Info().
|
|
|
|
|
Ctx(ctx).
|
|
|
|
|
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 {
|
|
|
|
|
sb := strings.Builder{}
|
|
|
|
|
sb.WriteString(o.Name)
|
|
|
|
|
sb.WriteRune('=')
|
|
|
|
|
|
|
|
|
|
switch o.Type {
|
|
|
|
|
case discordgo.ApplicationCommandOptionSubCommand, discordgo.ApplicationCommandOptionSubCommandGroup:
|
|
|
|
|
panic("unreachable")
|
|
|
|
|
case discordgo.ApplicationCommandOptionString:
|
|
|
|
|
span.SetAttributes(attribute.String(fmt.Sprintf("command_option.%s", o.Name), o.StringValue()))
|
|
|
|
|
sb.WriteString(o.StringValue())
|
|
|
|
|
case discordgo.ApplicationCommandOptionInteger:
|
|
|
|
|
span.SetAttributes(attribute.Int64(fmt.Sprintf("command_option.%s", o.Name), o.IntValue()))
|
|
|
|
|
sb.WriteString(fmt.Sprint(o.IntValue()))
|
|
|
|
|
case discordgo.ApplicationCommandOptionBoolean:
|
|
|
|
|
span.SetAttributes(attribute.Bool(fmt.Sprintf("command_option.%s", o.Name), o.BoolValue()))
|
|
|
|
|
sb.WriteString(fmt.Sprint(o.BoolValue()))
|
|
|
|
|
default:
|
|
|
|
|
span.SetAttributes(attribute.String(fmt.Sprintf("command_option.%s", o.Name), "unsupported_type"))
|
|
|
|
|
sb.WriteString("[unsupported type]")
|
|
|
|
|
}
|
|
|
|
|
p = append(p, sb.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Info().Ctx(ctx).Msg("command invoked")
|
|
|
|
|
return strings.Join(p, ", ")
|
|
|
|
|
}()).
|
|
|
|
|
Msg("command invoked")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func debugCommandCompletion(ctx context.Context, name string, dur time.Duration, err error, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
span := trace.SpanFromContext(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
span.SetStatus(codes.Error, err.Error())
|
|
|
|
|
}
|
|
|
|
|
log.Info().Ctx(ctx).Dur("latency_ms", dur).Msg("command completed")
|
|
|
|
|
func debugCommandCompletion(ctx context.Context, name string, dur time.Duration, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
|
|
|
log.Info().
|
|
|
|
|
Ctx(ctx).
|
|
|
|
|
Str("userid", i.Member.User.ID).
|
|
|
|
|
Str("username", i.Member.User.Username).
|
|
|
|
|
Str("command", name).
|
|
|
|
|
Dur("latency_ms", dur).
|
|
|
|
|
Msg("command completed")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func min(a, b int) int {
|
|
|
|
@ -1056,36 +1012,3 @@ func parseCustomIDState(qs string) (url.Values, error) {
|
|
|
|
|
}
|
|
|
|
|
return v, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func initTracing(ctx context.Context, db *sql.DB) error {
|
|
|
|
|
fd, _ := flydetector.NewDetector().Detect(ctx) // this can't error
|
|
|
|
|
rd, err := resource.New(ctx,
|
|
|
|
|
resource.WithHost(),
|
|
|
|
|
resource.WithOS(),
|
|
|
|
|
resource.WithProcess(),
|
|
|
|
|
resource.WithTelemetrySDK(),
|
|
|
|
|
resource.WithAttributes(semconv.ServiceName("themis")),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create resource: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res, err := resource.Merge(fd, rd)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to merge resources: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ex, err := sqliteexporter.NewSqliteSDKTraceExporterWithDB(db)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create span exporter: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tp := sdktrace.NewTracerProvider(
|
|
|
|
|
sdktrace.WithBatcher(ex, sdktrace.WithExportTimeout(time.Second)),
|
|
|
|
|
sdktrace.WithResource(res),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
otel.SetTracerProvider(tp)
|
|
|
|
|
tracer = tp.Tracer("themis")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|