diff --git a/DESIGN.md b/DESIGN.md index b57f4b5..49d4885 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -35,7 +35,7 @@ A Slack bot that allows users to track their flights and automatically posts upd ## Data Models -### Usere airport coordinate data to determine proxim +### User - `id`: Unique identifier - `slack_user_id`: Slack user ID - `slack_channel_id`: User's personal channel ID diff --git a/prisma/migrations/20250714144126_flight_init/migration.sql b/prisma/migrations/20250714144126_flight_init/migration.sql new file mode 100644 index 0000000..5f98a8d --- /dev/null +++ b/prisma/migrations/20250714144126_flight_init/migration.sql @@ -0,0 +1,55 @@ +-- CreateTable +CREATE TABLE "Flight" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "faFlightId" TEXT NOT NULL, + "ident" TEXT NOT NULL, + "identIcao" TEXT NOT NULL, + "identIata" TEXT, + "registration" TEXT, + "aircraftType" TEXT, + "originCode" TEXT NOT NULL, + "originIata" TEXT NOT NULL, + "originName" TEXT NOT NULL, + "originCity" TEXT NOT NULL, + "destinationCode" TEXT NOT NULL, + "destinationIata" TEXT NOT NULL, + "destinationName" TEXT NOT NULL, + "destinationCity" TEXT NOT NULL, + "scheduledOut" DATETIME NOT NULL, + "scheduledOff" DATETIME NOT NULL, + "scheduledOn" DATETIME NOT NULL, + "scheduledIn" DATETIME NOT NULL, + "estimatedOut" DATETIME, + "estimatedOff" DATETIME, + "estimatedOn" DATETIME, + "estimatedIn" DATETIME, + "actualOut" DATETIME, + "actualOff" DATETIME, + "actualOn" DATETIME, + "actualIn" DATETIME, + "status" TEXT NOT NULL, + "departureDelay" INTEGER, + "arrivalDelay" INTEGER, + "cancelled" BOOLEAN NOT NULL DEFAULT false, + "diverted" BOOLEAN NOT NULL DEFAULT false, + "progressPercent" INTEGER, + "gateOrigin" TEXT, + "gateDestination" TEXT, + "terminalOrigin" TEXT, + "terminalDestination" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Flight_faFlightId_key" ON "Flight"("faFlightId"); + +-- CreateIndex +CREATE INDEX "Flight_userId_idx" ON "Flight"("userId"); + +-- CreateIndex +CREATE INDEX "Flight_faFlightId_idx" ON "Flight"("faFlightId"); + +-- CreateIndex +CREATE INDEX "Flight_scheduledOff_idx" ON "Flight"("scheduledOff"); diff --git a/prisma/migrations/20250714180710_other_flight_schema_stuff/migration.sql b/prisma/migrations/20250714180710_other_flight_schema_stuff/migration.sql new file mode 100644 index 0000000..be0c456 --- /dev/null +++ b/prisma/migrations/20250714180710_other_flight_schema_stuff/migration.sql @@ -0,0 +1,63 @@ +/* + Warnings: + + - You are about to drop the column `destinationCode` on the `Flight` table. All the data in the column will be lost. + - You are about to drop the column `originCode` on the `Flight` table. All the data in the column will be lost. + - Added the required column `destinationIcao` to the `Flight` table without a default value. This is not possible if the table is not empty. + - Added the required column `originIcao` to the `Flight` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Flight" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "faFlightId" TEXT NOT NULL, + "ident" TEXT NOT NULL, + "identIcao" TEXT NOT NULL, + "identIata" TEXT, + "registration" TEXT, + "aircraftType" TEXT, + "originIcao" TEXT NOT NULL, + "originIata" TEXT NOT NULL, + "originName" TEXT NOT NULL, + "originCity" TEXT NOT NULL, + "destinationIcao" TEXT NOT NULL, + "destinationIata" TEXT NOT NULL, + "destinationName" TEXT NOT NULL, + "destinationCity" TEXT NOT NULL, + "scheduledOut" DATETIME NOT NULL, + "scheduledOff" DATETIME NOT NULL, + "scheduledOn" DATETIME NOT NULL, + "scheduledIn" DATETIME NOT NULL, + "estimatedOut" DATETIME, + "estimatedOff" DATETIME, + "estimatedOn" DATETIME, + "estimatedIn" DATETIME, + "actualOut" DATETIME, + "actualOff" DATETIME, + "actualOn" DATETIME, + "actualIn" DATETIME, + "status" TEXT NOT NULL, + "departureDelay" INTEGER, + "arrivalDelay" INTEGER, + "cancelled" BOOLEAN NOT NULL DEFAULT false, + "diverted" BOOLEAN NOT NULL DEFAULT false, + "progressPercent" INTEGER, + "gateOrigin" TEXT, + "gateDestination" TEXT, + "terminalOrigin" TEXT, + "terminalDestination" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Flight" ("actualIn", "actualOff", "actualOn", "actualOut", "aircraftType", "arrivalDelay", "cancelled", "createdAt", "departureDelay", "destinationCity", "destinationIata", "destinationName", "diverted", "estimatedIn", "estimatedOff", "estimatedOn", "estimatedOut", "faFlightId", "gateDestination", "gateOrigin", "id", "ident", "identIata", "identIcao", "originCity", "originIata", "originName", "progressPercent", "registration", "scheduledIn", "scheduledOff", "scheduledOn", "scheduledOut", "status", "terminalDestination", "terminalOrigin", "updatedAt", "userId") SELECT "actualIn", "actualOff", "actualOn", "actualOut", "aircraftType", "arrivalDelay", "cancelled", "createdAt", "departureDelay", "destinationCity", "destinationIata", "destinationName", "diverted", "estimatedIn", "estimatedOff", "estimatedOn", "estimatedOut", "faFlightId", "gateDestination", "gateOrigin", "id", "ident", "identIata", "identIcao", "originCity", "originIata", "originName", "progressPercent", "registration", "scheduledIn", "scheduledOff", "scheduledOn", "scheduledOut", "status", "terminalDestination", "terminalOrigin", "updatedAt", "userId" FROM "Flight"; +DROP TABLE "Flight"; +ALTER TABLE "new_Flight" RENAME TO "Flight"; +CREATE UNIQUE INDEX "Flight_faFlightId_key" ON "Flight"("faFlightId"); +CREATE INDEX "Flight_userId_idx" ON "Flight"("userId"); +CREATE INDEX "Flight_faFlightId_idx" ON "Flight"("faFlightId"); +CREATE INDEX "Flight_scheduledOff_idx" ON "Flight"("scheduledOff"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e002054..d360dc7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,3 +9,67 @@ datasource db { provider = "sqlite" url = env("DATABASE_URL") } + +model Flight { + id String @id @default(cuid()) + userId String + + // identifiers + faFlightId String @unique // fa_flight_id + ident String // flight number + identIcao String // ICAO ident + identIata String? // IATA ident (can be null) + + // aircraft info + registration String? // tail number + aircraftType String? // e.g., "A320" + + // route info + originIcao String // icao code (lemg) + originIata String // iata code (agp) + originName String // airport name + originCity String // city name + + destinationIcao String // icao code + destinationIata String // iata code + destinationName String // airport name + destinationCity String // city name + + // timing (stored in iso 8601 format on the api) + scheduledOut DateTime + scheduledOff DateTime // takeoff time + scheduledOn DateTime // landing time + scheduledIn DateTime + + estimatedOut DateTime? + estimatedOff DateTime? + estimatedOn DateTime? + estimatedIn DateTime? + + actualOut DateTime? + actualOff DateTime? + actualOn DateTime? + actualIn DateTime? + + // status and delays + status String // "Scheduled", "Delayed", etc. + departureDelay Int? // minutes + arrivalDelay Int? // minutes + cancelled Boolean @default(false) + diverted Boolean @default(false) + + // more useful info + progressPercent Int? // 0-100 + gateOrigin String? + gateDestination String? + terminalOrigin String? + terminalDestination String? + + // metadata + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@index([faFlightId]) + @@index([scheduledOff]) +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d89705a..2ec3bc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ export const flightAware = new FlightAware(); export const adsbDb = new AdsBDB(); app.command('/flight-add', async ({ command, ack, respond }) => { - const EXAMPLE = `_Example: \`/flight-add AGP today 11\` looks for flights from AGP today at 11:00 (24-hour)._`; + const EXAMPLE = `_Example: \`/flight-add AGP today 11\` looks for flights from AGP today at 11:00 (24-hour UTC)._`; await ack(); const parts = command.text.split(' '); @@ -71,7 +71,7 @@ app.command('/flight-add', async ({ command, ack, respond }) => { const flights = response.scheduled_departures; if (flights.length === 0) { - const timeInfo = hour !== undefined ? ` at hour ${hour}:00` : ''; + const timeInfo = hour !== undefined ? ` at hour ${hour}:00 UTC` : ''; await respond({ text: `No flights found for ${airportCode} on ${formatDate(new Date(date!.begin * 1000))}${timeInfo}`, }); @@ -126,7 +126,7 @@ async function showFlightPage( type: 'plain_text' as const, text: displayText.substring(0, 75), }, - value: flight.ident_icao, + value: flight.fa_flight_id, }; }); @@ -177,7 +177,7 @@ async function showFlightPage( } await respond({ - text: `Found flights for ${requestParams.airportCode} on ${formatDate( + text: `Found ${flights.length} flights for ${requestParams.airportCode} on ${formatDate( new Date(requestParams.originalDate * 1000) )}`, blocks, @@ -201,10 +201,58 @@ app.action('flight_selection', async ({ body, ack, respond }) => { const callsign = selectedValue; + const flight = await flightAware.getFlightInfo(callsign); + const flightData = flight.flights[0]!; + await db.flight.create({ + data: { + userId: body.user.id, + faFlightId: flightData.fa_flight_id, + ident: flightData.ident, + identIcao: flightData.ident_icao, + identIata: flightData.ident_iata, + registration: flightData.registration, + aircraftType: flightData.aircraft_type, + originIcao: flightData.origin.code_icao, + originIata: flightData.origin.code_iata, + originName: flightData.origin.name, + originCity: flightData.origin.city, + destinationIcao: flightData.destination.code_icao, + destinationIata: flightData.destination.code_iata, + destinationName: flightData.destination.name, + destinationCity: flightData.destination.city, + scheduledOut: flightData.scheduled_out, + scheduledOff: flightData.scheduled_off, + scheduledOn: flightData.scheduled_on, + scheduledIn: flightData.scheduled_in, + estimatedOut: flightData.estimated_out, + estimatedOff: flightData.estimated_off, + estimatedOn: flightData.estimated_on, + estimatedIn: flightData.estimated_in, + actualOut: flightData.actual_out, + actualOff: flightData.actual_off, + actualOn: flightData.actual_on, + actualIn: flightData.actual_in, + status: flightData.status, + departureDelay: flightData.departure_delay, + arrivalDelay: flightData.arrival_delay, + cancelled: flightData.cancelled, + diverted: flightData.diverted, + progressPercent: flightData.progress_percent, + gateOrigin: flightData.gate_origin, + gateDestination: flightData.gate_destination, + terminalOrigin: flightData.terminal_origin, + terminalDestination: flightData.terminal_destination, + } + }); + await respond({ - text: `✅ Added flight ${callsign} to your tracking list!`, + text: `✅ Now tracking \`${callsign}\` in this channel!`, replace_original: true, }); + await app.client.chat.postMessage({ + channel: body.user.id, + text: `Flight \`${callsign}\` is now being tracked inside this channel!`, + }); } catch (error) { console.error('Error handling flight selection:', error); await respond({ text: 'Error adding flight to tracking. Please try again.' }); diff --git a/src/util/flightAware.ts b/src/util/flightAware.ts index 3cc002d..03e1444 100644 --- a/src/util/flightAware.ts +++ b/src/util/flightAware.ts @@ -46,6 +46,21 @@ export class FlightAware { } }).json(); } + public async getFlightInfo(flightId: string): Promise { + const apiKey = process.env.FLIGHTAWARE; + if (!apiKey) { + throw new Error('FLIGHTAWARE environment variable is required'); + } + + const url = `https://aeroapi.flightaware.com/aeroapi/flights/${flightId}`; + return ky.get(url, { + headers: { + 'Accept': 'application/json; charset=UTF-8', + 'User-Agent': 'flight-slack/1.0', + 'x-apikey': apiKey, + } + }).json(); + } } interface Airport { @@ -120,4 +135,84 @@ export interface ScheduledDeparturesResponse { next?: string; } | null; num_pages: number; +} + +export interface Flight { + ident: string; + ident_icao: string; + ident_iata: string; + actual_runway_off: string; + actual_runway_on: string; + fa_flight_id: string; + operator: string; + operator_icao: string; + operator_iata: string; + flight_number: string; + registration: string; + atc_ident: string; + inbound_fa_flight_id: string; + codeshares: any[]; + codeshares_iata: any[]; + blocked: boolean; + diverted: boolean; + cancelled: boolean; + position_only: boolean; + origin: { + code: string; + code_icao: string; + code_iata: string; + code_lid: null; + timezone: string; + name: string; + city: string; + airport_info_url: string; + }; + destination: { + code: string; + code_icao: string; + code_iata: string; + code_lid: null; + timezone: string; + name: string; + city: string; + airport_info_url: string; + }; + departure_delay: number; + arrival_delay: number; + filed_ete: number; + foresight_predictions_available: boolean; + scheduled_out: string; + estimated_out: string; + actual_out: string; + scheduled_off: string; + estimated_off: string; + actual_off: string; + scheduled_on: string; + estimated_on: string; + actual_on: string; + scheduled_in: string; + estimated_in: string; + actual_in: null; + progress_percent: number; + status: string; + aircraft_type: string; + route_distance: number; + filed_airspeed: number; + filed_altitude: null; + route: null; + baggage_claim: null; + seats_cabin_business: null; + seats_cabin_coach: null; + seats_cabin_first: null; + gate_origin: string; + gate_destination: null; + terminal_origin: null; + terminal_destination: string; + type: string; +} + +export interface FlightsData { + flights: Flight[]; + links: null; + num_pages: number; } \ No newline at end of file