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