hubot-nextbus
Version:
Shows when the next transit vehicle will arrive at a particular stop.
183 lines (160 loc) • 6.03 kB
JavaScript
// Description:
// Get the next bus for a particular stop
//
// Configuration:
// HUBOT_NEXTBUS_BASE_URL - URL of a `gtfs-rails-api` instance
// HUBOT_NEXTBUS_LAT_LON - Default location for stop search
// HUBOT_NEXTBUS_STOP_ID - Default stop for `hubot nextbus`
//
// Commands:
// hubot nextbus
// hubot nextbus stops
// hubot nextbus stop <stop-identifier>
//
// Author:
// stephenyeargin
const moment = require('moment');
const AsciiTable = require('ascii-table');
module.exports = (robot) => {
const baseURL = process.env.HUBOT_NEXTBUS_BASE_URL || 'https://gtfs.transitnownash.org';
const latlon = process.env.HUBOT_NEXTBUS_LAT_LON;
const defaultStopId = process.env.HUBOT_NEXTBUS_STOP_ID;
const getAPIResponse = (path, msg, cb) => {
const url = `${baseURL}/${path}`;
robot.logger.debug(url);
robot.http(url)
.get()((err, res, body) => {
const response = JSON.parse(body);
if (err) {
msg.send(err);
return;
}
if (response.error) {
msg.send(response.error);
return;
}
if (!body) {
msg.send('No data returned.');
return;
}
cb(response);
});
};
const formatTripTimeAsMoment = (timeStr) => {
if (/^2[4-9]:/.test(timeStr)) {
// eslint-disable-next-line no-param-reassign
timeStr = (parseInt(timeStr.substr(0, 2), 10) - 24) + timeStr.substr(2, 8);
return moment(`${moment().format('YYYY-MM-DD')} ${timeStr.padStart(8, '0')}`).add(1, 'days');
}
return moment(`${moment().format('YYYY-MM-DD')} ${timeStr.trim().padStart(8, '0')}`);
};
const getRealTimeStatus = (stopTime) => {
// Check if realtime data is available
if (!stopTime.realtime || !stopTime.realtime.departure) {
return '';
}
const scheduled = formatTripTimeAsMoment(stopTime.departure_time);
const actual = formatTripTimeAsMoment(stopTime.realtime.departure);
const diffMinutes = actual.diff(scheduled, 'minutes');
if (diffMinutes === 0) {
return 'On time';
} if (diffMinutes > 0) {
return `${diffMinutes}m late`;
}
return `${Math.abs(diffMinutes)}m early`;
};
const formatAlerts = (alerts) => {
if (!alerts || alerts.length === 0) {
return '';
}
const alertLines = alerts.map((alert) => {
const headerText = alert.header_text?.translation?.[0]?.text || 'Service Alert';
return `⚠️ *${headerText.trim()}*`;
});
return alertLines.join('\n');
};
const queryStopById = (stopId, msg) => getAPIResponse('agencies.json', msg, (agencies) => {
// Override timezone for moment() calls
process.env.TZ = agencies.data[0].agency_timezone;
robot.logger.debug(process.env.TZ);
robot.logger.debug('Current Time:', moment());
getAPIResponse(`stops/${stopId}/next.json`, msg, (response) => {
const {
stop,
next_trip: nextTrip,
upcoming_trips: upcomingTrips,
alerts,
vehicle_positions: vehiclePositions,
} = response;
// Show alerts if any exist
const alertMessage = formatAlerts(alerts);
if (alertMessage) {
msg.send(alertMessage);
}
// Combine next trip with upcoming trips to get all trips
const allTrips = nextTrip ? [nextTrip, ...upcomingTrips] : upcomingTrips;
const nextTripsData = allTrips.filter((tripData) => {
const tripTime = formatTripTimeAsMoment(tripData.stop_time.arrival_time);
return tripTime.isAfter(moment(), 'second');
});
robot.logger.debug(nextTripsData);
if (nextTripsData.length === 0) {
msg.send('The last bus has already run for today.');
return;
}
const table = new AsciiTable();
nextTripsData.slice(0, 5).forEach((tripData) => {
const tripTime = formatTripTimeAsMoment(tripData.stop_time.arrival_time);
const realtimeStatus = getRealTimeStatus(tripData.stop_time);
const hasVehicle = vehiclePositions
&& vehiclePositions.some((vp) => vp.trip && vp.trip.trip_id === tripData.trip.trip_gid);
const busIndicator = hasVehicle ? ' 🚌' : '';
const timeUntilText = realtimeStatus ? `${tripTime.fromNow()} (${realtimeStatus})` : tripTime.fromNow();
const columns = [
formatTripTimeAsMoment(tripData.stop_time.arrival_time).format('LT'),
`#${tripData.trip.route_gid} - ${tripData.trip.trip_headsign}${busIndicator}`,
timeUntilText,
];
table.addRow(columns);
});
const adapterName = robot.adapterName ?? robot.adapter?.name ?? '';
table.removeBorder();
const tableOutput = table.toString().split('\n').map((line) => line.trimEnd()).join('\n');
const heading = `🚏 *${stop.stop_name}*`;
if (/slack/i.test(adapterName)) {
msg.send(`${heading}\n\`\`\`\n${tableOutput}\n\`\`\``);
return;
}
msg.send(`${heading}\n${tableOutput}`);
});
});
// query the default stop ID or location's closest bus stop
robot.respond(/(?:bus|nextbus)(?: me)?$/i, (msg) => {
if (defaultStopId) {
queryStopById(defaultStopId, msg);
return;
}
getAPIResponse(`stops/near/${latlon}/1000.json?per_page=5`, msg, (stops) => {
if (stops.total > 0) {
queryStopById(stops.data[0].stop_gid, msg);
return;
}
msg.send(`No stops found near ${latlon}`);
});
});
// get a list of nearby stops
robot.respond(/(?:bus|nextbus) stops$/i, (msg) => getAPIResponse(`stops/near/${latlon}/1000.json?per_page=5`, msg, (stops) => {
msg.send('List of nearby stops:');
const output = [];
stops.data.forEach((stop) => {
output.push(`- [${stop.stop_gid}] ${stop.stop_name}`);
});
msg.send(output.join('\n'));
}));
// get a particular stop's next bus
robot.respond(/(?:bus|nextbus) stop ([A-Z0-9_]+)$/i, (msg) => {
const stopId = msg.match[1];
robot.logger.debug(stopId);
queryStopById(stopId, msg);
});
};