Skip to content

Commit

Permalink
V2.2.0 (#113)
Browse files Browse the repository at this point in the history
* typo fix

* remove webhook

* update deps

* add startDate feature

* downgrade eslint

* show warning for old config file

* fix analysis workflow
  • Loading branch information
phamleduy04 authored May 23, 2024
1 parent 26a7e47 commit be1e867
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 130 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ name: Code Analysis
on:
push:
branches:
- main
- '*'
pull_request:
branches:
- '*'


jobs:
Expand Down
21 changes: 6 additions & 15 deletions example.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ personalInfo:
typeId: 71

location:
# Zipcode of your location can add more by using ','
# Zipcode of your location. Can add more DPS location by using ',' for multiple zipcode
zipCode: ['75067', '75080']
# Choose your DPS location by yourself when running the application if set to true
pickDPSLocation: false
Expand All @@ -35,6 +35,10 @@ location:
sameDay: false
# Put how many day from today you want to book from start to end (7 is a good number)
daysAround:
## Start date: start and end will start counting from this date
# MM/DD/YYYY format, if blank will use current date
# If the date input is invalid or in the past, the app will automatically use the current date as start date
startDate: null
start: 0
end: 7
# Put what time you want to book
Expand All @@ -53,17 +57,4 @@ appSettings:
# Set this to higher if you encounter Header Timeout error. This one is in miliseconds
headersTimeout: 20000
# How many times to retry if the request to DPS server failled
maxRetry: 3

# If you dont know what to change here, dont change it :)
webhook:
# If your using Bluebubbles webhook/api put this to true otherwise, keep it false
enable: false
url: ''
password: ''
# phone number with country code (EX: +11111111111)
phoneNumber: ''
# private-api or apple-script
sendMethod: 'apple-script'
# iMessage or SMS
phoneNumberType: 'SMS'
maxRetry: 3
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "texas-dps-scheduler",
"version": "2.1.0",
"version": "2.2.0",
"description": "Texas DPS Automatic Scheduler",
"main": "dist/index.js",
"scripts": {
Expand All @@ -20,30 +20,30 @@
},
"homepage": "https://github.com/phamleduy04/texas-dps-scheduler#readme",
"devDependencies": {
"@eslint/create-config": "^0.4.6",
"@eslint/create-config": "^1.1.1",
"@types/ms": "^0.7.34",
"@types/node": "^20.11.21",
"@types/node": "^20.12.12",
"@types/prompts": "^2.4.9",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@typescript-eslint/eslint-plugin": "^7.10.0",
"@typescript-eslint/parser": "^7.10.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.4.5"
},
"dependencies": {
"colorette": "^2.0.20",
"dayjs": "^1.11.10",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5",
"js-yaml": "^4.1.0",
"p-queue": "6.6.2",
"prompts": "^2.4.2",
"tslib": "^2.6.2",
"undici": "^6.6.2",
"yaml": "^2.4.0",
"zod": "^3.22.4"
"undici": "^6.18.1",
"yaml": "^2.4.2",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18.0"
Expand Down
122 changes: 43 additions & 79 deletions src/Client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import isBetween from 'dayjs/plugin/isBetween';
dayjs.extend(isBetween);
import prompts from 'prompts';
import type { EligibilityPayload } from '../Interfaces/Eligibility';
import type { AvaliableLocationPayload, AvaliableLocationResponse } from '../Interfaces/AvaliableLocation';
import type { AvaliableLocationDatesPayload, AvaliableLocationDatesResponse, AvaliableTimeSlots } from '../Interfaces/AvaliableLocationDates';
import type { AvailableLocationPayload, AvailableLocationResponse } from '../Interfaces/AvailableLocation';
import type { AvailableLocationDatesPayload, AvailableLocationDatesResponse, AvailableTimeSlots } from '../Interfaces/AvailableLocationDates';
import type { HoldSlotPayload, HoldSlotResponse } from '../Interfaces/HoldSlot';
import type { BookSlotPayload, BookSlotResponse } from '../Interfaces/BookSlot';
import type { ExistBookingPayload, ExistBookingResponse } from '../Interfaces/ExistBooking';
import type { CancelBookingPayload } from '../Interfaces/CancelBooking';
import type { webhookPayload } from '../Interfaces/Webhook';

import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';

Expand All @@ -34,7 +33,7 @@ class TexasScheduler {
public config = parseConfig();
public existBooking: { exist: boolean; response: ExistBookingResponse[] } | undefined;

private avaliableLocation: AvaliableLocationResponse[] | null = null;
private availableLocation: AvailableLocationResponse[] | null = null;
private isBooked = false;
private isHolded = false;
private queue = new pQueue({ concurrency: 1 });
Expand All @@ -43,7 +42,7 @@ class TexasScheduler {
// eslint-disable-next-line @typescript-eslint/no-var-requires, prettier/prettier
if (this.config.appSettings.webserver) require('http').createServer((req: any, res: any) => res.end('Bot is alive!')).listen(process.env.PORT || 3000);
log.info(`Texas Scheduler v${packagejson.version} is starting...`);
log.info('Requesting Avaliable Location....');
log.info('Requesting Available Location....');
if (!existsSync('cache')) mkdirSync('cache');
this.run();
}
Expand All @@ -55,7 +54,7 @@ class TexasScheduler {
log.warn(`You have an existing booking at ${response[0].SiteName} ${dayjs(response[0].BookingDateTime).format('MM/DD/YYYY hh:mm A')}`);
log.warn(`The bot will continue to run, but will cancel existing booking if it found a new one`);
}
await this.requestAvaliableLocation();
await this.requestAvailableLocation();
await this.getLocationDatesAll();
}

Expand Down Expand Up @@ -99,31 +98,31 @@ class TexasScheduler {
return response[0].ResponseId;
}

public async getAllLocationFromZipCodes(): Promise<AvaliableLocationResponse[]> {
public async getAllLocationFromZipCodes(): Promise<AvailableLocationResponse[]> {
const zipcodeList = this.config.location.zipCode;
const finalArray: AvaliableLocationResponse[] = [];
const finalArray: AvailableLocationResponse[] = [];
for (let i = 0; i < zipcodeList.length; i++) {
const requestBody: AvaliableLocationPayload = {
const requestBody: AvailableLocationPayload = {
CityName: '',
PreferredDay: 0,
// 71 is new driver license
TypeId: this.config.personalInfo.typeId || 71,
ZipCode: zipcodeList[i],
};
const response: AvaliableLocationResponse[] = await this.requestApi('/api/AvailableLocation/', 'POST', requestBody).then(
res => res.body.json() as Promise<AvaliableLocationResponse[]>,
const response: AvailableLocationResponse[] = await this.requestApi('/api/AvailableLocation/', 'POST', requestBody).then(
res => res.body.json() as Promise<AvailableLocationResponse[]>,
);
response.forEach(el => (el.ZipCode = zipcodeList[i]));
finalArray.push(...response);
}

return finalArray.sort((a, b) => a.Distance - b.Distance).filter((elem, index) => finalArray.findIndex(obj => obj.Id === elem.Id) === index);
}
public async requestAvaliableLocation(): Promise<void> {
public async requestAvailableLocation(): Promise<void> {
const response = await this.getAllLocationFromZipCodes();
if (this.config.location.pickDPSLocation) {
if (existsSync('././cache/location.json')) {
this.avaliableLocation = JSON.parse(readFileSync('././cache/location.json', 'utf-8'));
this.availableLocation = JSON.parse(readFileSync('././cache/location.json', 'utf-8'));
log.info('Found cached location selection, using cached location selection');
log.info('If you want to change location selection, please delete cache folder!');
return;
Expand All @@ -138,76 +137,74 @@ class TexasScheduler {
log.error('You must choose at least one location!');
process.exit(1);
}
this.avaliableLocation = userResponse.location;
this.availableLocation = userResponse.location;
writeFileSync('././cache/location.json', JSON.stringify(userResponse.location));
return;
}
const filteredResponse = response.filter((location: AvaliableLocationResponse) => location.Distance < this.config.location.miles);
const filteredResponse = response.filter((location: AvailableLocationResponse) => location.Distance < this.config.location.miles);
if (filteredResponse.length === 0) {
log.error(`No avaliable location found! Nearest location is ${response[0].Distance} miles away! Please change your config and try again!`);
log.error(`No Available location found! Nearest location is ${response[0].Distance} miles away! Please change your config and try again!`);
process.exit(0);
}
log.info(`Found ${filteredResponse.length} avaliable location that match your criteria`);
log.info(`Found ${filteredResponse.length} Available location that match your criteria`);
log.info(`${filteredResponse.map(el => el.Name).join(', ')}`);
this.avaliableLocation = filteredResponse;
this.availableLocation = filteredResponse;
return;
}

private async getLocationDatesAll() {
log.info('Checking Avaliable Location Dates....');
if (!this.avaliableLocation) return;
const getLocationFunctions = this.avaliableLocation.map(location => () => this.getLocationDates(location));
log.info('Checking Available Location Dates....');
if (!this.availableLocation) return;
const getLocationFunctions = this.availableLocation.map(location => () => this.getLocationDates(location));
for (;;) {
console.log('--------------------------------------------------------------------------------');
await this.queue.addAll(getLocationFunctions).catch(() => null);
await sleep.setTimeout(this.config.appSettings.interval);
}
}

private async getLocationDates(location: AvaliableLocationResponse) {
private async getLocationDates(location: AvailableLocationResponse) {
const locationConfig = this.config.location;
const requestBody: AvaliableLocationDatesPayload = {
const requestBody: AvailableLocationDatesPayload = {
LocationId: location.Id,
PreferredDay: 0,
SameDay: locationConfig.sameDay,
StartDate: null,
TypeId: this.config.personalInfo.typeId || 71,
};
const response = (await this.requestApi('/api/AvailableLocationDates', 'POST', requestBody).then(res => res.body.json())) as AvaliableLocationDatesResponse;
let avaliableDates = response.LocationAvailabilityDates;
const response = (await this.requestApi('/api/AvailableLocationDates', 'POST', requestBody).then(res => res.body.json())) as AvailableLocationDatesResponse;
let AvailableDates = response.LocationAvailabilityDates;

if (!locationConfig.sameDay) {
avaliableDates = response.LocationAvailabilityDates.filter(date => {
AvailableDates = response.LocationAvailabilityDates.filter(date => {
const AvailabilityDate = dayjs(date.AvailabilityDate);
const today = dayjs();
const startDate = dayjs(this.config.location.daysAround.startDate);
let preferredDaysCondition = true;
if (locationConfig.preferredDays.length > 0) preferredDaysCondition = locationConfig.preferredDays.includes(AvailabilityDate.day());
return (
AvailabilityDate.isBetween(today.add(locationConfig.daysAround.start, 'day'), today.add(locationConfig.daysAround.end, 'day'), 'day') &&
AvailabilityDate.isBetween(startDate.add(locationConfig.daysAround.start, 'day'), startDate.add(locationConfig.daysAround.end, 'day'), 'day') &&
date.AvailableTimeSlots.length > 0 &&
preferredDaysCondition
);
});
}

if (avaliableDates.length !== 0) {
const filteredAvailabilityDates = avaliableDates
.map(date => {
const filteredTimeSlots = date.AvailableTimeSlots.filter(timeSlot => {
const startDateTime = dayjs(timeSlot.StartDateTime);
const startHour = startDateTime.hour();
return startHour >= this.config.location.timesAround.start && startHour < this.config.location.timesAround.end;
});
return {
...date,
AvailableTimeSlots: filteredTimeSlots,
};
})
.filter(date => date.AvailableTimeSlots.length > 0);
if (AvailableDates.length !== 0) {
const filteredAvailabilityDates = AvailableDates.map(date => {
const filteredTimeSlots = date.AvailableTimeSlots.filter(timeSlot => {
const startDateTime = dayjs(timeSlot.StartDateTime);
const startHour = startDateTime.hour();
return startHour >= this.config.location.timesAround.start && startHour < this.config.location.timesAround.end;
});
return {
...date,
AvailableTimeSlots: filteredTimeSlots,
};
}).filter(date => date.AvailableTimeSlots.length > 0);

const booking = filteredAvailabilityDates[0].AvailableTimeSlots[0];

log.info(`${location.Name} is avaliable on ${booking.FormattedStartDateTime}`);
log.info(`${location.Name} is Available on ${booking.FormattedStartDateTime}`);
if (!this.queue.isPaused) this.queue.pause();
if (!this.config.appSettings.cancelIfExist && this.existBooking?.exist) {
log.warn('cancelIfExist is disabled! Please cancel existing appointment manually!');
Expand All @@ -217,7 +214,7 @@ class TexasScheduler {
return Promise.resolve(true);
}
log.info(
`${location.Name} is not avaliable in ${
`${location.Name} is not Available in ${
locationConfig.sameDay ? 'the same day' : `around ${locationConfig.daysAround.start}-${locationConfig.daysAround.end} days from today! `
} `,
);
Expand Down Expand Up @@ -249,7 +246,7 @@ class TexasScheduler {
return response;
}

private async holdSlot(booking: AvaliableTimeSlots, location: AvaliableLocationResponse) {
private async holdSlot(booking: AvailableTimeSlots, location: AvailableLocationResponse) {
if (this.isHolded) return;
const requestBody: HoldSlotPayload = {
DateOfBirth: this.config.personalInfo.dob,
Expand All @@ -269,7 +266,7 @@ class TexasScheduler {
await this.bookSlot(booking, location);
}

private async bookSlot(booking: AvaliableTimeSlots, location: AvaliableLocationResponse) {
private async bookSlot(booking: AvailableTimeSlots, location: AvailableLocationResponse) {
if (this.isBooked) return;
log.info('Booking slot....');
if (this.existBooking?.exist) {
Expand Down Expand Up @@ -310,46 +307,13 @@ class TexasScheduler {
log.info(`Slot booked successfully. Confirmation Number: ${bookingInfo.Booking.ConfirmationNumber}`);
log.info(`Visiting this link to print your booking:`);
log.info(appointmentURL);
if (this.config.webhook.enable)
await this.sendWebhook(
// this string kinda long so i put it in a array and join it :)
[
`Booking for ${this.config.personalInfo.firstName} ${this.config.personalInfo.lastName} has been booked.`,
`Confirmation Number: ${bookingInfo.Booking.ConfirmationNumber}`,
`Location: ${location.Name} DPS`,
`Time: ${booking.FormattedStartDateTime}`,
`Appointment URL: ${appointmentURL}`,
].join('\n'),
);
process.exit(0);
} else {
if (this.queue.isPaused) this.queue.start();
log.error('Failed to book slot');
log.error(await response.body.text());
}
}

private async sendWebhook(message: string) {
const requestBody: webhookPayload = {
chatGuid: `${this.config.webhook.phoneNumberType};-;${this.config.webhook.phoneNumber}`,
tempGuild: '',
message,
method: this.config.webhook.sendMethod,
subject: '',
effectId: '',
selectedMessageGuild: '',
};
const response = await undici.request(`${this.config.webhook.url}/api/v1/message/text?password=${this.config.webhook.password}`, {
method: 'POST',
body: JSON.stringify(requestBody),
headers: { 'Content-Type': 'application/json' },
});
if (response.statusCode === 200) log.info('[INFO] Webhook sent successfully');
else {
log.error('Failed to send webhook');
log.error(await response.body.text());
}
}
}

export default TexasScheduler;
7 changes: 7 additions & 0 deletions src/Config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { configZod, Config } from '../Interfaces/Config';
import preferredDayList from '../Assets/preferredDay';
import * as log from '../Log';
import 'dotenv/config';
import dayjs from 'dayjs';

const parseConfig = (): Config => {
if (!existsSync('./config.yml')) {
Expand All @@ -16,6 +17,12 @@ const parseConfig = (): Config => {
configData = parsePersonalInfo(configData);
configData.location.preferredDays = parsePreferredDays(configData.location.preferredDays);
configData.personalInfo.phoneNumber = parsePhoneNumber(configData.personalInfo.phoneNumber);
let startDate = dayjs(configData.location.daysAround.startDate);
if (!configData.location.daysAround.startDate || !startDate.isValid() || startDate.isBefore(dayjs())) {
log.warn('Invalid date in config.yml, using current date');
startDate = dayjs();
}
configData.location.daysAround.startDate = startDate.format('MM/DD/YYYY');

try {
return configZod.parse(configData);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// This type request to AvaliableLocation endpoints and check avaliable locations
export interface AvaliableLocationPayload {
// This type request to AvailableLocation endpoints and check available locations
export interface AvailableLocationPayload {
CityName: string;
PreferredDay: number;
TypeId: number;
ZipCode: string;
}

// The response is more than this but i only use stuff I needed
export interface AvaliableLocationResponse {
export interface AvailableLocationResponse {
Id: number;
Address: string;
Distance: number;
Expand Down
Loading

0 comments on commit be1e867

Please sign in to comment.