From 146da98a7db60255d3726c376c8145bff196185c Mon Sep 17 00:00:00 2001 From: Zhiyuan Ma <631253076@qq.com> Date: Sun, 29 Sep 2024 23:38:09 -0400 Subject: [PATCH] feat: Add github action to run the command --- .github/workflows/go-run.yml | 7 +- cmd/server/main.go | 4 +- pkg/ncdmv/client.go | 120 ++++++++++++++++++----------------- 3 files changed, 66 insertions(+), 65 deletions(-) diff --git a/.github/workflows/go-run.yml b/.github/workflows/go-run.yml index 1c24b61..bd95121 100644 --- a/.github/workflows/go-run.yml +++ b/.github/workflows/go-run.yml @@ -3,9 +3,9 @@ name: Go Run on: push: branches: - - main + - master schedule: - - cron: '00 * * * *' + - cron: '30 * * * *' jobs: go_run: runs-on: ubuntu-latest @@ -39,5 +39,4 @@ jobs: - name: Run run: | - go run ./cmd/server -appt_type=driver-license-renewal -database_path=./database/ncdmv.db -locations=cary,clayton,durham-east,durham-south,fuquay-varina,garner,raleigh-east,raleigh-north,raleigh-west,wendell --discord_webhook=https://discord.com/api/webhooks/1289829329596973119/dArKKjlXCzBs0UUrWHN4uUxJkxm4jU0IN7m6rT8d3PJr_HqkLMMll4aoLoWQRQk32Hbf - \ No newline at end of file + go run ./cmd/server -appt_type=driver-license-renewal -database_path=./database/ncdmv.db -locations=cary,clayton,durham-east,durham-south,fuquay-varina,garner,raleigh-east,raleigh-north,raleigh-west,wendell --discord_webhook=${{ secrets.DISCORD_WEBHOOK_URL }} \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index fa2c6a7..4d2c485 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -85,7 +85,7 @@ func main() { } // Initialize the Chrome context and open a new window. - ctx, cancel, err := ncdmv.NewChromeContext(ctx, *headless, disableGpu, *debug) + ctx, cancel, err := ncdmv.NewChromeContext(ctx, true, true, *debug) if err != nil { log.Fatalf("Failed to init Chrome context: %s", err) } @@ -100,7 +100,7 @@ func main() { parsedTimeout := time.Duration(*timeout) * time.Second parsedInterval := time.Duration(*interval) * time.Minute - if err := client.Start(ctx, parsedApptType, parsedLocations, parsedTimeout, parsedInterval); err != nil { + if err := client.Start(ctx, parsedApptType, parsedLocations, parsedTimeout, parsedInterval, *locations); err != nil { log.Fatal(err) } } diff --git a/pkg/ncdmv/client.go b/pkg/ncdmv/client.go index 447fc66..2df64e4 100644 --- a/pkg/ncdmv/client.go +++ b/pkg/ncdmv/client.go @@ -429,9 +429,9 @@ func (c Client) RunForLocations(ctx context.Context, apptType AppointmentType, l return appointments, nil } -func (c Client) sendNotifications(ctx context.Context, apptType AppointmentType, appointmentsToNotify []models.Appointment, discordWebhook string, interval time.Duration) error { +func (c Client) sendNotifications(ctx context.Context, apptType AppointmentType, appointmentsToNotify []Appointment, discordWebhook string, interval time.Duration) error { // Sort appointments by time. - slices.SortFunc(appointmentsToNotify, func(a, b models.Appointment) int { + slices.SortFunc(appointmentsToNotify, func(a, b Appointment) int { if a.Time.Before(b.Time) { return -1 } else if a.Time.After(b.Time) { @@ -441,9 +441,9 @@ func (c Client) sendNotifications(ctx context.Context, apptType AppointmentType, }) // Group appointments by location. - appointmentsByLocation := make(map[string][]models.Appointment) + appointmentsByLocation := make(map[string][]Appointment) for _, a := range appointmentsToNotify { - appointmentsByLocation[a.Location] = append(appointmentsByLocation[a.Location], a) + appointmentsByLocation[a.Location.String()] = append(appointmentsByLocation[a.Location.String()], a) } // Sort locations by name. @@ -453,6 +453,12 @@ func (c Client) sendNotifications(ctx context.Context, apptType AppointmentType, } slices.Sort(locations) + if len(locations) == 0 { + if err := c.sendDiscordMessage("No availabilities found for the locations\n"); err != nil { + log.Printf("Failed to send message to Discord webhook %q: %v", c.discordWebhook, err) + } + } + for i, location := range locations { appointments := appointmentsByLocation[location] b := strings.Builder{} @@ -475,11 +481,7 @@ func (c Client) sendNotifications(ctx context.Context, apptType AppointmentType, b.WriteString(" - `(... more appointments available)`\n") break } - if appointment.Available { - b.WriteString(fmt.Sprintf(" - :white_check_mark: `%s`\n", appointment.Time.String())) - } else if c.notifyUnavailable { - b.WriteString(fmt.Sprintf(" - :x: `%s`\n", appointment.Time.String())) - } + b.WriteString(fmt.Sprintf(" - :white_check_mark: `%s`\n", appointment.Time.String())) } if i == len(locations)-1 { @@ -494,16 +496,16 @@ func (c Client) sendNotifications(ctx context.Context, apptType AppointmentType, } // Mark all of the appointments in the batch as "notified". - for _, appointment := range appointments { - if _, err := c.db.CreateNotification(ctx, models.CreateNotificationParams{ - AppointmentID: appointment.ID, - DiscordWebhook: sql.NullString{String: discordWebhook, Valid: true}, - Available: appointment.Available, - ApptType: apptType.String(), - }); err != nil { - return fmt.Errorf("failed to create notification for appointment %v: %w", appointment, err) - } - } + //for _, appointment := range appointments { + // if _, err := c.db.CreateNotification(ctx, models.CreateNotificationParams{ + // AppointmentID: 1, + // DiscordWebhook: sql.NullString{String: discordWebhook, Valid: true}, + // Available: true, + // ApptType: apptType.String(), + // }); err != nil { + // return fmt.Errorf("failed to create notification for appointment %v: %w", appointment, err) + // } + //} time.Sleep(interval) } @@ -588,14 +590,14 @@ func (c Client) listExistingAppointmentsInLocations(ctx context.Context, t time. func (c Client) handleTick(ctx context.Context, apptType AppointmentType, locations []Location, timeout time.Duration) error { now := time.Now() - // Prune all invalid appointments (i.e., those that are in the past) by setting them as unavailable. - rows, err := c.db.PruneAppointmentsBeforeDate(ctx, now) - if err != nil { - return fmt.Errorf("failed to delete appointments before current time (%v): %w", now, err) - } - if len(rows) > 0 { - slog.Info("Pruned invalid appointments", "count", len(rows)) - } + //// Prune all invalid appointments (i.e., those that are in the past) by setting them as unavailable. + //rows, err := c.db.PruneAppointmentsBeforeDate(ctx, now) + //if err != nil { + // return fmt.Errorf("failed to delete appointments before current time (%v): %w", now, err) + //} + //if len(rows) > 0 { + // slog.Info("Pruned invalid appointments", "count", len(rows)) + //} existingAppointments, err := c.listExistingAppointmentsInLocations(ctx, now, locations) if err != nil { @@ -650,9 +652,9 @@ func (c Client) handleTick(ctx context.Context, apptType AppointmentType, locati slog.Info("Updated appointments successfully", "count", len(appointmentsToUpdate)) } - if err := c.sendNotifications(ctx, apptType, appointmentsToNotify, c.discordWebhook, 1*time.Second); err != nil { - return fmt.Errorf("failed to send notifications: %w", err) - } + //if err := c.sendNotifications(ctx, apptType, appointmentsToNotify, c.discordWebhook, 1*time.Second); err != nil { + // return fmt.Errorf("failed to send notifications: %w", err) + //} if len(appointmentsToNotify) > 0 { slog.Info("Sent notifications successfully", "count", len(appointmentsToNotify)) } @@ -671,42 +673,42 @@ func (c Client) handleTick(ctx context.Context, apptType AppointmentType, locati // Each provided location is processed in a _separate_ Chrome browser tab. This allows for some degree of parallelism // as each tab can run independently of the others. The downside is that the list of locations needs to bounded based // on the resources available on your machine. -func (c Client) Start(ctx context.Context, apptType AppointmentType, locations []Location, timeout, interval time.Duration) error { - t := time.NewTicker(interval) - defer t.Stop() +func (c Client) Start(ctx context.Context, apptType AppointmentType, locations []Location, timeout, interval time.Duration, locationsString string) error { slog.Info("Starting client", "appt_type", apptType, "locations", locations, "timeout", timeout, "interval", interval) tick := func() error { - defer slog.Info("Sleeping between location checks...", "interval", interval) - for { - if err := c.handleTick(ctx, apptType, locations, timeout); err != nil { - if strings.Contains(err.Error(), temporaryErrString) { - slog.Warn("handleTick failed with temporary error; retrying tick...") - continue - } - slog.Error("handleTick failed", "err", err) - if c.stopOnFailure { - return err - } - } - return nil + b := strings.Builder{} + b.WriteString(fmt.Sprintf("Start searching at %s\n", time.Now().Format("2006-01-02 15:04:05"))) + b.WriteString(fmt.Sprintf("- Appointment type: %s\n", apptType)) + b.WriteString("- Locations: ") + b.WriteString(strings.Join(strings.Split(locationsString, ","), ", ")) + b.WriteString("\n") + + if err := c.sendDiscordMessage(b.String()); err != nil { + log.Printf("Failed to send message to Discord webhook %q: %v", c.discordWebhook, err) } - } - for { - // Trigger a "tick" immediately as the ticker does not do so for us. - if err := tick(); err != nil { - return err - } - // Block until the next tick or the context is cancelled. - select { - case <-t.C: - if err := tick(); err != nil { - return err + var appointments []*Appointment + for _, location := range locations { + appts, err := c.RunForLocations(ctx, apptType, []Location{location}, timeout) + if err != nil { + return fmt.Errorf("failed to check locations: %w", err) } - case <-ctx.Done(): - return ctx.Err() + appointments = append(appointments, appts...) + } + + values := make([]Appointment, len(appointments)) + for i, appt := range appointments { + values[i] = *appt } + if err := c.sendNotifications(ctx, apptType, values, c.discordWebhook, 1*time.Second); err != nil { + return fmt.Errorf("failed to send notifications: %w", err) + } + if len(appointments) > 0 { + slog.Info("Sent notifications successfully", "count", len(appointments)) + } + return nil } + return tick() }