Compare commits

..

No commits in common. 'main' and 'absences' have entirely different histories.

@ -6,7 +6,7 @@ jobs:
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: 1.21 go-version: 1.19
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: "fmt" - name: "fmt"
run: test -z $(go fmt ./...) run: test -z $(go fmt ./...)

@ -3,13 +3,13 @@ WORKDIR /app
COPY . . COPY . .
RUN mkdir ./bin; go build -buildvcs=false -o ./bin ./cmd/... RUN mkdir ./bin; go build -buildvcs=false -o ./bin ./cmd/...
FROM ubuntu:23.10 as litestream FROM ubuntu:22.04 as litestream
WORKDIR /download WORKDIR /download
RUN apt update -y && apt install -y wget tar RUN apt update -y && apt install -y wget tar
RUN wget https://github.com/benbjohnson/litestream/releases/download/v0.3.9/litestream-v0.3.9-linux-amd64.tar.gz; \ RUN wget https://github.com/benbjohnson/litestream/releases/download/v0.3.9/litestream-v0.3.9-linux-amd64.tar.gz; \
tar -zxf litestream-v0.3.9-linux-amd64.tar.gz; tar -zxf litestream-v0.3.9-linux-amd64.tar.gz;
FROM ubuntu:23.10 FROM ubuntu:22.04
WORKDIR /themis WORKDIR /themis
COPY --from=builder /app/bin/themis-server /usr/local/bin/themis-server COPY --from=builder /app/bin/themis-server /usr/local/bin/themis-server
COPY --from=litestream /download/litestream /usr/local/bin/litestream COPY --from=litestream /download/litestream /usr/local/bin/litestream

@ -7,7 +7,7 @@ Discord App to allow EU4 players to take claims on regions and provinces.
### Requirements ### Requirements
To develop: To develop:
- [Go](https://go.dev/) version 1.21 or higher installed locally - [Go](https://go.dev/) version 1.19 or higher installed locally
- `sqlite3` installed locally (already ships by default on most OSes) - `sqlite3` installed locally (already ships by default on most OSes)
To deploy: To deploy:

@ -4,54 +4,26 @@ import (
"context" "context"
"fmt" "fmt"
"time" "time"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
) )
func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string) error { func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string) error {
ctx, span := tracer.Start(ctx, "add_absence", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("absences"),
semconv.DBOperation("insert"),
attribute.String("user_id", userId),
attribute.String("session_date", session.Format(time.DateOnly)),
))
defer span.End()
if session.Weekday() != time.Monday { if session.Weekday() != time.Monday {
log.Debug().Ctx(ctx).Msg(fmt.Sprintf("%s is not a monday", session))
span.RecordError(fmt.Errorf("%s is not a monday", session))
return fmt.Errorf("not a monday") return fmt.Errorf("not a monday")
} }
defer s.Audit(ctx, &AuditableEvent{
userId: userId,
eventType: EventAbsence,
})
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to begin transaction")
return fmt.Errorf("failed to begin transaction: %w", err) return fmt.Errorf("failed to begin transaction: %w", err)
} }
defer tx.Commit() //nolint:errcheck defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, "INSERT INTO absences (session_date, userid) VALUES (?, ?)") stmt, err := s.db.PrepareContext(ctx, "INSERT INTO absences (session_date, userid) VALUES (?, ?)")
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare absence query")
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(time.DateOnly), userId) _, err = stmt.ExecContext(ctx, session.Format(time.DateOnly), userId)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to insert absence")
return fmt.Errorf("failed to insert absence: %w", err) return fmt.Errorf("failed to insert absence: %w", err)
} }
@ -59,34 +31,19 @@ func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string
} }
func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string, error) { func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string, error) {
ctx, span := tracer.Start(ctx, "get_absentees", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("absences"),
semconv.DBOperation("select"),
attribute.String("session_date", session.Format(time.DateOnly)),
))
defer span.End()
log.Debug().Ctx(ctx).Time("session", session).Msg("getting list of absentees")
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to begin transaction")
return nil, fmt.Errorf("failed to begin transaction: %w", err) return nil, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer tx.Commit() //nolint:errcheck defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, `SELECT userid FROM absences WHERE session_date = ?`) stmt, err := s.db.PrepareContext(ctx, `SELECT userid FROM absences WHERE session_date = ?`)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
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(time.DateOnly)) rows, err := stmt.QueryContext(ctx, session.Format(time.DateOnly))
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to execute query: %w", err) return nil, fmt.Errorf("failed to execute query: %w", err)
} }
@ -108,38 +65,22 @@ func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string,
type Schedule map[string][]string type Schedule map[string][]string
func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) (Schedule, error) { func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) (Schedule, error) {
ctx, span := tracer.Start(ctx, "get_schedule", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("absences"),
semconv.DBOperation("select"),
attribute.String("from", from.Format(time.DateOnly)),
attribute.String("to", to.Format(time.DateOnly)),
))
defer span.End()
log.Debug().Ctx(ctx).Time("from", from).Time("to", to).Msg("getting next sessions schedule")
schedule := make(Schedule) schedule := make(Schedule)
initSchedule(schedule, from, to) initSchedule(schedule, from, to)
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to begin transaction")
return nil, fmt.Errorf("failed to begin transaction: %w", err) return nil, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer tx.Commit() //nolint:errcheck defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, `SELECT session_date, userid FROM absences WHERE session_date BETWEEN ? AND ? ORDER BY session_date ASC`) stmt, err := s.db.PrepareContext(ctx, `SELECT session_date, userid FROM absences WHERE session_date BETWEEN ? AND ? ORDER BY session_date ASC`)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
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(time.DateOnly), to.Format(time.DateOnly)) rows, err := stmt.QueryContext(ctx, from.Format(time.DateOnly), to.Format(time.DateOnly))
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to execute query: %w", err) return nil, fmt.Errorf("failed to execute query: %w", err)
} }
@ -148,12 +89,14 @@ func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) (Schedule,
var user string var user string
err = rows.Scan(&date, &user) err = rows.Scan(&date, &user)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return nil, fmt.Errorf("failed to scan row: %w", err) return nil, fmt.Errorf("failed to scan row: %w", err)
} }
if _, ok := schedule[date]; ok {
schedule[date] = append(schedule[date], user) schedule[date] = append(schedule[date], user)
} else {
schedule[date] = []string{user}
}
} }
return schedule, nil return schedule, nil

@ -2,20 +2,16 @@ package themis
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"testing" "testing"
"time" "time"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestAddAbsence(t *testing.T) { func TestAddAbsence(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestAddAbsence")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestAddAbsence"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
require.NoError(t, err) require.NoError(t, err)
now := NextMonday() now := NextMonday()
@ -29,15 +25,10 @@ func TestAddAbsence(t *testing.T) {
absentees, err = store.GetAbsentees(context.TODO(), now) absentees, err = store.GetAbsentees(context.TODO(), now)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, len(absentees)) assert.Equal(t, 1, len(absentees))
_, err = store.LastOf(context.TODO(), EventAbsence)
require.NoError(t, err)
} }
func TestGetSchedule(t *testing.T) { func TestGetSchedule(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestGetSchedule")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestGetSchedule"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
require.NoError(t, err) require.NoError(t, err)
now := NextMonday() now := NextMonday()

@ -1,148 +0,0 @@
package themis
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
)
type EventType int
const (
EventFlush EventType = iota
EventClaim
EventUnclaim
EventAbsence
)
func (et EventType) String() string {
switch et {
case EventFlush:
return "FLUSH"
case EventClaim:
return "CLAIM"
case EventUnclaim:
return "UNCLAIM"
case EventAbsence:
return "ABSENT"
default:
return ""
}
}
func EventTypeFromString(ev string) (EventType, error) {
switch ev {
case "FLUSH":
return EventFlush, nil
case "CLAIM":
return EventClaim, nil
case "UNCLAIM":
return EventUnclaim, nil
case "ABSENT":
return EventAbsence, nil
default:
return EventType(9999), fmt.Errorf("no such event type: %s", ev)
}
}
type AuditableEvent struct {
userId string
eventType EventType
err error
}
// Audit writes to the audit table, returns nothing because it is meant to be
// used in a defered statement on functions that write to the database.
func (s *Store) Audit(ctx context.Context, ev *AuditableEvent) {
ctx, span := tracer.Start(ctx, "audit", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("audit_log"),
semconv.DBOperation("insert"),
attribute.String("user_id", ev.userId),
attribute.Stringer("event_type", ev.eventType),
))
defer span.End()
if ev.err == nil {
log.Debug().Ctx(ctx).Str("event_type", ev.eventType.String()).Str("userid", ev.userId).Msg("recording audit log")
ctx := context.Background()
tx, err := s.db.Begin()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to start transaction")
log.Error().Ctx(ctx).Err(err).Msg("failed to start transaction")
}
defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, "INSERT INTO audit_log (userid, event_type, ts) VALUES (?, ?, ?)")
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare audit log insert")
log.Error().Ctx(ctx).Err(err).Msg("failed to prepare audit log insert")
}
if _, err := stmt.ExecContext(ctx, ev.userId, ev.eventType.String(), time.Now()); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to insert audit log")
log.Error().Ctx(ctx).Err(err).Msg("failed to insert audit log")
}
}
}
type AuditEvent struct {
Id int
UserId string
EventType EventType
Timestamp time.Time
}
func (s *Store) LastOf(ctx context.Context, t EventType) (AuditEvent, error) {
ctx, span := tracer.Start(ctx, "find_last_audit_log", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("audit_log"),
semconv.DBOperation("select"),
attribute.Stringer("event_type", t),
))
defer span.End()
log.Debug().Ctx(ctx).Str("event_type", t.String()).Msg("finding last audit log")
stmt, err := s.db.PrepareContext(ctx, `SELECT id, userid, event_type, ts FROM audit_log WHERE event_type = ? ORDER BY ts DESC LIMIT 1`)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to get last event")
return AuditEvent{}, fmt.Errorf("failed to get last event of type %s: %w", t.String(), err)
}
row := stmt.QueryRowContext(ctx, t.String())
ev := AuditEvent{}
var rawEventType string
err = row.Scan(&ev.Id, &ev.UserId, &rawEventType, &ev.Timestamp)
if err == sql.ErrNoRows {
span.RecordError(ErrNever)
return AuditEvent{}, ErrNever
}
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return AuditEvent{}, fmt.Errorf("failed to scan row: %w", err)
}
ev.EventType, err = EventTypeFromString(rawEventType)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to parse event type")
return AuditEvent{}, fmt.Errorf("failed to parse event type: %w", err)
}
return ev, nil
}

@ -34,6 +34,12 @@ const (
CLAIM_TYPE_TRADE = "trade" CLAIM_TYPE_TRADE = "trade"
) )
var claimTypeToColumn = map[ClaimType]string{
CLAIM_TYPE_AREA: "area",
CLAIM_TYPE_REGION: "region",
CLAIM_TYPE_TRADE: "trade_node",
}
type Claim struct { type Claim struct {
ID int ID int
Player string Player string

@ -7,10 +7,8 @@ import (
"flag" "flag"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"os/signal" "os/signal"
"runtime/pprof"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -19,21 +17,9 @@ 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.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"
) )
const ( const (
@ -41,52 +27,19 @@ 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.")
cpuProfile = flag.String("cpuprofile", "", "Output file for pprof profiling.")
store *themis.Store store *themis.Store
tracer trace.Tracer
propagator = propagation.TraceContext{}
) )
type Handler func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error type Handler func(s *discordgo.Session, i *discordgo.InteractionCreate)
func main() { func main() {
log.Info().Msg("startup.")
start := time.Now()
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGKILL, syscall.SIGINT) ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGKILL, syscall.SIGINT)
defer cancel() defer cancel()
flag.Parse() flag.Parse()
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if *debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
log.Logger = log.Logger.Hook(correlation.TraceContextHook{})
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")
}
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")
@ -94,23 +47,13 @@ func main() {
connString := fmt.Sprintf(CONN_STRING_PATTERN, *dbFile) connString := fmt.Sprintf(CONN_STRING_PATTERN, *dbFile)
log.Debug().Str("connection_string", connString).Msg("opening sqlite3 database") store, err = themis.NewStore(connString)
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()
if err := initTracing(ctx, db); err != nil { notifChan := make(chan struct{})
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)
@ -226,12 +169,6 @@ func main() {
Description: "Get the schedule for the following weeks.", Description: "Get the schedule for the following weeks.",
Type: discordgo.ChatApplicationCommand, Type: discordgo.ChatApplicationCommand,
}, },
{
Name: "send-schedule",
Description: "Trigger the scheduled message. Admins only",
Type: discordgo.ChatApplicationCommand,
DefaultMemberPermissions: new(int64), // default 0 == admins only
},
{ {
Name: "absent", Name: "absent",
Description: "Mark yourself as absent for a session", Description: "Mark yourself as absent for a session",
@ -247,9 +184,10 @@ func main() {
}, },
} }
handlers := map[string]Handler{ handlers := map[string]Handler{
"info": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error { "info": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
uptime, err := themis.Uptime(ctx) uptime, err := themis.Uptime()
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to get server uptime")
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{
@ -257,13 +195,13 @@ func main() {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return fmt.Errorf("failed to get server uptime: %w", err)
} }
claimCount, uniquePlayers, err := store.CountClaims(ctx) claimCount, uniquePlayers, err := store.CountClaims(ctx)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to count claims")
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{
@ -271,45 +209,24 @@ func main() {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to count claims: %w", err)
}
ev, err := store.LastOf(ctx, themis.EventFlush)
var lastFlush string
if err != nil {
if err != themis.ErrNever {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Oops, something went wrong! :(",
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
}
return fmt.Errorf("failed get last flush event: %w", err)
} }
lastFlush = "never"
} else {
lastFlush = ev.Timestamp.Format(time.DateTime)
} }
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{
Content: fmt.Sprintf("Server has been up for %s, has %d claims from %d unique players.\nThe last time claims were flushed was: %s.", uptime, claimCount, uniquePlayers, lastFlush), Content: fmt.Sprintf("Server has been up for %s, has %d claims from %d unique players", uptime, claimCount, uniquePlayers),
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().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(s *discordgo.Session, i *discordgo.InteractionCreate) {
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")
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{
@ -317,9 +234,8 @@ func main() {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return fmt.Errorf("failed to list claims: %w", err)
} }
sb := strings.Builder{} sb := strings.Builder{}
@ -335,16 +251,13 @@ func main() {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return nil
}, },
"claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error { "claim": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type == discordgo.InteractionApplicationCommandAutocomplete { if i.Type == discordgo.InteractionApplicationCommandAutocomplete {
log.Debug().Ctx(ctx).Msg("command type interaction autocomplete")
// TODO(wperron) fix this
handleClaimAutocomplete(ctx, store, s, i) handleClaimAutocomplete(ctx, store, s, i)
return nil return
} }
opts := i.ApplicationCommandData().Options opts := i.ApplicationCommandData().Options
@ -352,29 +265,28 @@ 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 {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return nil return
} }
claimType, err := themis.ClaimTypeFromString(opts[0].StringValue()) claimType, err := themis.ClaimTypeFromString(opts[0].StringValue())
if err != nil { if err != nil {
log.Error().Err(err).Str("claim_type", opts[0].StringValue()).Msg("failed to parse claim")
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 {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return fmt.Errorf("failed to parse claim: %w", err) return
} }
name := opts[1].StringValue() name := opts[1].StringValue()
@ -387,7 +299,6 @@ 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{}
@ -400,27 +311,26 @@ 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 {
return fmt.Errorf("failed to respond to interaction") log.Error().Err(err).Msg("failed to respond to interaction")
} }
return nil return
} }
log.Error().Err(err).Msg("failed to acquire claim")
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 {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return fmt.Errorf("failed to acquire claim: %w", err) return
} }
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
@ -430,14 +340,14 @@ func main() {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().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(s *discordgo.Session, i *discordgo.InteractionCreate) {
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 {
log.Error().Err(err).Msg("failed to describe claim")
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{
@ -445,9 +355,8 @@ func main() {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return fmt.Errorf("failed to describe claim: %w", err)
} }
sb := strings.Builder{} sb := strings.Builder{}
@ -463,19 +372,19 @@ func main() {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().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(s *discordgo.Session, i *discordgo.InteractionCreate) {
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)
} }
log.Error().Err(err).Msg("failed to delete claim")
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{
@ -483,48 +392,29 @@ func main() {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return fmt.Errorf("failed to delete claim: %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: "Got it chief.", Content: "Got it chief.",
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return nil
}, },
"flush": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error { "flush": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
baggage := make(url.Values)
propagator.Inject(ctx, correlation.UrlValuesCarrier(baggage))
sb := strings.Builder{}
sb.WriteString("modal_flush")
if len(baggage) != 0 {
sb.WriteRune(':')
sb.WriteString(baggage.Encode())
}
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{
CustomID: sb.String(), CustomID: "modals_flush_" + i.Interaction.Member.User.ID,
Title: "Are you sure?", Title: "Are you sure?",
Components: []discordgo.MessageComponent{ Components: []discordgo.MessageComponent{
discordgo.ActionsRow{ discordgo.ActionsRow{
Components: []discordgo.MessageComponent{ Components: []discordgo.MessageComponent{
// Note: Currently, Discord *requires* modal have at least one field present, it's not
// possible to simply have a cancel/confirm box. That is why this modal uses an old
// school [y/N] confirmation.
//
// source: https://stackoverflow.com/questions/71874810/discord-modals-without-textinputcomponent-but-a-text-and-button-only#answer-74317739
discordgo.TextInput{ discordgo.TextInput{
CustomID: "confirmation", CustomID: "confirmation",
Label: "Delete all claims permanently? [y/N]", Label: "Delete all claims permanently? [y/N]",
@ -540,14 +430,13 @@ func main() {
}, },
}, },
}); err != nil { }); err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return nil
}, },
"query": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error { "query": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
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 {
return fmt.Errorf("failed to open read-only copy of database: %w", err) log.Error().Err(err).Msg("failed to open read-only copy of databse")
} }
q := i.ApplicationCommandData().Options[0].StringValue() q := i.ApplicationCommandData().Options[0].StringValue()
@ -555,12 +444,13 @@ func main() {
defer cancelDeadline() defer cancelDeadline()
rows, err := roDB.QueryContext(deadlined, q) rows, err := roDB.QueryContext(deadlined, q)
if err != nil { if err != nil {
return fmt.Errorf("faied to exec user-provided query: %w", err) log.Error().Err(err).Msg("failed to exec user-provided query")
return
} }
fmtd, err := themis.FormatRows(ctx, rows) fmtd, err := themis.FormatRows(rows)
if err != nil { if err != nil {
return fmt.Errorf("failed to format rows: %w", err) log.Error().Err(err).Msg("failed to format rows")
} }
// 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
@ -574,24 +464,22 @@ func main() {
Content: table, Content: table,
}, },
}); err != nil { }); err != nil {
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction") log.Error().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(s *discordgo.Session, i *discordgo.InteractionCreate) {
// 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 {
log.Error().Err(err).Msg("failed to get schedule")
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{
Content: "failed to get schedule, check logs for more info.", Content: "failed to get schedule, check logs for more info.",
}, },
}); err != nil { }); err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return fmt.Errorf("failed to get schedule: %w", err)
} }
sb := strings.Builder{} sb := strings.Builder{}
@ -620,27 +508,10 @@ func main() {
Content: sb.String(), Content: sb.String(),
}, },
}); err != nil { }); err != nil {
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction") log.Error().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)
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(s *discordgo.Session, i *discordgo.InteractionCreate) {
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)
@ -653,40 +524,34 @@ 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 {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
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 {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
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 {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
// TODO(wperron) suggest Mondays before and after? // TODO(wperron) suggest Mondays before and after?
return nil
} }
userId := i.Member.User.ID userId := i.Member.User.ID
@ -697,22 +562,19 @@ 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 {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
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 {
return fmt.Errorf("failed to respond to interaction: %w", err) log.Error().Err(err).Msg("failed to respond to interaction")
} }
return nil
}, },
} }
@ -724,21 +586,25 @@ func main() {
} }
defer discord.Close() defer discord.Close()
log.Debug().Int("count", len(commands)).Msg("registering commands via bulk overwrite") registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
created, err := discord.ApplicationCommandBulkOverwrite(appId, guildId, commands) for i, c := range commands {
command, err := discord.ApplicationCommandCreate(appId, guildId, c)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed to register commands with discord") log.Fatal().Err(err).Msg("failed to register command")
}
registeredCommands[i] = command
} }
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) { log.Info().Int("count", len(registeredCommands)).Msg("registered commands")
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
ctx, span := tracer.Start(ctx, "weekly_notification") go func() {
defer span.End() if err := serve(":8080"); err != nil {
log.Error().Err(err).Msg("failed to serve requests")
}
cancel()
}()
log.Info().Msg("sending weekly reminder") go notifier.NotifyFunc(ctx, func() {
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")
@ -751,32 +617,13 @@ func main() {
} }
var msg string var msg string
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{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.Button{
CustomID: "schedule-response",
Label: "I Can't Make It",
Style: discordgo.DangerButton,
Disabled: false,
Emoji: discordgo.ComponentEmoji{
Name: "🙁",
},
},
},
},
}
} else { } 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{ _, err = discord.ChannelMessageSend(channelId, msg)
Content: msg,
Components: components,
})
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to send scheduled notification") log.Error().Err(err).Msg("failed to send scheduled notification")
} }
@ -784,11 +631,18 @@ func main() {
<-ctx.Done() <-ctx.Done()
log.Info().Msg("context cancelled, exiting") log.Info().Msg("context cancelled, exiting")
store.Close()
for _, c := range registeredCommands {
err = discord.ApplicationCommandDelete(appId, guildId, c.ID)
if err != nil {
log.Error().Err(err).Msg("failed to deregister commands")
}
}
log.Info().Msg("deregistered commands, exiting")
os.Exit(0)
} }
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) {
@ -813,34 +667,18 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
sess.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { sess.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
switch i.Type { switch i.Type {
case discordgo.InteractionApplicationCommand: case discordgo.InteractionApplicationCommand:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if h, ok := handlers[i.ApplicationCommandData().Name]; ok { if h, ok := handlers[i.ApplicationCommandData().Name]; ok {
_ = inSpan(i.ApplicationCommandData().Name, withLogging(i.ApplicationCommandData().Name, h))(ctx, s, i) h(s, i)
} }
case discordgo.InteractionModalSubmit: case discordgo.InteractionModalSubmit:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) if strings.HasPrefix(i.ModalSubmitData().CustomID, "modals_flush_") {
defer cancel()
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.")
return
}
ctx = propagator.Extract(ctx, correlation.UrlValuesCarrier(state))
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
sub = strings.ToLower(sub) sub = strings.ToLower(sub)
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(ctx, i.Member.User.ID) err := store.Flush(context.Background())
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().Err(err).Msg("failed to flush claims")
msg = "failed to flush claims from database" msg = "failed to flush claims from database"
} }
@ -851,7 +689,7 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
}, },
}) })
if err != nil { if err != nil {
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction") log.Error().Err(err).Msg("failed to respond to interaction")
} }
return return
} }
@ -863,49 +701,10 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
}, },
}) })
if err != nil { if err != nil {
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return
}
case discordgo.InteractionMessageComponent:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
state, err := parseCustomIDState(i.MessageComponentData().CustomID)
if err != nil {
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))
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 := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "something went wrong recording your absence, check logs for more info.",
},
}); err != nil {
log.Error().Err(err).Msg("failed to respond to interaction") log.Error().Err(err).Msg("failed to respond to interaction")
} }
return return
} }
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Looks like <@%s> can't make it after all.", userId),
},
})
if err != nil {
log.Error().Err(err).Msg("failed to respond to interaction")
}
}
} }
}) })
} }
@ -942,7 +741,6 @@ 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 {
@ -964,8 +762,6 @@ 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{
@ -984,108 +780,9 @@ 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 {
return func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
start := time.Now()
logCommandInvocation(ctx, name, s, i)
err := h(ctx, 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) {
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),
)
for _, o := range i.ApplicationCommandData().Options {
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()))
case discordgo.ApplicationCommandOptionInteger:
span.SetAttributes(attribute.Int64(fmt.Sprintf("command_option.%s", o.Name), o.IntValue()))
case discordgo.ApplicationCommandOptionBoolean:
span.SetAttributes(attribute.Bool(fmt.Sprintf("command_option.%s", o.Name), o.BoolValue()))
default:
span.SetAttributes(attribute.String(fmt.Sprintf("command_option.%s", o.Name), "unsupported_type"))
}
}
log.Info().Ctx(ctx).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 min(a, b int) int { func min(a, b int) int {
if a < b { if a < b {
return a return a
} }
return b return b
} }
func parseCustomIDState(qs string) (url.Values, error) {
parts := strings.Split(qs, ":")
if len(parts) == 1 {
return make(url.Values), nil
}
v, err := url.ParseQuery(parts[1])
if err != nil {
return nil, err
}
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
}

@ -3,12 +3,6 @@ package themis
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
) )
type Conflict struct { type Conflict struct {
@ -23,47 +17,36 @@ func (c Conflict) String() string {
return fmt.Sprintf("%s owned by #%d %s %s (%s)", c.Province, c.ClaimID, c.ClaimType, c.Claim, c.Player) return fmt.Sprintf("%s owned by #%d %s %s (%s)", c.Province, c.ClaimID, c.ClaimType, c.Claim, c.Player)
} }
const conflictQuery string = `WITH claiming AS ( const conflictQuery string = `SELECT name, player, claim_type, val, id FROM (
SELECT province FROM claimables SELECT provinces.name, claims.player, claims.claim_type, claims.val, claims.id
WHERE claimables.typ = ? FROM claims
AND claimables.name = ? LEFT JOIN provinces ON claims.val = provinces.trade_node
) WHERE claims.claim_type = 'trade' AND claims.userid IS NOT ?
SELECT claimables.province, claims.player, claims.claim_type, claims.val, claims.id AND provinces.%[1]s = ?
UNION
SELECT provinces.name, claims.player, claims.claim_type, claims.val, claims.id
FROM claims FROM claims
INNER JOIN claimables LEFT JOIN provinces ON claims.val = provinces.region
ON claims.claim_type = claimables.typ WHERE claims.claim_type = 'region' AND claims.userid IS NOT ?
AND claims.val = claimables.name AND provinces.%[1]s = ?
INNER JOIN claiming UNION
ON claiming.province = claimables.province SELECT provinces.name, claims.player, claims.claim_type, claims.val, claims.id
WHERE claims.userid IS NOT ?;` FROM claims
LEFT JOIN provinces ON claims.val = provinces.area
WHERE claims.claim_type = 'area' AND claims.userid IS NOT ?
AND provinces.%[1]s = ?
);`
func (s *Store) FindConflicts(ctx context.Context, userId, name string, claimType ClaimType) ([]Conflict, error) { func (s *Store) FindConflicts(ctx context.Context, userId, name string, claimType ClaimType) ([]Conflict, error) {
ctx, span := tracer.Start(ctx, "find_conflicts", trace.WithAttributes( stmt, err := s.db.PrepareContext(ctx, fmt.Sprintf(conflictQuery, claimTypeToColumn[claimType]))
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("select"),
attribute.String("user_id", userId),
attribute.String("claim_name", name),
attribute.Stringer("claim_type", claimType),
))
defer span.End()
log.Debug().Ctx(ctx).Stringer("claim_type", claimType).Str("userid", userId).Msg("searching for potential conflicts")
stmt, err := s.db.PrepareContext(ctx, conflictQuery)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return nil, fmt.Errorf("failed to prepare conflicts query: %w", err) return nil, fmt.Errorf("failed to prepare conflicts query: %w", err)
} }
rows, err := stmt.QueryContext(ctx, claimType, name, userId) rows, err := stmt.QueryContext(ctx, userId, name, userId, name, userId, name)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to get conflicting provinces: %w", err) return nil, fmt.Errorf("failed to get conflicting provinces: %w", err)
} }
defer stmt.Close()
conflicts := make([]Conflict, 0) conflicts := make([]Conflict, 0)
for rows.Next() { for rows.Next() {
@ -76,8 +59,6 @@ func (s *Store) FindConflicts(ctx context.Context, userId, name string, claimTyp
) )
err = rows.Scan(&province, &player, &sClaimType, &claimName, &claimId) err = rows.Scan(&province, &player, &sClaimType, &claimName, &claimId)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return nil, fmt.Errorf("failed to scan row: %w", err) return nil, fmt.Errorf("failed to scan row: %w", err)
} }

@ -2,24 +2,19 @@ package themis
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestStore_FindConflicts(t *testing.T) { func TestStore_FindConflicts(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_FindConflicts")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_FindConflicts"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
id, err := store.Claim(context.Background(), "000000000000000001", "foo", "Bordeaux", CLAIM_TYPE_TRADE) id, err := store.Claim(context.TODO(), "000000000000000001", "foo", "Bordeaux", CLAIM_TYPE_TRADE)
require.NoError(t, err) assert.NoError(t, err)
type args struct { type args struct {
ctx context.Context ctx context.Context
@ -47,7 +42,7 @@ func TestStore_FindConflicts(t *testing.T) {
{ {
name: "overlapping", name: "overlapping",
args: args{ args: args{
context.Background(), context.TODO(),
"000000000000000002", "000000000000000002",
"Iberia", "Iberia",
CLAIM_TYPE_REGION, CLAIM_TYPE_REGION,

@ -1,44 +0,0 @@
package correlation
import (
"net/url"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
type TraceContextHook struct{}
func (h TraceContextHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
ctx := e.GetCtx()
spanContext := trace.SpanContextFromContext(ctx)
trace_id := spanContext.TraceID()
if trace_id.IsValid() {
e.Stringer("trace_id", trace_id)
}
}
var _ propagation.TextMapCarrier = UrlValuesCarrier{}
type UrlValuesCarrier url.Values
// Get implements propagation.TextMapCarrier.
func (u UrlValuesCarrier) Get(key string) string {
return url.Values(u).Get(key)
}
// Keys implements propagation.TextMapCarrier.
func (u UrlValuesCarrier) Keys() []string {
raw := map[string][]string(u)
ks := make([]string, 0, len(raw))
for k := range raw {
ks = append(ks, k)
}
return ks
}
// Set implements propagation.TextMapCarrier.
func (u UrlValuesCarrier) Set(key string, value string) {
url.Values(u).Set(key, value)
}

File diff suppressed because it is too large Load Diff

@ -6,7 +6,6 @@ import (
) )
var ErrNoSuchClaim = errors.New("no such claim") var ErrNoSuchClaim = errors.New("no such claim")
var ErrNever = errors.New("event never occured")
type ErrConflict struct { type ErrConflict struct {
Conflicts []Conflict Conflicts []Conflict

@ -7,7 +7,6 @@ processes = []
[env] [env]
TZ = "America/New_York" TZ = "America/New_York"
ENV = "production"
DISCORD_APP_ID = "1014881815921705030" DISCORD_APP_ID = "1014881815921705030"
DISCORD_GUILD_ID = "375417755777892353" DISCORD_GUILD_ID = "375417755777892353"
DISCORD_BOT_CHANNEL_ID = "1018997240968265768" DISCORD_BOT_CHANNEL_ID = "1018997240968265768"
@ -27,6 +26,15 @@ DISCORD_BOT_CHANNEL_ID = "1018997240968265768"
soft_limit = 20 soft_limit = 20
type = "connections" type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]] [[services.tcp_checks]]
grace_period = "1s" grace_period = "1s"
interval = "15s" interval = "15s"

@ -1,34 +1,19 @@
package themis package themis
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
"strings" "strings"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
) )
func FormatRows(ctx context.Context, rows *sql.Rows) (string, error) { func FormatRows(rows *sql.Rows) (string, error) {
ctx, span := tracer.Start(ctx, "format_rows", trace.WithAttributes(
semconv.DBSystemSqlite,
))
defer span.End()
sb := strings.Builder{} sb := strings.Builder{}
cols, err := rows.Columns() cols, err := rows.Columns()
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to get rows columns")
return "", fmt.Errorf("failed to get rows columns: %w", err) return "", fmt.Errorf("failed to get rows columns: %w", err)
} }
log.Debug().Ctx(ctx).Int("columns", len(cols)).Msg("formatting sql rows to markdown table")
c := make([]string, len(cols)) c := make([]string, len(cols))
for i := range c { for i := range c {
c[i] = " %-*s " c[i] = " %-*s "
@ -47,8 +32,6 @@ func FormatRows(ctx context.Context, rows *sql.Rows) (string, error) {
row[i] = new(sql.NullString) row[i] = new(sql.NullString)
} }
if err := rows.Scan(row...); err != nil { if err := rows.Scan(row...); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return "", fmt.Errorf("failed to scan next row: %w", err) return "", fmt.Errorf("failed to scan next row: %w", err)
} }
@ -61,8 +44,6 @@ func FormatRows(ctx context.Context, rows *sql.Rows) (string, error) {
} }
} }
log.Debug().Ctx(ctx).Int("rows", len(scanned)).Ints("lengths", lengths).Msg("scanned rows and extracted max column lengths")
// Write column names // Write column names
curr := make([]any, 0, 2*len(cols)) curr := make([]any, 0, 2*len(cols))
for i := range lengths { for i := range lengths {

@ -1,26 +1,20 @@
package themis package themis
import ( import (
"context"
"database/sql"
"fmt" "fmt"
"testing" "testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestFormatRows(t *testing.T) { func TestFormatRows(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
rows, err := store.db.Query("SELECT provinces.name, provinces.region, provinces.area, provinces.trade_node FROM provinces WHERE area = 'Gascony'") rows, err := store.db.Query("SELECT provinces.name, provinces.region, provinces.area, provinces.trade_node FROM provinces WHERE area = 'Gascony'")
assert.NoError(t, err) assert.NoError(t, err)
fmtd, err := FormatRows(context.Background(), rows) fmtd, err := FormatRows(rows)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, `| name | region | area | trade_node | assert.Equal(t, `| name | region | area | trade_node |
| -------- | ------ | ------- | ---------- | | -------- | ------ | ------- | ---------- |
@ -32,15 +26,13 @@ func TestFormatRows(t *testing.T) {
} }
func TestFormatRowsAggregated(t *testing.T) { func TestFormatRowsAggregated(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
rows, err := store.db.Query("SELECT count(1) as total, trade_node from provinces where region = 'France' group by trade_node") rows, err := store.db.Query("SELECT count(1) as total, trade_node from provinces where region = 'France' group by trade_node")
assert.NoError(t, err) assert.NoError(t, err)
fmtd, err := FormatRows(context.Background(), rows) fmtd, err := FormatRows(rows)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, `| total | trade_node | assert.Equal(t, `| total | trade_node |
| ----- | --------------- | | ----- | --------------- |
@ -53,9 +45,7 @@ func TestFormatRowsAggregated(t *testing.T) {
} }
func TestFormatRowsInvalidQuery(t *testing.T) { func TestFormatRowsInvalidQuery(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
_, err = store.db.Query("SELECT count(name), distinct(trade_node) from provinces where region = 'France'") _, err = store.db.Query("SELECT count(name), distinct(trade_node) from provinces where region = 'France'")

@ -1,54 +1,13 @@
module go.wperron.io/themis module go.wperron.io/themis
go 1.21.1 go 1.21
require ( require (
github.com/bwmarrin/discordgo v0.26.1 github.com/bwmarrin/discordgo v0.26.1
github.com/golang-migrate/migrate/v4 v4.17.0 github.com/golang-migrate/migrate/v4 v4.16.2
github.com/mattn/go-sqlite3 v1.14.19 github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.28.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.1
go.opentelemetry.io/otel/trace v1.24.0
go.wperron.io/sqliteexporter v0.1.0
)
require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/knadh/koanf/providers/confmap v0.1.0 // indirect
github.com/knadh/koanf/v2 v2.0.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/collector v0.92.0 // indirect
go.opentelemetry.io/collector/component v0.94.1 // indirect
go.opentelemetry.io/collector/config/configretry v0.92.0 // indirect
go.opentelemetry.io/collector/config/configtelemetry v0.94.1 // indirect
go.opentelemetry.io/collector/confmap v0.94.1 // indirect
go.opentelemetry.io/collector/consumer v0.92.0 // indirect
go.opentelemetry.io/collector/exporter v0.92.0 // indirect
go.opentelemetry.io/collector/extension v0.92.0 // indirect
go.opentelemetry.io/collector/featuregate v1.0.1 // indirect
go.opentelemetry.io/collector/pdata v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
go.wperron.io/flydetector v0.0.0-20240302202855-7f606a93cdb6 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
) )
require ( require (
@ -56,13 +15,11 @@ require (
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/sdk v1.24.0
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.18.0 // indirect golang.org/x/crypto v0.7.0 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

279
go.sum

@ -1,70 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg=
contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE= github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU=
github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -72,223 +14,46 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
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/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
github.com/knadh/koanf/v2 v2.0.2 h1:sEZzPW2rVWSahcYILNq/syJdEyRafZIG0l9aWwL86HA=
github.com/knadh/koanf/v2 v2.0.2/go.mod h1:HN9uZ+qFAejH1e4G41gnoffIanINWQuONLXiV7kir6k=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0=
github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/collector v0.92.0 h1:XiC0ptaT1EmOkK2RI0gt3n2tkzLAkNQGf0E7hrGdyeA=
go.opentelemetry.io/collector v0.92.0/go.mod h1:wbksjM63DTKA1BbdUVS7gAFzAngCZTWb46RBpKdtsPw=
go.opentelemetry.io/collector/component v0.94.1 h1:j4peKsWb+QVBKPs2RJeIj5EoQW7yp2ZVGrd8Bu9HU9M=
go.opentelemetry.io/collector/component v0.94.1/go.mod h1:vg+kAH81C3YS0SPzUXkSFWLPC1WH7zx70dAtUWWIHcE=
go.opentelemetry.io/collector/config/configretry v0.92.0 h1:3WUabmCRIBHSkOLGCHGieUGchlHkBw3Fa4Cj9Do5Xdw=
go.opentelemetry.io/collector/config/configretry v0.92.0/go.mod h1:gt1HRYyMxcMca9lbDLPbivQzsUCjVjkPAn/3S6fiD14=
go.opentelemetry.io/collector/config/configtelemetry v0.94.1 h1:ztYpBEBlvhcoxMiDKNmQ2SS+A41JZ4M19GfcxjCo8Zs=
go.opentelemetry.io/collector/config/configtelemetry v0.94.1/go.mod h1:2XLhyR/GVpWeZ2K044vCmrvH/d4Ewt0aD/y46avZyMU=
go.opentelemetry.io/collector/confmap v0.94.1 h1:O69bkeyR1YPAFz+jMd45aDZc1DtYnwb3Skgr2yALPqQ=
go.opentelemetry.io/collector/confmap v0.94.1/go.mod h1:pCT5UtcHaHVJ5BIILv1Z2VQyjZzmT9uTdBmC9+Z0AgA=
go.opentelemetry.io/collector/consumer v0.92.0 h1:twa8T0iR9KVglvRbwZ5OPKLXPCC2DO6gVhrgDZ47MPE=
go.opentelemetry.io/collector/consumer v0.92.0/go.mod h1:fBZqP7bou3I7pDhWjleBuzdaLfQgJBc92wPJVOcKaGU=
go.opentelemetry.io/collector/exporter v0.92.0 h1:z6u+/hswJUuZbuPYIF2gXMZsqjIDd/tJO60XjLM850U=
go.opentelemetry.io/collector/exporter v0.92.0/go.mod h1:54ODYn1weY/Wr0bdxESj4P1fgyX+zaUsnJJnafORqIY=
go.opentelemetry.io/collector/extension v0.92.0 h1:zaehgW+LXCMNEb1d6Af/VHWphh5ZwX9aZl+NuQLGhpQ=
go.opentelemetry.io/collector/extension v0.92.0/go.mod h1:5EYwiaGU6deSY8YWqT5gvlnD850yJXP3NqFRKVVbYLs=
go.opentelemetry.io/collector/featuregate v1.0.1 h1:ok//hLSXttBbyu4sSV1pTx1nKdr5udSmrWy5sFMIIbM=
go.opentelemetry.io/collector/featuregate v1.0.1/go.mod h1:QQXjP4etmJQhkQ20j4P/rapWuItYxoFozg/iIwuKnYg=
go.opentelemetry.io/collector/pdata v1.1.0 h1:cE6Al1rQieUjMHro6p6cKwcu3sjHXGG59BZ3kRVUvsM=
go.opentelemetry.io/collector/pdata v1.1.0/go.mod h1:IDkDj+B4Fp4wWOclBELN97zcb98HugJ8Q2gA4ZFsN8Q=
go.opentelemetry.io/collector/receiver v0.92.0 h1:TRz4ufr5bFEszpAWgYVEx/b7VPZzEcECsyMzztf5PsQ=
go.opentelemetry.io/collector/receiver v0.92.0/go.mod h1:bYAAYbMuUVj3wx7ave2iyyJ+aGUpACliYOQ5xI92I7k=
go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E=
go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/prometheus v0.45.1 h1:R/bW3afad6q6VGU+MFYpnEdo0stEARMCdhWu6+JI6aI=
go.opentelemetry.io/otel/exporters/prometheus v0.45.1/go.mod h1:wnHAfKRav5Dfp4iZhyWZ7SzQfT+rDZpEpYG7To+qJ1k=
go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo=
go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.23.0 h1:0KM9Zl2esnl+WSukEmlaAEjVY5HDZANOHferLq36BPc=
go.opentelemetry.io/otel/sdk v1.23.0/go.mod h1:wUscup7byToqyKJSilEtMf34FgdCAsFpFOjXnAwFfO0=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/sdk/metric v1.23.0 h1:u81lMvmK6GMgN4Fty7K7S6cSKOZhMKJMK2TB+KaTs0I=
go.opentelemetry.io/otel/sdk/metric v1.23.0/go.mod h1:2LUOToN/FdX6wtfpHybOnCZjoZ6ViYajJYMiJ1LKDtQ=
go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI=
go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.wperron.io/flydetector v0.0.0-20240302202855-7f606a93cdb6 h1:SaV3yjkbcoC6sFgQbQhmVIYN9VSEMmvuUadAzBFe/YA=
go.wperron.io/flydetector v0.0.0-20240302202855-7f606a93cdb6/go.mod h1:XUxNxHOGyI6655abXX5+04qdore1sDRErBVmWFrmZV4=
go.wperron.io/sqliteexporter v0.1.0-rc5.0.20240209234500-ad89647cf9b6 h1:GbIjafkOpD/bWS2qR8PAOQAZPTQTQLk5XC4+d7DI7Fw=
go.wperron.io/sqliteexporter v0.1.0-rc5.0.20240209234500-ad89647cf9b6/go.mod h1:iQD28FG3zrdOEpKTGxvWCcLdir5eavk5bKjjN3RQ6Xc=
go.wperron.io/sqliteexporter v0.1.0 h1:0oK7DxZOMAwaGqRo5FVr9Nba3/IB5uO8Arfvi9rQADo=
go.wperron.io/sqliteexporter v0.1.0/go.mod h1:iQD28FG3zrdOEpKTGxvWCcLdir5eavk5bKjjN3RQ6Xc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -1,6 +0,0 @@
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT,
userid TEXT,
ts TIMESTAMP
);

@ -1,3 +0,0 @@
DROP TABLE provinces;
CREATE TABLE provinces AS SELECT * FROM provinces_bak;
DROP TABLE provinces_bak;

File diff suppressed because it is too large Load Diff

@ -1,14 +0,0 @@
create view if not exists claimables as
with
trades as (select distinct trade_node from provinces where trade_node != ''),
areas as (select distinct area from provinces where area != ''),
regions as (select distinct region from provinces where region != '')
select 'trade' as typ, provinces.trade_node as name, name as province, id
from provinces inner join trades on trades.trade_node = provinces.trade_node
union
select 'area' as typ, provinces.area as name, name as province, id
from provinces inner join areas on areas.area = provinces.area
union
select 'region' as typ, provinces.region as name, name as province, id
from provinces inner join regions on regions.region = provinces.region
;

@ -1,15 +0,0 @@
drop table claimables;
create view if not exists claimables as
with
trades as (select distinct trade_node from provinces where trade_node != ''),
areas as (select distinct area from provinces where area != ''),
regions as (select distinct region from provinces where region != '')
select 'trade' as typ, provinces.trade_node as name, name as province, id
from provinces inner join trades on trades.trade_node = provinces.trade_node
union
select 'area' as typ, provinces.area as name, name as province, id
from provinces inner join areas on areas.area = provinces.area
union
select 'region' as typ, provinces.region as name, name as province, id
from provinces inner join regions on regions.region = provinces.region
;

@ -1,15 +0,0 @@
drop view claimables;
create table if not exists claimables as
with
trades as (select distinct trade_node from provinces where trade_node != ''),
areas as (select distinct area from provinces where area != ''),
regions as (select distinct region from provinces where region != '')
select 'trade' as typ, provinces.trade_node as name, name as province, id
from provinces inner join trades on trades.trade_node = provinces.trade_node
union
select 'area' as typ, provinces.area as name, name as province, id
from provinces inner join areas on areas.area = provinces.area
union
select 'region' as typ, provinces.region as name, name as province, id
from provinces inner join regions on regions.region = provinces.region
;

@ -4,9 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"time" "time"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/trace"
) )
var loc *time.Location var loc *time.Location
@ -16,10 +13,10 @@ func init() {
} }
type Notifier struct { type Notifier struct {
c chan context.Context c chan struct{}
} }
func NewNotifier(c chan context.Context) *Notifier { func NewNotifier(c chan struct{}) *Notifier {
return &Notifier{ return &Notifier{
c: c, c: c,
} }
@ -37,57 +34,34 @@ func (n *Notifier) Start(ctx context.Context) {
panic("failed to parse next monday notif time. this is likely a bug.") panic("failed to parse next monday notif time. this is likely a bug.")
} }
log.Debug().Time("next", t).Msg("starting notifier instance")
first := time.NewTimer(time.Until(t)) first := time.NewTimer(time.Until(t))
<-first.C <-first.C
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msg("context deadline exceeded, exiting notifier")
return return
default: default:
log.Debug().Str("parent", "ticker").Msg("notifier tick") n.c <- struct{}{}
ctx, span := tracer.Start(ctx, "notifier_tick", trace.WithNewRoot())
n.c <- ctx
span.End()
} }
ticker := time.NewTicker(time.Hour * 24 * 7) ticker := time.NewTicker(time.Hour * 24 * 7)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msg("context deadline exceeded, exiting notifier")
return return
case <-ticker.C: case <-ticker.C:
log.Debug().Str("parent", "ticker").Msg("notifier tick") n.c <- struct{}{}
ctx, span := tracer.Start(ctx, "notifier_tick", trace.WithNewRoot())
n.c <- ctx
span.End()
} }
time.Sleep(time.Second) time.Sleep(time.Second)
} }
} }
// Trigger the notifier manually. Should be used for testing purposes only. func (n *Notifier) NotifyFunc(ctx context.Context, f func()) {
func (n *Notifier) Send(ctx context.Context) {
log.Debug().Str("parent", "trigger").Ctx(ctx).Msg("notifier tick")
n.c <- ctx
}
func (n *Notifier) NotifyFunc(ctx context.Context, f func(context.Context)) {
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msg("context deadline exceeded, exiting notify func")
return return
case innerCtx := <-n.c: case <-n.c:
innerCtx, span := tracer.Start(innerCtx, "notify_func") f()
log.Debug().Ctx(innerCtx).Msg("tick received, notifying function")
f(innerCtx)
span.End()
} }
time.Sleep(time.Second) time.Sleep(time.Second)
} }

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"embed" "embed"
_ "embed"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -12,30 +13,21 @@ import (
"github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs" "github.com/golang-migrate/migrate/v4/source/iofs"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
) )
//go:embed migrations/*.sql //go:embed migrations/*.sql
var migrations embed.FS var migrations embed.FS
var tracer trace.Tracer
func init() {
tp := otel.GetTracerProvider()
tracer = tp.Tracer("themis")
}
type Store struct { type Store struct {
db *sql.DB db *sql.DB
logger zerolog.Logger
} }
func NewStore(db *sql.DB, logger zerolog.Logger) (*Store, error) { func NewStore(conn string) (*Store, error) {
db, err := sql.Open("sqlite3", conn)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
d, err := iofs.New(migrations, "migrations") d, err := iofs.New(migrations, "migrations")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open iofs migration source: %w", err) return nil, fmt.Errorf("failed to open iofs migration source: %w", err)
@ -56,120 +48,60 @@ func NewStore(db *sql.DB, logger zerolog.Logger) (*Store, error) {
return nil, fmt.Errorf("failed to roll up migrations: %w", err) return nil, fmt.Errorf("failed to roll up migrations: %w", err)
} }
ver, dirty, err := m.Version()
if err != nil && err != migrate.ErrNilVersion {
return nil, fmt.Errorf("failed to get database migration version: %w", err)
}
logger.Debug().Uint("current_version", ver).Bool("dirty", dirty).Msg("running database migrations")
return &Store{ return &Store{
logger: logger,
db: db, db: db,
}, nil }, nil
} }
func (s *Store) Close() error { func (s *Store) Close() error {
s.logger.Debug().Msg("closing database")
return s.db.Close() return s.db.Close()
} }
func (s *Store) Claim(ctx context.Context, userId, player, province string, claimType ClaimType) (int, error) { func (s *Store) Claim(ctx context.Context, userId, player, province string, claimType ClaimType) (int, error) {
ctx, span := tracer.Start(ctx, "claim", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("insert"),
attribute.String("user_id", userId),
attribute.String("claim_name", province),
attribute.Stringer("claim_type", claimType),
))
defer span.End()
s.logger.Debug().
Ctx(ctx).
Str("userid", userId).
Str("player", player).
Str("province", province).
Stringer("claim_type", claimType).
Msg("inserting claim")
audit := &AuditableEvent{
userId: userId,
eventType: EventClaim,
}
defer s.Audit(ctx, audit)
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to begin transaction")
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer tx.Commit() //nolint:errcheck defer tx.Commit() //nolint:errcheck
conflicts, err := s.FindConflicts(ctx, userId, province, claimType) conflicts, err := s.FindConflicts(ctx, userId, province, claimType)
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "conflict check failed")
return 0, fmt.Errorf("failed to run conflicts check: %w", err) return 0, fmt.Errorf("failed to run conflicts check: %w", err)
} }
if len(conflicts) > 0 { if len(conflicts) > 0 {
s.logger.Debug().Ctx(ctx).Int("len", len(conflicts)).Msg("found conflicts")
audit.err = errors.New("found conflicts")
return 0, ErrConflict{Conflicts: conflicts} return 0, ErrConflict{Conflicts: conflicts}
} }
// check that provided name matches the claim type // check that provided name matches the claim type
stmt, err := s.db.PrepareContext(ctx, `SELECT COUNT(1) FROM claimables WHERE lower(name) = ? and typ = ?`) stmt, err := s.db.PrepareContext(ctx, fmt.Sprintf(`SELECT COUNT(1) FROM provinces WHERE LOWER(provinces.%s) = ?`, claimTypeToColumn[claimType]))
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return 0, fmt.Errorf("failed to prepare count query: %w", err) return 0, fmt.Errorf("failed to prepare count query: %w", err)
} }
defer stmt.Close()
row := stmt.QueryRowContext(ctx, strings.ToLower(province), claimType) row := stmt.QueryRowContext(ctx, strings.ToLower(province))
var count int var count int
err = row.Scan(&count) err = row.Scan(&count)
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return 0, fmt.Errorf("failed to scan: %w", err) return 0, fmt.Errorf("failed to scan: %w", err)
} }
if count == 0 { if count == 0 {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "no matching provinces found")
return 0, fmt.Errorf("found no provinces for %s named %s", claimType, province) return 0, fmt.Errorf("found no provinces for %s named %s", claimType, province)
} }
stmt, err = s.db.PrepareContext(ctx, "INSERT INTO claims (player, claim_type, val, userid) VALUES (?, ?, ?, ?)") stmt, err = s.db.PrepareContext(ctx, "INSERT INTO claims (player, claim_type, val, userid) VALUES (?, ?, ?, ?)")
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return 0, fmt.Errorf("failed to prepare claim query: %w", err) return 0, fmt.Errorf("failed to prepare claim query: %w", err)
} }
defer stmt.Close()
res, err := stmt.ExecContext(ctx, player, claimType, province, userId) res, err := stmt.ExecContext(ctx, player, claimType, province, userId)
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return 0, fmt.Errorf("failed to insert claim: %w", err) return 0, fmt.Errorf("failed to insert claim: %w", err)
} }
id, err := res.LastInsertId() id, err := res.LastInsertId()
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to last insert id")
return 0, fmt.Errorf("failed to get last ID: %w", err) return 0, fmt.Errorf("failed to get last ID: %w", err)
} }
@ -177,42 +109,24 @@ func (s *Store) Claim(ctx context.Context, userId, player, province string, clai
} }
func (s *Store) ListAvailability(ctx context.Context, claimType ClaimType, search ...string) ([]string, error) { func (s *Store) ListAvailability(ctx context.Context, claimType ClaimType, search ...string) ([]string, error) {
ctx, span := tracer.Start(ctx, "list_availability", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claimables"),
semconv.DBOperation("select"),
attribute.StringSlice("search_terms", search),
attribute.Stringer("claim_type", claimType),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Stringer("claim_type", claimType).Strs("search_terms", search).Msg("listing available entries")
queryParams := []any{string(claimType)} queryParams := []any{string(claimType)}
queryPattern := `SELECT DISTINCT(provinces.%[1]s)
queryPattern := `SELECT distinct name FROM provinces LEFT JOIN claims ON provinces.%[1]s = claims.val AND claims.claim_type = ?
FROM claimables
LEFT JOIN claims ON claimables.name = claims.val AND claimables.typ = claims.claim_type
WHERE claims.val IS NULL WHERE claims.val IS NULL
AND claimables.typ = ?` AND provinces.typ = 'Land'`
if len(search) > 0 && search[0] != "" { if len(search) > 0 && search[0] != "" {
// only take one search param, ignore the rest // only take one search param, ignore the rest
queryPattern += `AND claimables.name LIKE ?` queryPattern += `AND provinces.%[1]s LIKE ?`
queryParams = append(queryParams, fmt.Sprintf("%%%s%%", search[0])) queryParams = append(queryParams, fmt.Sprintf("%%%s%%", search[0]))
} }
stmt, err := s.db.PrepareContext(ctx, queryPattern) stmt, err := s.db.PrepareContext(ctx, fmt.Sprintf(queryPattern, claimTypeToColumn[claimType]))
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return nil, fmt.Errorf("failed to prepare query: %w", err) return nil, fmt.Errorf("failed to prepare query: %w", err)
} }
defer stmt.Close()
rows, err := stmt.QueryContext(ctx, queryParams...) rows, err := stmt.QueryContext(ctx, queryParams...)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to execute query: %w", err) return nil, fmt.Errorf("failed to execute query: %w", err)
} }
@ -220,8 +134,6 @@ func (s *Store) ListAvailability(ctx context.Context, claimType ClaimType, searc
for rows.Next() { for rows.Next() {
var s string var s string
if err := rows.Scan(&s); err != nil { if err := rows.Scan(&s); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return nil, fmt.Errorf("failed to scan rows: %w", err) return nil, fmt.Errorf("failed to scan rows: %w", err)
} }
avail = append(avail, s) avail = append(avail, s)
@ -231,26 +143,13 @@ func (s *Store) ListAvailability(ctx context.Context, claimType ClaimType, searc
} }
func (s *Store) ListClaims(ctx context.Context) ([]Claim, error) { func (s *Store) ListClaims(ctx context.Context) ([]Claim, error) {
ctx, span := tracer.Start(ctx, "list_claims", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("select"),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Msg("listing all claims currently in database")
stmt, err := s.db.PrepareContext(ctx, `SELECT id, player, claim_type, val FROM claims`) stmt, err := s.db.PrepareContext(ctx, `SELECT id, player, claim_type, val FROM claims`)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return nil, fmt.Errorf("failed to prepare query: %w", err) return nil, fmt.Errorf("failed to prepare query: %w", err)
} }
defer stmt.Close()
rows, err := stmt.QueryContext(ctx) rows, err := stmt.QueryContext(ctx)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to execute query: %w", err) return nil, fmt.Errorf("failed to execute query: %w", err)
} }
@ -260,14 +159,10 @@ func (s *Store) ListClaims(ctx context.Context) ([]Claim, error) {
var rawType string var rawType string
err = rows.Scan(&c.ID, &c.Player, &rawType, &c.Name) err = rows.Scan(&c.ID, &c.Player, &rawType, &c.Name)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return nil, fmt.Errorf("failed to scan row: %w", err) return nil, fmt.Errorf("failed to scan row: %w", err)
} }
cl, err := ClaimTypeFromString(rawType) cl, err := ClaimTypeFromString(rawType)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to parse claim type")
return nil, fmt.Errorf("unexpected error converting raw claim type: %w", err) return nil, fmt.Errorf("unexpected error converting raw claim type: %w", err)
} }
c.Type = cl c.Type = cl
@ -293,22 +188,10 @@ func (cd ClaimDetail) String() string {
} }
func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error) { func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error) {
ctx, span := tracer.Start(ctx, "describe_claim", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("select"),
attribute.Int("claim_id", ID),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Int("id", ID).Msg("describing claim")
stmt, err := s.db.PrepareContext(ctx, `SELECT id, player, claim_type, val FROM claims WHERE id = ?`) stmt, err := s.db.PrepareContext(ctx, `SELECT id, player, claim_type, val FROM claims WHERE id = ?`)
if err != nil { if err != nil {
span.RecordError(err) return ClaimDetail{}, fmt.Errorf("failed to get claim: %w", err)
span.SetStatus(codes.Error, "failed to prepare query")
return ClaimDetail{}, fmt.Errorf("failed to prepare select claim query: %w", err)
} }
defer stmt.Close()
row := stmt.QueryRowContext(ctx, ID) row := stmt.QueryRowContext(ctx, ID)
@ -316,34 +199,24 @@ func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error)
var rawType string var rawType string
err = row.Scan(&c.ID, &c.Player, &rawType, &c.Name) err = row.Scan(&c.ID, &c.Player, &rawType, &c.Name)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
span.RecordError(ErrNoSuchClaim)
return ClaimDetail{}, ErrNoSuchClaim return ClaimDetail{}, ErrNoSuchClaim
} }
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return ClaimDetail{}, fmt.Errorf("failed to scan row: %w", err) return ClaimDetail{}, fmt.Errorf("failed to scan row: %w", err)
} }
cl, err := ClaimTypeFromString(rawType) cl, err := ClaimTypeFromString(rawType)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to parse claim type")
return ClaimDetail{}, fmt.Errorf("unexpected error converting raw claim type: %w", err) return ClaimDetail{}, fmt.Errorf("unexpected error converting raw claim type: %w", err)
} }
c.Type = cl c.Type = cl
stmt, err = s.db.PrepareContext(ctx, `SELECT province FROM claimables WHERE name = ? AND typ = ?`) stmt, err = s.db.PrepareContext(ctx, fmt.Sprintf(`SELECT name FROM provinces where provinces.%s = ?`, claimTypeToColumn[cl]))
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return ClaimDetail{}, fmt.Errorf("failed to prepare query: %w", err) return ClaimDetail{}, fmt.Errorf("failed to prepare query: %w", err)
} }
defer stmt.Close()
rows, err := stmt.QueryContext(ctx, c.Name, cl) rows, err := stmt.QueryContext(ctx, c.Name)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return ClaimDetail{}, fmt.Errorf("failed to execute query: %w", err) return ClaimDetail{}, fmt.Errorf("failed to execute query: %w", err)
} }
@ -352,8 +225,6 @@ func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error)
var p string var p string
err = rows.Scan(&p) err = rows.Scan(&p)
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return ClaimDetail{}, fmt.Errorf("failed to scan result set: %w", err) return ClaimDetail{}, fmt.Errorf("failed to scan result set: %w", err)
} }
provinces = append(provinces, p) provinces = append(provinces, p)
@ -366,102 +237,44 @@ func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error)
} }
func (s *Store) DeleteClaim(ctx context.Context, ID int, userId string) error { func (s *Store) DeleteClaim(ctx context.Context, ID int, userId string) error {
ctx, span := tracer.Start(ctx, "delete_claim", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("delete"),
attribute.Int("claim_id", ID),
attribute.String("user_id", userId),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Str("userid", userId).Int("id", ID).Msg("deleting claim")
audit := &AuditableEvent{
userId: userId,
eventType: EventUnclaim,
}
defer s.Audit(ctx, audit)
stmt, err := s.db.PrepareContext(ctx, "DELETE FROM claims WHERE id = ? AND userid = ?") stmt, err := s.db.PrepareContext(ctx, "DELETE FROM claims WHERE id = ? AND userid = ?")
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return fmt.Errorf("failed to prepare query: %w", err) return fmt.Errorf("failed to prepare query: %w", err)
} }
defer stmt.Close()
res, err := stmt.ExecContext(ctx, ID, userId) res, err := stmt.ExecContext(ctx, ID, userId)
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return fmt.Errorf("failed to delete claim ID %d: %w", ID, err) return fmt.Errorf("failed to delete claim ID %d: %w", ID, err)
} }
rows, err := res.RowsAffected() rows, err := res.RowsAffected()
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to get number of affected rows")
return fmt.Errorf("failed to get affected rows: %w", err) return fmt.Errorf("failed to get affected rows: %w", err)
} }
if rows == 0 { if rows == 0 {
audit.err = ErrNoSuchClaim
span.RecordError(ErrNoSuchClaim)
return ErrNoSuchClaim return ErrNoSuchClaim
} }
return nil return nil
} }
func (s *Store) CountClaims(ctx context.Context) (total, uniquePlayers int, err error) { func (s *Store) CountClaims(ctx context.Context) (total, uniquePlayers int, err error) {
ctx, span := tracer.Start(ctx, "count_claims", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("select"),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Msg("counting all claims and unique users")
stmt, err := s.db.PrepareContext(ctx, "SELECT COUNT(1), COUNT(DISTINCT(userid)) FROM claims") stmt, err := s.db.PrepareContext(ctx, "SELECT COUNT(1), COUNT(DISTINCT(userid)) FROM claims")
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return 0, 0, fmt.Errorf("failed to prepare query: %w", err) return 0, 0, fmt.Errorf("failed to prepare query: %w", err)
} }
defer stmt.Close()
res := stmt.QueryRowContext(ctx) res := stmt.QueryRowContext(ctx)
if err := res.Scan(&total, &uniquePlayers); err != nil { if err := res.Scan(&total, &uniquePlayers); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return 0, 0, fmt.Errorf("failed to scan result: %w", err) return 0, 0, fmt.Errorf("failed to scan result: %w", err)
} }
return total, uniquePlayers, nil return total, uniquePlayers, nil
} }
func (s *Store) Flush(ctx context.Context, userId string) error { func (s *Store) Flush(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "flush", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("delete"),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Str("initiated_by", userId).Msg("flushing all currently help claims")
audit := &AuditableEvent{
userId: userId,
eventType: EventFlush,
}
defer s.Audit(ctx, audit)
_, err := s.db.ExecContext(ctx, "DELETE FROM claims;") _, err := s.db.ExecContext(ctx, "DELETE FROM claims;")
if err != nil { if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return fmt.Errorf("failed to execute delete query: %w", err) return fmt.Errorf("failed to execute delete query: %w", err)
} }
return nil return nil

@ -3,13 +3,11 @@ package themis
import ( import (
"context" "context"
"database/sql"
_ "embed" _ "embed"
"fmt" "fmt"
"testing" "testing"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -17,9 +15,7 @@ import (
const TEST_CONN_STRING_PATTERN = "file:%s?mode=memory&cache=shared" const TEST_CONN_STRING_PATTERN = "file:%s?mode=memory&cache=shared"
func TestStore_Claim(t *testing.T) { func TestStore_Claim(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_Claim")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_Claim"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
require.NoError(t, err) require.NoError(t, err)
type args struct { type args struct {
@ -94,27 +90,17 @@ func TestStore_Claim(t *testing.T) {
wantErr: false, wantErr: false,
}, },
} }
lastAudit := 0
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if _, err := store.Claim(context.TODO(), tt.args.userId, tt.args.player, tt.args.province, tt.args.claimType); (err != nil) != tt.wantErr { if _, err := store.Claim(context.TODO(), tt.args.userId, tt.args.player, tt.args.province, tt.args.claimType); (err != nil) != tt.wantErr {
t.Errorf("Store.Claim() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("Store.Claim() error = %v, wantErr %v", err, tt.wantErr)
} }
if !tt.wantErr {
ae, err := store.LastOf(context.TODO(), EventClaim)
require.NoError(t, err)
assert.Greater(t, ae.Id, lastAudit)
lastAudit = ae.Id
}
}) })
} }
} }
func TestAvailability(t *testing.T) { func TestAvailability(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_Availability")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestAvailability"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE) store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE)
@ -130,22 +116,22 @@ func TestAvailability(t *testing.T) {
store.Claim(context.TODO(), "000000000000000001", "foo", "France", CLAIM_TYPE_REGION) store.Claim(context.TODO(), "000000000000000001", "foo", "France", CLAIM_TYPE_REGION)
store.Claim(context.TODO(), "000000000000000001", "foo", "Italy", CLAIM_TYPE_REGION) store.Claim(context.TODO(), "000000000000000001", "foo", "Italy", CLAIM_TYPE_REGION)
// There's a total of 92 distinct regions, there should be 90 available // There's a total of 73 distinct regions, there should be 71 available
// after the two claims above // after the two claims above
availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_REGION) availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_REGION)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 90, len(availability)) assert.Equal(t, 71, len(availability))
store.Claim(context.TODO(), "000000000000000001", "foo", "Normandy", CLAIM_TYPE_AREA) store.Claim(context.TODO(), "000000000000000001", "foo", "Normandy", CLAIM_TYPE_AREA)
store.Claim(context.TODO(), "000000000000000001", "foo", "Champagne", CLAIM_TYPE_AREA) store.Claim(context.TODO(), "000000000000000001", "foo", "Champagne", CLAIM_TYPE_AREA)
store.Claim(context.TODO(), "000000000000000001", "foo", "Lorraine", CLAIM_TYPE_AREA) store.Claim(context.TODO(), "000000000000000001", "foo", "Lorraine", CLAIM_TYPE_AREA)
store.Claim(context.TODO(), "000000000000000001", "foo", "Provence", CLAIM_TYPE_AREA) store.Claim(context.TODO(), "000000000000000001", "foo", "Provence", CLAIM_TYPE_AREA)
// There's a total of 882 distinct regions, there should be 878 available // There's a total of 823 distinct regions, there should be 819 available
// after the four claims above // after the four claims above
availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA) availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 878, len(availability)) assert.Equal(t, 819, len(availability))
// There is both a Trade Node and an Area called 'Valencia', while the trade // There is both a Trade Node and an Area called 'Valencia', while the trade
// node is claimed, the area should show up in the availability list (even // node is claimed, the area should show up in the availability list (even
@ -153,17 +139,15 @@ func TestAvailability(t *testing.T) {
store.Claim(context.TODO(), "000000000000000001", "foo", "Valencia", CLAIM_TYPE_TRADE) store.Claim(context.TODO(), "000000000000000001", "foo", "Valencia", CLAIM_TYPE_TRADE)
availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA) availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 878, len(availability)) // availability for areas should be the same as before assert.Equal(t, 819, len(availability)) // availability for areas should be the same as before
availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA, "bay") availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA, "bay")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 6, len(availability)) // availability for areas should be the same as before assert.Equal(t, 3, len(availability)) // availability for areas should be the same as before
} }
func TestDeleteClaim(t *testing.T) { func TestDeleteClaim(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_DeleteClaim")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestDeleteClaim"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
// make sure all claims are gone, this is due to how the in-memory database // make sure all claims are gone, this is due to how the in-memory database
// with a shared cache interacts with other tests running in parallel // with a shared cache interacts with other tests running in parallel
@ -181,23 +165,13 @@ func TestDeleteClaim(t *testing.T) {
err = store.DeleteClaim(context.TODO(), fooId, "000000000000000001") err = store.DeleteClaim(context.TODO(), fooId, "000000000000000001")
assert.NoError(t, err) assert.NoError(t, err)
ae, err := store.LastOf(context.TODO(), EventUnclaim)
require.NoError(t, err)
last := ae.Id
err = store.DeleteClaim(context.TODO(), barId, "000000000000000001") err = store.DeleteClaim(context.TODO(), barId, "000000000000000001")
assert.Error(t, err) assert.Error(t, err)
assert.ErrorIs(t, err, ErrNoSuchClaim) assert.ErrorIs(t, err, ErrNoSuchClaim)
ae, err = store.LastOf(context.TODO(), EventUnclaim)
require.NoError(t, err)
assert.Equal(t, last, ae.Id) // no new audit log was added
} }
func TestDescribeClaim(t *testing.T) { func TestDescribeClaim(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_DescribeClaim")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestDescribeClaim"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
id, err := store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE) id, err := store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE)
@ -213,9 +187,7 @@ func TestDescribeClaim(t *testing.T) {
} }
func TestCountClaims(t *testing.T) { func TestCountClaims(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_CountClaim")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestFlush"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE) store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE)
@ -231,9 +203,7 @@ func TestCountClaims(t *testing.T) {
} }
func TestFlush(t *testing.T) { func TestFlush(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_Flush")) store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestFlush"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
assert.NoError(t, err) assert.NoError(t, err)
store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE) store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE)
@ -242,11 +212,8 @@ func TestFlush(t *testing.T) {
store.Claim(context.TODO(), "000000000000000001", "foo", "Iberia", CLAIM_TYPE_REGION) store.Claim(context.TODO(), "000000000000000001", "foo", "Iberia", CLAIM_TYPE_REGION)
store.Claim(context.TODO(), "000000000000000001", "foo", "Ragusa", CLAIM_TYPE_TRADE) store.Claim(context.TODO(), "000000000000000001", "foo", "Ragusa", CLAIM_TYPE_TRADE)
assert.NoError(t, store.Flush(context.TODO(), "bob")) assert.NoError(t, store.Flush(context.TODO()))
claims, err := store.ListClaims(context.TODO()) claims, err := store.ListClaims(context.TODO())
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, len(claims)) assert.Equal(t, 0, len(claims))
_, err = store.LastOf(context.TODO(), EventFlush)
require.NoError(t, err)
} }

@ -3,11 +3,10 @@
package themis package themis
import ( import (
"context"
"time" "time"
) )
// Uptime returns the time elapsed since the start of the current process ID. // Uptime returns the time elapsed since the start of the current process ID.
func Uptime(ctx context.Context) (time.Duration, error) { func Uptime() (time.Duration, error) {
return 0, nil return 0, nil
} }

@ -4,22 +4,18 @@ package themis
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/rs/zerolog/log"
) )
// Uptime returns the time elapsed since the start of the current process ID. // Uptime returns the time elapsed since the start of the current process ID.
func Uptime(ctx context.Context) (time.Duration, error) { func Uptime() (time.Duration, error) {
raw, err := os.ReadFile("/proc/uptime") raw, err := os.ReadFile("/proc/uptime")
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to read uptime from OS: %w", err) return 0, fmt.Errorf("failed to read uptime from OS: %w", err)
} }
log.Debug().Ctx(ctx).Str("raw", string(raw)).Msg("reading /proc/uptime to get server up time")
i := bytes.IndexRune(raw, ' ') i := bytes.IndexRune(raw, ' ')

@ -1,10 +1,9 @@
package themis package themis
import ( import (
"context"
"runtime"
"testing" "testing"
"time" "time"
"runtime"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -13,7 +12,7 @@ func TestUptime(t *testing.T) {
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
t.Skip() t.Skip()
} }
uptime, err := Uptime(context.Background()) uptime, err := Uptime()
assert.NoError(t, err) assert.NoError(t, err)
assert.Greater(t, uptime, 100*time.Millisecond) assert.Greater(t, uptime, 100*time.Millisecond)
} }

Loading…
Cancel
Save