diff --git a/cmd/themis-server/main.go b/cmd/themis-server/main.go index f5a47ca..11a065e 100644 --- a/cmd/themis-server/main.go +++ b/cmd/themis-server/main.go @@ -76,8 +76,8 @@ func main() { commands := []*discordgo.ApplicationCommand{ { - Name: "ping", - Description: "Ping Themis", + Name: "info", + Description: "Server Information", Type: discordgo.ChatApplicationCommand, }, { @@ -151,11 +151,39 @@ func main() { }, } handlers := map[string]Handler{ - "ping": func(s *discordgo.Session, i *discordgo.InteractionCreate) { - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + "info": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + uptime, err := themis.Uptime() + if err != nil { + log.Error().Err(err).Msg("failed to get server uptime") + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Oops, something went wrong! :(", + }, + }) + if err != nil { + log.Error().Err(err).Msg("failed to respond to interaction") + } + } + + claimCount, uniquePlayers, err := store.CountClaims(ctx) + if err != nil { + log.Error().Err(err).Msg("failed to count claims") + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Oops, something went wrong! :(", + }, + }) + if err != nil { + log.Error().Err(err).Msg("failed to respond to interaction") + } + } + + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "Pong", + Content: fmt.Sprintf("Server has been up for %s, has %d claims from %d unique players", uptime, claimCount, uniquePlayers), }, }) if err != nil { diff --git a/store.go b/store.go index 8365540..158490b 100644 --- a/store.go +++ b/store.go @@ -237,6 +237,21 @@ func (s *Store) DeleteClaim(ctx context.Context, ID int, userId string) error { return nil } +func (s *Store) CountClaims(ctx context.Context) (total, uniquePlayers int, err error) { + stmt, err := s.db.PrepareContext(ctx, "SELECT COUNT(1), COUNT(DISTINCT(userid)) FROM claims") + if err != nil { + return 0, 0, fmt.Errorf("failed to prepare query: %w", err) + } + + res := stmt.QueryRowContext(ctx) + + if err := res.Scan(&total, &uniquePlayers); err != nil { + return 0, 0, fmt.Errorf("failed to scan result: %w", err) + } + + return total, uniquePlayers, nil +} + func (s *Store) Flush(ctx context.Context) error { _, err := s.db.ExecContext(ctx, "DELETE FROM claims;") if err != nil { diff --git a/store_test.go b/store_test.go index 3cdd03e..6d62886 100644 --- a/store_test.go +++ b/store_test.go @@ -185,6 +185,22 @@ func TestDescribeClaim(t *testing.T) { assert.ErrorIs(t, err, ErrNoSuchClaim) } +func TestCountClaims(t *testing.T) { + store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestFlush")) + assert.NoError(t, err) + + store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE) + store.Claim(context.TODO(), "000000000000000001", "foo", "Valencia", CLAIM_TYPE_TRADE) + store.Claim(context.TODO(), "000000000000000001", "foo", "Italy", CLAIM_TYPE_REGION) + store.Claim(context.TODO(), "000000000000000001", "foo", "Iberia", CLAIM_TYPE_REGION) + store.Claim(context.TODO(), "000000000000000001", "foo", "Ragusa", CLAIM_TYPE_TRADE) + + total, uniquePlayers, err := store.CountClaims(context.TODO()) + assert.NoError(t, err) + assert.Condition(t, func() bool { return total > 0 }) + assert.Condition(t, func() bool { return uniquePlayers > 0 }) +} + func TestFlush(t *testing.T) { store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestFlush")) assert.NoError(t, err) diff --git a/uptime.go b/uptime.go new file mode 100644 index 0000000..03d53e1 --- /dev/null +++ b/uptime.go @@ -0,0 +1,26 @@ +package themis + +import ( + "bytes" + "fmt" + "os" + "strconv" + "time" +) + +// Uptime returns the time elapsed since the start of the current process ID. +func Uptime() (time.Duration, error) { + raw, err := os.ReadFile("/proc/uptime") + if err != nil { + return 0, fmt.Errorf("failed to read uptime from OS: %w", err) + } + + i := bytes.IndexRune(raw, ' ') + + up, err := strconv.ParseFloat(string(raw[:i]), 64) + if err != nil { + return 0, fmt.Errorf("failed to parse uptime from OS: %w", err) + } + + return time.Duration(int(up*1000) * int(time.Millisecond)), nil +} diff --git a/uptime_test.go b/uptime_test.go new file mode 100644 index 0000000..1b15d25 --- /dev/null +++ b/uptime_test.go @@ -0,0 +1,14 @@ +package themis + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestUptime(t *testing.T) { + uptime, err := Uptime() + assert.NoError(t, err) + assert.Greater(t, uptime, 100*time.Millisecond) +}