@@ -26,7 +26,7 @@ import {
26
26
escapeHtml ,
27
27
} from "./html.js" ;
28
28
import expressWebsockets from "express-ws" ;
29
-
29
+ import ical from "node-ical" ;
30
30
import fs from "node:fs" ;
31
31
32
32
// Catch and snuff all uncaught exceptions and uncaught promise rejections.
@@ -60,79 +60,79 @@ expressWebsockets(app);
60
60
61
61
const zoomRooms = [
62
62
{
63
- href : "https://recurse.com/zoom/aegis" ,
63
+ location : "https://www. recurse.com/zoom/aegis" ,
64
64
roomName : "Aegis" ,
65
65
} ,
66
66
{
67
- href : "https://recurse.com/zoom/arca" ,
67
+ location : "https://www. recurse.com/zoom/arca" ,
68
68
roomName : "Arca" ,
69
69
} ,
70
70
{
71
- href : "https://recurse.com/zoom/edos" ,
71
+ location : "https://www. recurse.com/zoom/edos" ,
72
72
roomName : "Edos" ,
73
73
} ,
74
74
{
75
- href : "https://recurse.com/zoom/genera" ,
75
+ location : "https://www. recurse.com/zoom/genera" ,
76
76
roomName : "Genera" ,
77
77
} ,
78
78
{
79
- href : "https://recurse.com/zoom/midori" ,
79
+ location : "https://www. recurse.com/zoom/midori" ,
80
80
roomName : "Midori" ,
81
81
} ,
82
82
{
83
- href : "https://recurse.com/zoom/verve" ,
83
+ location : "https://www. recurse.com/zoom/verve" ,
84
84
roomName : "Verve" ,
85
85
} ,
86
86
{
87
- href : "https://recurse.com/zoom/couches" ,
87
+ location : "https://www. recurse.com/zoom/couches" ,
88
88
roomName : "Couches" ,
89
89
} ,
90
90
{
91
- href : "https://recurse.com/zoom/kitchen" ,
91
+ location : "https://www. recurse.com/zoom/kitchen" ,
92
92
roomName : "Kitchen" ,
93
93
} ,
94
94
{
95
- href : "https://recurse.com/zoom/pairing_station_1" ,
95
+ location : "https://www. recurse.com/zoom/pairing_station_1" ,
96
96
roomName : "Pairing Station 1" ,
97
97
} ,
98
98
{
99
- href : "https://recurse.com/zoom/pairing_station_2" ,
99
+ location : "https://www. recurse.com/zoom/pairing_station_2" ,
100
100
roomName : "Pairing Station 2" ,
101
101
} ,
102
102
{
103
- href : "https://recurse.com/zoom/pairing_station_3" ,
103
+ location : "https://www. recurse.com/zoom/pairing_station_3" ,
104
104
roomName : "Pairing Station 3" ,
105
105
} ,
106
106
{
107
- href : "https://recurse.com/zoom/pairing_station_4" ,
107
+ location : "https://www. recurse.com/zoom/pairing_station_4" ,
108
108
roomName : "Pairing Station 4" ,
109
109
} ,
110
110
{
111
- href : "https://recurse.com/zoom/pairing_station_5" ,
111
+ location : "https://www. recurse.com/zoom/pairing_station_5" ,
112
112
roomName : "Pairing Station 5" ,
113
113
} ,
114
114
{
115
- href : "https://recurse.rctogether.com/zoom_meetings/35980/join" ,
115
+ location : "https://recurse.rctogether.com/zoom_meetings/35980/join" ,
116
116
roomName : "Pairing Station 6" ,
117
117
} ,
118
118
{
119
- href : "https://recurse.rctogether.com/zoom_meetings/35983/join" ,
119
+ location : "https://recurse.rctogether.com/zoom_meetings/35983/join" ,
120
120
roomName : "Pairing Station 7" ,
121
121
} ,
122
122
{
123
- href : "https://recurse.com/zoom/pomodoro_room" ,
123
+ location : "https://www. recurse.com/zoom/pomodoro_room" ,
124
124
roomName : "Pomodoro Room" ,
125
125
} ,
126
126
{
127
- href : "https://recurse.com/zoom/presentation_space" ,
127
+ location : "https://www. recurse.com/zoom/presentation_space" ,
128
128
roomName : "Presentation Space" ,
129
129
} ,
130
130
{
131
- href : "https://recurse.com/zoom/faculty_area" ,
131
+ location : "https://www. recurse.com/zoom/faculty_area" ,
132
132
roomName : "Faculty Area" ,
133
133
} ,
134
134
{
135
- href : "https://recurse.com/zoom/faculty_lounge" ,
135
+ location : "https://www. recurse.com/zoom/faculty_lounge" ,
136
136
roomName : "Faculty Lounge" ,
137
137
} ,
138
138
] ;
@@ -142,6 +142,10 @@ const zoomRoomsByName = {};
142
142
zoomRooms . forEach ( ( { roomName, ...rest } ) => {
143
143
zoomRoomsByName [ roomName ] = { roomName, ...rest } ;
144
144
} ) ;
145
+ const zoomRoomsByLocation = { } ;
146
+ zoomRooms . forEach ( ( { location, ...rest } ) => {
147
+ zoomRoomsByLocation [ location ] = { location, ...rest } ;
148
+ } ) ;
145
149
146
150
// Zoom Rooms that are reported but that we purposely don't track
147
151
const silentZoomRooms = [
@@ -196,6 +200,9 @@ const secretAuthToken = process.env.SPECIAL_SECRET_AUTH_TOKEN_DONT_SHARE;
196
200
// Mixpanel
197
201
const mixpanelToken = process . env . MIXPANEL_TOKEN ;
198
202
203
+ // Recurse.com Calendar
204
+ const recurseCalendarToken = process . env . RECURSE_CALENDAR_TOKEN ;
205
+
199
206
let inTheHubParticipantNames = [ ] ;
200
207
let roomNameToParticipantNames = { } ;
201
208
let participantNameToEntity = { } ;
@@ -612,10 +619,11 @@ app.ws("/websocket", async function (ws, req) {
612
619
Room (
613
620
mungeRoom ( {
614
621
roomName,
615
- roomHref : zoomRoomsByName [ roomName ] . href ,
622
+ roomLocation : zoomRoomsByName [ roomName ] . location ,
616
623
roomNameToNote,
617
624
roomNameToParticipantNames,
618
625
participantNameToEntity,
626
+ locationToNowAndNextEvents,
619
627
} ) ,
620
628
) ,
621
629
) ;
@@ -705,6 +713,60 @@ function cleanNotes() {
705
713
}
706
714
cleanNotes ( ) ;
707
715
716
+ let locationToNowAndNextEvents = { } ;
717
+ async function updateCalendar ( ) {
718
+ const now = new Date ( ) ;
719
+ const tomorrow = new Date ( ) ;
720
+ tomorrow . setTime ( tomorrow . getTime ( ) + 1000 * 60 * 60 * 24 ) ;
721
+ const yesterday = new Date ( ) ;
722
+ yesterday . setTime ( yesterday . getTime ( ) - 1000 * 60 * 60 * 24 ) ;
723
+ const soonish = new Date ( ) ;
724
+ soonish . setTime ( soonish . getTime ( ) + 1000 * 60 * 80 ) ; // 80 minutes
725
+ const ics = await ical . async . fromURL (
726
+ `https://www.recurse.com/calendar/events.ics?token=${ recurseCalendarToken } &omit_cancelled_events=1&scope=all` ,
727
+ ) ;
728
+
729
+ const locationToEvents = { } ;
730
+ Object . entries ( ics ) . forEach ( ( [ _ , event ] ) => {
731
+ const { location, start, end } = event ;
732
+ let keep = true ;
733
+ keep &&= event . type === "VEVENT" ;
734
+ keep &&= location in zoomRoomsByLocation ;
735
+ keep &&= start >= yesterday ; // Starts less than 24 hours ago
736
+ keep &&= end <= tomorrow ; // Ends less than 24 hours from now
737
+ keep &&= now <= end ; // Hasn't ended yet
738
+ if ( ! keep ) return ;
739
+
740
+ if ( ! locationToEvents [ location ] ) {
741
+ locationToEvents [ location ] = [ ] ;
742
+ }
743
+ locationToEvents [ location ] . push ( event ) ;
744
+ } ) ;
745
+
746
+ locationToNowAndNextEvents = { } ;
747
+ Object . entries ( locationToEvents ) . forEach ( ( [ location , events ] ) => {
748
+ events . sort ( ( a , b ) => a . start - b . start ) ;
749
+
750
+ locationToNowAndNextEvents [ location ] = {
751
+ now : [ ] ,
752
+ next : [ ] ,
753
+ } ;
754
+
755
+ events . forEach ( ( event ) => {
756
+ const { start } = event ;
757
+
758
+ if ( start <= now ) {
759
+ locationToNowAndNextEvents [ location ] . now . push ( event ) ;
760
+ } else if ( start <= soonish ) {
761
+ locationToNowAndNextEvents [ location ] . next . push ( event ) ;
762
+ }
763
+ } ) ;
764
+ } ) ;
765
+
766
+ setTimeout ( updateCalendar , 1000 * 60 * 10 ) ;
767
+ }
768
+ updateCalendar ( ) ;
769
+
708
770
// Currently unused, adds a text field to submit a note when you check in
709
771
app . get (
710
772
"/checkIntoHub.html" ,
@@ -844,10 +906,11 @@ const mungeRootBody = ({
844
906
const rooms = zoomRooms . map ( ( { roomName } ) =>
845
907
mungeRoom ( {
846
908
roomName,
847
- roomHref : zoomRoomsByName [ roomName ] . href ,
909
+ roomLocation : zoomRoomsByName [ roomName ] . location ,
848
910
roomNameToNote,
849
911
roomNameToParticipantNames,
850
912
participantNameToEntity,
913
+ locationToNowAndNextEvents,
851
914
} ) ,
852
915
) ;
853
916
@@ -908,14 +971,15 @@ const mungeCustomization = ({
908
971
909
972
const mungeRoom = ( {
910
973
roomName,
911
- roomHref ,
974
+ roomLocation ,
912
975
roomNameToNote,
913
976
roomNameToParticipantNames,
914
977
participantNameToEntity,
978
+ locationToNowAndNextEvents,
915
979
} ) => {
916
980
return {
917
981
roomName,
918
- roomHref ,
982
+ roomLocation ,
919
983
hasNote : Boolean ( roomNameToNote [ roomName ] ) ,
920
984
noteContent : roomNameToNote [ roomName ] ?. content ?? "" ,
921
985
noteDateTime : roomNameToNote [ roomName ] ?. date ?. toISOString ( ) ?? null ,
@@ -930,6 +994,20 @@ const mungeRoom = ({
930
994
participantNameToEntity [ participantName ] ?. faceMarkerImagePath ??
931
995
"recurse-community-bot.png" ,
932
996
} ) ) ?? [ ] ,
997
+ hasNowEvent : locationToNowAndNextEvents [ roomLocation ] ?. now ?. [ 0 ] ,
998
+ nowEventName : locationToNowAndNextEvents [ roomLocation ] ?. now ?. [ 0 ] ?. summary ,
999
+ nowEventStartedHowManyMinutesAgo : howManyMinutesAgo (
1000
+ locationToNowAndNextEvents [ roomLocation ] ?. now ?. [ 0 ] ?. start ,
1001
+ ) ,
1002
+ nowEventCalendarUrl :
1003
+ locationToNowAndNextEvents [ roomLocation ] ?. now ?. [ 0 ] ?. url ,
1004
+ hasNextEvent : locationToNowAndNextEvents [ roomLocation ] ?. next ?. [ 0 ] ,
1005
+ nextEventName : locationToNowAndNextEvents [ roomLocation ] ?. next ?. [ 0 ] ?. summary ,
1006
+ nextEventStartsInHowLong : howLongInTheFuture (
1007
+ locationToNowAndNextEvents [ roomLocation ] ?. next ?. [ 0 ] ?. start ,
1008
+ ) ,
1009
+ nextEventCalendarUrl :
1010
+ locationToNowAndNextEvents [ roomLocation ] ?. next ?. [ 0 ] ?. url ,
933
1011
} ;
934
1012
} ;
935
1013
@@ -1103,11 +1181,37 @@ const howManyMinutesAgo = (date) => {
1103
1181
? "a half hour ago"
1104
1182
: difference < 60 * MIN
1105
1183
? "45 min ago"
1106
- : difference < 60 * MIN
1184
+ : difference < 80 * MIN
1107
1185
? "over an hour ago"
1108
1186
: "a while ago" ;
1109
1187
} ;
1110
1188
1189
+ const howLongInTheFuture = ( date ) => {
1190
+ if ( ! date ) return null ;
1191
+ const millisNow = Date . now ( ) ;
1192
+ const millisThen = date . getTime ( ) ;
1193
+ const difference = millisThen - millisNow ;
1194
+ return difference < 0
1195
+ ? "in the past?" // ???
1196
+ : difference < 2 * MIN
1197
+ ? "now"
1198
+ : difference < 5 * MIN
1199
+ ? "in a few minutes"
1200
+ : difference < 10 * MIN
1201
+ ? "in five-ish minutes"
1202
+ : difference < 20 * MIN
1203
+ ? "in 15 minutes"
1204
+ : difference < 30 * MIN
1205
+ ? "in 20 minutes"
1206
+ : difference < 45 * MIN
1207
+ ? "in a half hour"
1208
+ : difference < 60 * MIN
1209
+ ? "in 45 min"
1210
+ : difference < 80 * MIN
1211
+ ? "in just over an hour"
1212
+ : "in quite a while" ;
1213
+ } ;
1214
+
1111
1215
const countPhrase = ( count ) => {
1112
1216
return count === 0 ? "empty" : count === 1 ? "1 person" : `${ count } people` ;
1113
1217
} ;
0 commit comments