hubot-nextbus 2.2.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +8 -0
- package/.github/workflows/nodejs.yml +1 -2
- package/.github/workflows/npm-publish.yml +32 -7
- package/package.json +10 -9
- package/script/test +1 -1
- package/src/nextbus.js +106 -19
- package/test/adapters/slack.js +5 -0
- package/test/fixtures/agencies.json +3 -3
- package/test/fixtures/stops-BRO12WN-next.json +1811 -0
- package/test/fixtures/stops-PORGRESF-next.json +1908 -0
- package/test/fixtures/stops-near-gps.json +51 -51
- package/test/helpers/hubot-helper.js +77 -0
- package/test/nextbus-slack.test.js +132 -0
- package/test/nextbus.test.js +178 -0
- package/vitest.config.mjs +14 -0
- package/test/fixtures/stops-CHA7AWN-trips.json +0 -277720
- package/test/nextbus_test.js +0 -215
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
on:
|
|
2
2
|
push:
|
|
3
|
-
branches:
|
|
3
|
+
branches:
|
|
4
|
+
- main
|
|
4
5
|
|
|
5
6
|
name: npm-publish
|
|
6
7
|
|
|
7
8
|
permissions:
|
|
8
9
|
contents: write
|
|
10
|
+
id-token: write
|
|
9
11
|
|
|
10
12
|
jobs:
|
|
11
13
|
publish:
|
|
12
14
|
runs-on: ubuntu-latest
|
|
13
15
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
15
|
-
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v6
|
|
16
19
|
with:
|
|
17
|
-
node-version: "
|
|
20
|
+
node-version: "24"
|
|
21
|
+
registry-url: https://registry.npmjs.org
|
|
22
|
+
|
|
23
|
+
- name: Update npm to latest version
|
|
24
|
+
run: npm install -g npm@latest
|
|
25
|
+
|
|
18
26
|
- run: npm ci
|
|
19
27
|
- run: npm test
|
|
20
|
-
|
|
28
|
+
|
|
29
|
+
- name: Publish to npm
|
|
21
30
|
id: publish
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
run: |
|
|
32
|
+
# Verify npm version supports OIDC
|
|
33
|
+
echo "npm version: $(npm --version)"
|
|
34
|
+
|
|
35
|
+
# Check if version is already published
|
|
36
|
+
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
37
|
+
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
38
|
+
|
|
39
|
+
if npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version 2>/dev/null; then
|
|
40
|
+
echo "Version $PACKAGE_VERSION already published"
|
|
41
|
+
echo "type=" >> $GITHUB_OUTPUT
|
|
42
|
+
else
|
|
43
|
+
echo "Publishing $PACKAGE_NAME@$PACKAGE_VERSION..."
|
|
44
|
+
npm publish --access public
|
|
45
|
+
echo "type=patch" >> $GITHUB_OUTPUT
|
|
46
|
+
echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
|
|
47
|
+
fi
|
|
48
|
+
|
|
24
49
|
- if: ${{ steps.publish.outputs.type }}
|
|
25
50
|
name: Create Release
|
|
26
51
|
env:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hubot-nextbus",
|
|
3
3
|
"description": "Shows when the next transit vehicle will arrive at a particular stop.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
5
|
"author": "Stephen Yeargin <stephen@yearg.in>",
|
|
6
6
|
"homepage": "https://github.com/transitnownash/hubot-nextbus",
|
|
7
7
|
"license": "MIT",
|
|
@@ -24,18 +24,16 @@
|
|
|
24
24
|
"hubot": ">=3 || 0.0.0-development"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"
|
|
27
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
28
28
|
"eslint": "^8.56.0",
|
|
29
29
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
30
30
|
"eslint-plugin-import": "^2.29.1",
|
|
31
31
|
"eslint-plugin-n": "^16.5.0",
|
|
32
32
|
"eslint-plugin-promise": "^6.1.1",
|
|
33
|
-
"hubot
|
|
34
|
-
"husky": "^
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"sinon": "^17.0.1",
|
|
38
|
-
"sinon-chai": "^3.7.0"
|
|
33
|
+
"hubot": "^14.1.0",
|
|
34
|
+
"husky": "^9.1.7",
|
|
35
|
+
"nock": "^14.0.17",
|
|
36
|
+
"vitest": "^4.1.6"
|
|
39
37
|
},
|
|
40
38
|
"main": "index.js",
|
|
41
39
|
"scripts": {
|
|
@@ -46,7 +44,10 @@
|
|
|
46
44
|
"ascii-table": "^0.0.9",
|
|
47
45
|
"moment": "^2.29.4"
|
|
48
46
|
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22"
|
|
49
|
+
},
|
|
49
50
|
"volta": {
|
|
50
|
-
"node": "
|
|
51
|
+
"node": "22.22.3"
|
|
51
52
|
}
|
|
52
53
|
}
|
package/script/test
CHANGED
package/src/nextbus.js
CHANGED
|
@@ -27,11 +27,12 @@ module.exports = (robot) => {
|
|
|
27
27
|
robot.logger.debug(url);
|
|
28
28
|
robot.http(url)
|
|
29
29
|
.get()((err, res, body) => {
|
|
30
|
-
const response = JSON.parse(body);
|
|
31
30
|
if (err) {
|
|
32
|
-
|
|
31
|
+
const detail = err.errors ? err.errors.map((e) => e.message).join(', ') : err.message;
|
|
32
|
+
msg.send(`Error fetching NextBus data: ${detail || err}`);
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
+
const response = JSON.parse(body);
|
|
35
36
|
if (response.error) {
|
|
36
37
|
msg.send(response.error);
|
|
37
38
|
return;
|
|
@@ -53,33 +54,115 @@ module.exports = (robot) => {
|
|
|
53
54
|
return moment(`${moment().format('YYYY-MM-DD')} ${timeStr.trim().padStart(8, '0')}`);
|
|
54
55
|
};
|
|
55
56
|
|
|
57
|
+
const getRealTimeStatus = (stopTime) => {
|
|
58
|
+
// Check if realtime data is available
|
|
59
|
+
if (!stopTime.realtime || !stopTime.realtime.departure) {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const scheduled = formatTripTimeAsMoment(stopTime.departure_time);
|
|
64
|
+
const actual = formatTripTimeAsMoment(stopTime.realtime.departure);
|
|
65
|
+
const diffMinutes = actual.diff(scheduled, 'minutes');
|
|
66
|
+
|
|
67
|
+
if (diffMinutes === 0) {
|
|
68
|
+
return 'On time';
|
|
69
|
+
} if (diffMinutes > 0) {
|
|
70
|
+
return `${diffMinutes}m late`;
|
|
71
|
+
}
|
|
72
|
+
return `${Math.abs(diffMinutes)}m early`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const formatAlerts = (alerts) => {
|
|
76
|
+
if (!alerts || alerts.length === 0) {
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const alertLines = alerts.map((alert) => {
|
|
81
|
+
const headerText = alert.header_text?.translation?.[0]?.text || 'Service Alert';
|
|
82
|
+
return `⚠️ *${headerText.trim()}*`;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return alertLines.join('\n');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const tripTableHeading = ['Time', 'Route', 'ETA'];
|
|
89
|
+
|
|
90
|
+
const formatSlackTripTable = (heading, rows) => ({
|
|
91
|
+
blocks: [
|
|
92
|
+
{
|
|
93
|
+
type: 'header',
|
|
94
|
+
text: { type: 'plain_text', text: heading, emoji: true },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: 'table',
|
|
98
|
+
rows: [tripTableHeading, ...rows].map((row) => row.map((cell) => ({
|
|
99
|
+
type: 'raw_text',
|
|
100
|
+
text: String(cell ?? ''),
|
|
101
|
+
}))),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
|
|
56
106
|
const queryStopById = (stopId, msg) => getAPIResponse('agencies.json', msg, (agencies) => {
|
|
57
107
|
// Override timezone for moment() calls
|
|
58
108
|
process.env.TZ = agencies.data[0].agency_timezone;
|
|
59
109
|
robot.logger.debug(process.env.TZ);
|
|
60
110
|
robot.logger.debug('Current Time:', moment());
|
|
61
|
-
getAPIResponse(`stops/${stopId}/
|
|
62
|
-
const
|
|
63
|
-
|
|
111
|
+
getAPIResponse(`stops/${stopId}/next.json`, msg, (response) => {
|
|
112
|
+
const {
|
|
113
|
+
stop,
|
|
114
|
+
next_trip: nextTrip,
|
|
115
|
+
upcoming_trips: upcomingTrips,
|
|
116
|
+
alerts,
|
|
117
|
+
vehicle_positions: vehiclePositions,
|
|
118
|
+
} = response;
|
|
119
|
+
|
|
120
|
+
// Show alerts if any exist
|
|
121
|
+
const alertMessage = formatAlerts(alerts);
|
|
122
|
+
if (alertMessage) {
|
|
123
|
+
msg.send(alertMessage);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Combine next trip with upcoming trips to get all trips
|
|
127
|
+
const allTrips = nextTrip ? [nextTrip, ...upcomingTrips] : upcomingTrips;
|
|
128
|
+
|
|
129
|
+
const nextTripsData = allTrips.filter((tripData) => {
|
|
130
|
+
const tripTime = formatTripTimeAsMoment(tripData.stop_time.arrival_time);
|
|
64
131
|
return tripTime.isAfter(moment(), 'second');
|
|
65
132
|
});
|
|
66
|
-
|
|
67
|
-
|
|
133
|
+
|
|
134
|
+
robot.logger.debug(nextTripsData);
|
|
135
|
+
if (nextTripsData.length === 0) {
|
|
68
136
|
msg.send('The last bus has already run for today.');
|
|
69
137
|
return;
|
|
70
138
|
}
|
|
71
139
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
140
|
+
const rows = nextTripsData.slice(0, 5).map((tripData) => {
|
|
141
|
+
const tripTime = formatTripTimeAsMoment(tripData.stop_time.arrival_time);
|
|
142
|
+
const realtimeStatus = getRealTimeStatus(tripData.stop_time);
|
|
143
|
+
const hasVehicle = vehiclePositions
|
|
144
|
+
&& vehiclePositions.some((vp) => vp.trip && vp.trip.trip_id === tripData.trip.trip_gid);
|
|
145
|
+
const busIndicator = hasVehicle ? ' 🚌' : '';
|
|
146
|
+
const timeUntilText = realtimeStatus ? `${tripTime.fromNow()} (${realtimeStatus})` : tripTime.fromNow();
|
|
147
|
+
return [
|
|
148
|
+
tripTime.format('LT'),
|
|
149
|
+
`#${tripData.trip.route_gid} - ${tripData.trip.trip_headsign}${busIndicator}`,
|
|
150
|
+
timeUntilText,
|
|
151
|
+
];
|
|
80
152
|
});
|
|
153
|
+
const adapterName = robot.adapterName ?? robot.adapter?.name ?? '';
|
|
154
|
+
|
|
155
|
+
if (/slack/i.test(adapterName)) {
|
|
156
|
+
msg.send(formatSlackTripTable(`🚏 ${stop.stop_name}`, rows));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const table = new AsciiTable();
|
|
161
|
+
rows.forEach((row) => table.addRow(row));
|
|
81
162
|
table.removeBorder();
|
|
82
|
-
|
|
163
|
+
const tableOutput = table.toString().split('\n').map((line) => line.trimEnd()).join('\n');
|
|
164
|
+
const heading = `🚏 *${stop.stop_name}*`;
|
|
165
|
+
msg.send(`${heading}\n${tableOutput}`);
|
|
83
166
|
});
|
|
84
167
|
});
|
|
85
168
|
|
|
@@ -92,7 +175,7 @@ module.exports = (robot) => {
|
|
|
92
175
|
|
|
93
176
|
getAPIResponse(`stops/near/${latlon}/1000.json?per_page=5`, msg, (stops) => {
|
|
94
177
|
if (stops.total > 0) {
|
|
95
|
-
queryStopById(stops.data[0].
|
|
178
|
+
queryStopById(stops.data[0].stop_code, msg);
|
|
96
179
|
return;
|
|
97
180
|
}
|
|
98
181
|
msg.send(`No stops found near ${latlon}`);
|
|
@@ -101,16 +184,20 @@ module.exports = (robot) => {
|
|
|
101
184
|
|
|
102
185
|
// get a list of nearby stops
|
|
103
186
|
robot.respond(/(?:bus|nextbus) stops$/i, (msg) => getAPIResponse(`stops/near/${latlon}/1000.json?per_page=5`, msg, (stops) => {
|
|
187
|
+
if (stops.total === 0) {
|
|
188
|
+
msg.send(`No stops found near ${latlon}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
104
191
|
msg.send('List of nearby stops:');
|
|
105
192
|
const output = [];
|
|
106
193
|
stops.data.forEach((stop) => {
|
|
107
|
-
output.push(`-
|
|
194
|
+
output.push(`- \`#${stop.stop_code}\` - ${stop.stop_name}`);
|
|
108
195
|
});
|
|
109
196
|
msg.send(output.join('\n'));
|
|
110
197
|
}));
|
|
111
198
|
|
|
112
199
|
// get a particular stop's next bus
|
|
113
|
-
robot.respond(/(?:bus|nextbus) stop ([A-Z0-9_]+)$/i, (msg) => {
|
|
200
|
+
robot.respond(/(?:bus|nextbus) stop #?([A-Z0-9_]+)$/i, (msg) => {
|
|
114
201
|
const stopId = msg.match[1];
|
|
115
202
|
robot.logger.debug(stopId);
|
|
116
203
|
queryStopById(stopId, msg);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"per_page": 100,
|
|
5
5
|
"data": [
|
|
6
6
|
{
|
|
7
|
-
"id":
|
|
7
|
+
"id": 178,
|
|
8
8
|
"agency_gid": "WeGo Public Transit",
|
|
9
9
|
"agency_name": "WeGo Public Transit",
|
|
10
10
|
"agency_url": "http://www.wegotransit.com",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"agency_phone": "615-862-8833",
|
|
14
14
|
"agency_fare_url": null,
|
|
15
15
|
"agency_email": null,
|
|
16
|
-
"created_at": "
|
|
17
|
-
"updated_at": "
|
|
16
|
+
"created_at": "2026-08-30T04:05:14.056-05:00",
|
|
17
|
+
"updated_at": "2026-08-30T04:05:14.056-05:00"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|