Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
working-directory: ${{ github.workspace }}
- name: Test Suite
run: |
export DOCUMENTED=1
export MBUS_URL=https://mbus.bustime.mock.mb.thething.fyi/
export RIDE_URL=https://ride.bustime.mock.mb.thething.fyi/
npm start &
Expand Down Expand Up @@ -58,7 +59,7 @@ jobs:
- name: Build Docs
run: npx typedoc --entryPointStrategy expand ./src --treatWarningsAsErrors
working-directory: ${{ github.workspace }}
- name: Sync files
- name: Sync Files
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.FTP_SERVER }}
Expand All @@ -67,3 +68,30 @@ jobs:
password: ${{ secrets.FTP_PASSWORD }}
local-dir: ${{ github.workspace }}/docs/
server-dir: ${{ github.ref }}/typedoc/

OpenAPI:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v6
- name: NPM Install
run: npm i
working-directory: ${{ github.workspace }}
- name: Build Spec
run: |
export DOCUMENTED=1
export DOCUMENTED_OUTPUT_FILE=openapi/spec.json
export DOCUMENTED_EXIT_ON_OUTPUT=1
mkdir openapi
npm start
working-directory: ${{ github.workspace }}
- name: Sync Files
uses: SamKirkland/FTP-Deploy-Action@v4.4.0
with:
server: ${{ secrets.FTP_SERVER }}
port: ${{ secrets.FTP_PORT }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: ${{ github.workspace }}/openapi/
server-dir: ${{ github.ref }}/openapi/

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package-lock.json
.env
.vscode/
src/assets/walkingCache.json
/docs/
/docs/
*.log
9 changes: 5 additions & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import express from "express";

import mbus from "./routes/api"
import { addRouter, dumpReflectionInfo, reflection } from "./routes/helper";
import * as documented from "./routes/documented";

const app = express();

app.use(express.json());
addRouter(app, "/mbus/api/v3", mbus);
documented.addRouter(documented.globalContext, app, "/mbus/api/v3", mbus);
app.use("/docs", express.static("docs"));

const PORT = process.env.PORT || 3000;


app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
if (reflection)
dumpReflectionInfo();
if (documented.ENABLED) {
documented.outputDocsFor(documented.globalContext);
}
});
112 changes: 52 additions & 60 deletions src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as journeyService from '../services/journey';
import * as reminderService from '../services/reminder';
import * as graphBuilder from '../services/graphBuilder';
import { startBackgroundJobs } from '../jobs';
import { addGetRoute, makeSuccessResponse } from "./helper";
import * as documented from "./documented";

/**
* Express router for the MBus API v3.
Expand Down Expand Up @@ -365,41 +365,44 @@ export function getNearestStops(req: express.Request, res: express.Response) {
}
router.get('/nearest-stops', getNearestStops);

/**
* Plans a journey between origin and destination coordinates.
* @param req - Express request
* @param res - Express response
* @returns JSON object with `journeys` array containing possible routes.
*/
export async function planJourney(req: express.Request, res: express.Response) {
try {
const { originLat, originLon, destLat, destLon, walkingPenalty, range } = req.query;

if (!originLat || !originLon || !destLat || !destLon) {
res.status(400).json({ error: 'coordinates are required' });
return;
documented.addGetRoute(
documented.globalContext, router, '/plan-journey',
{
...documented.emptyFormat,
query: z.object({
originLat: z.coerce.number(),
originLon: z.coerce.number(),
destLat: z.coerce.number(),
destLon: z.coerce.number(),
walkingPenalty: z.optional(z.string())
.transform((x) => x === undefined ? undefined : parseFloat(x))
.pipe(z.optional(z.number())),
range: z.optional(z.string())
.transform((x) => x === undefined ? undefined : parseInt(x))
.pipe(z.optional(z.number())),
}),
resBody: z.any(),
},
async (_, { originLat, originLon, destLat, destLon, walkingPenalty, range }) => {
try {
const now = new Date();
const secondsSinceMidnight = now.getUTCHours() * 3600 + now.getUTCMinutes() * 60 + now.getUTCSeconds();

const results = await journeyService.planJourney(
originLat, originLon,
destLat, destLon,
secondsSinceMidnight,
{ walkingPenalty, range },
);

return documented.makeSuccessResponse({ journeys: results });
} catch (error) {
console.error("Journey plan error:", error);
return documented.makeFailureResponse(500, 'Journey planning failed');
}

const now = new Date();
const secondsSinceMidnight = now.getUTCHours() * 3600 + now.getUTCMinutes() * 60 + now.getUTCSeconds();

const results = await journeyService.planJourney(
parseFloat(originLat as string), parseFloat(originLon as string),
parseFloat(destLat as string), parseFloat(destLon as string),
secondsSinceMidnight,
{
walkingPenalty: walkingPenalty ? parseFloat(walkingPenalty as string) : undefined,
range: range ? parseInt(range as string) : undefined
}
);

res.json({ journeys: results });
} catch (error) {
console.error("Journey plan error:", error);
res.status(500).json({ error: 'Journey planning failed' });
}
}
router.get('/plan-journey', planJourney);
},
{ description: 'Plans a journey between origin and destination coordinates.' },
);

/**
* Saves the current graph state to a file (DEV mode only).
Expand Down Expand Up @@ -488,22 +491,12 @@ router.get('/get-key-stops', getKeyStops);
// Notifications / Reminders

const SetReminderBody = z.object({ token: z.string(), stpid: z.string(), rtid: z.string(), thresh: z.number() });
/**
* @param req - Express request, expects `SetReminderBody` in the body
* @param res - Express response, error message as string if error occurs
*/
export function setReminder(req: express.Request, res: express.Response) {
const result = SetReminderBody.safeParse(req.body);
if (!result.success) {
res.status(400);
res.send(result.error.message);
} else {
const { token, stpid, rtid, thresh } = result.data;
documented.addPostRoute(
documented.globalContext, router, '/setReminder', { ...documented.emptyFormat, reqBody: SetReminderBody },
async (_, __, { token, stpid, rtid, thresh }) => {
const info = reminderService.infoToUseForRoute(rtid);
if (info === null) {
res.status(400);
res.send(`Invalid route ${rtid}`);
return;
return documented.makeFailureResponse(400, `Invalid route ${rtid}`);
}
const { reminderSubscriptions, predsByStopId } = info;
reminderSubscriptions.add(
Expand All @@ -513,11 +506,9 @@ export function setReminder(req: express.Request, res: express.Response) {
predsByStopId,
Date.now(),
);
res.sendStatus(200);
return documented.makeSuccessResponse({});
}

}
router.post('/setReminder', setReminder);
);

const UnsetReminderBody = z.object({ token: z.string(), stpid: z.string(), rtid: z.string() });
/**
Expand Down Expand Up @@ -568,22 +559,23 @@ export function swapToken(req: express.Request, res: express.Response) {
}
router.post('/swapToken', swapToken);

const Token = z.string().transform(reminderService.registrationToken).meta({ id: "Token" })
const Token = z.string().meta({ id: "Token" })
const ActiveReminder = z.object({
stpid: z.string(),
rtid: z.string(),
thresh: z.number().nullable(),
eta: z.number().nullable(),
}).meta({ id: "Reminder" });

addGetRoute(
router, '/activeReminders/:token',
documented.addGetRoute(
documented.globalContext, router, '/activeReminders/:token',
{
params: z.object({ token: Token }),
query: z.object(),
resBody: z.object({ reminders: z.array(ActiveReminder) }),
},
({ token }, _) => {
async ({ token }, _) => {
const regTok = reminderService.registrationToken(token);
const subscriptionInfo = (r: reminderService.PreThreshold | reminderService.PostThreshold) => {
return {
stpid: r.event.stpid,
Expand All @@ -595,13 +587,13 @@ addGetRoute(
console.log(`Got request for active reminders of ${token}`);
const universityReminders = reminderService
.universityReminderSubscriptions
.activeRemindersFor(token)
.activeRemindersFor(regTok)
.map(subscriptionInfo);
const rideReminders = reminderService
.rideReminderSubscriptions
.activeRemindersFor(token)
.activeRemindersFor(regTok)
.map(subscriptionInfo);
return makeSuccessResponse(200, { reminders: universityReminders.concat(rideReminders) });
return documented.makeSuccessResponse({ reminders: universityReminders.concat(rideReminders) });
},
{
summary: "active reminders",
Expand Down
Loading