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.
@@ -0,0 +1,8 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "npm"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ ignore:
8
+ - dependency-name: "eslint*"
@@ -10,7 +10,7 @@ jobs:
10
10
  strategy:
11
11
  fail-fast: false
12
12
  matrix:
13
- node-version: [16.x, 18.x, 20.x]
13
+ node-version: [20.x, 22.x, 24.x]
14
14
 
15
15
  steps:
16
16
  - uses: actions/checkout@v3
@@ -32,4 +32,3 @@ jobs:
32
32
  npm test
33
33
  env:
34
34
  CI: true
35
-
@@ -1,26 +1,51 @@
1
1
  on:
2
2
  push:
3
- branches: main
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@v4
15
- - uses: actions/setup-node@v3
16
+ - uses: actions/checkout@v6
17
+
18
+ - uses: actions/setup-node@v6
16
19
  with:
17
- node-version: "20"
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
- - uses: JS-DevTools/npm-publish@v3
28
+
29
+ - name: Publish to npm
21
30
  id: publish
22
- with:
23
- token: ${{ secrets.NPM_AUTH_TOKEN }}
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.2.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
- "chai": "^4.3.10",
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-test-helper": "^1.9.0",
34
- "husky": "^8.0.3",
35
- "mocha": "^10.2.0",
36
- "nock": "^13.4.0",
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": "18.19.0"
51
+ "node": "22.22.3"
51
52
  }
52
53
  }
package/script/test CHANGED
@@ -3,4 +3,4 @@
3
3
  # bootstrap environment
4
4
  source script/bootstrap
5
5
 
6
- mocha "test/**/*.js" --reporter spec --exit
6
+ vitest run --coverage
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
- msg.send(err);
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}/trips.json?per_page=2000`, msg, (trips) => {
62
- const nextTrips = trips.data.filter((trip) => {
63
- const tripTime = formatTripTimeAsMoment(trip.stop_times[0].arrival_time);
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
- robot.logger.debug(nextTrips);
67
- if (nextTrips.length === 0) {
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 table = new AsciiTable();
73
- const {
74
- stop,
75
- } = nextTrips[0].stop_times[0];
76
- msg.send(`Upcoming Trips for [${stop.stop_gid}] ${stop.stop_name}`);
77
- nextTrips.slice(0, 5).forEach((trip) => {
78
- const tripTime = formatTripTimeAsMoment(trip.stop_times[0].arrival_time);
79
- table.addRow([formatTripTimeAsMoment(trip.stop_times[0].arrival_time).format('LT'), `#${trip.route_gid} - ${trip.trip_headsign}`, tripTime.fromNow()]);
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
- msg.send(table.toString());
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].stop_gid, msg);
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(`- [${stop.stop_gid}] ${stop.stop_name}`);
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);
@@ -0,0 +1,5 @@
1
+ // Description
2
+ // Mock Slack adapter
3
+ module.exports = (robot) => {
4
+ robot.adapterName = 'slack';
5
+ };
@@ -4,7 +4,7 @@
4
4
  "per_page": 100,
5
5
  "data": [
6
6
  {
7
- "id": 1,
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": "2022-08-02T21:14:03.545-05:00",
17
- "updated_at": "2022-08-02T21:14:03.545-05:00"
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
  }