-
Notifications
You must be signed in to change notification settings - Fork 0
/
zoom-manage
executable file
·1020 lines (937 loc) · 35.5 KB
/
zoom-manage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/osascript
(*
Copyright (c) 2023 Kayvan A. Sylvan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*)
-- Global properties
property scriptName : "Zoom Manage"
property topLevelDirectory : missing value
-- Zoom Related properties
property appName : "zoom.us"
property appVersion : missing value
property topZoomWindow : "Zoom"
property meetingWindow : "Zoom Meeting"
property participantWindow : missing value
-- These three files are created in the logs/ subdirectory.
-- The pattern of filenames: logs/20231014-log.txt
-- This makes it easy to store and index the logs at a later time.
-- roster.txt is the running log file capturing the roster information.
-- log.txt is the overall log file.
-- filtered.txt is the filtered log file (capturing "Hands Raised", etc.)
property logFile : "log.txt"
property meetingRoster : "roster.txt"
property filteredRoster : "filtered.txt"
-- The FastAPI server should be running at this URL
property trackerURL : "http://localhost:5000"
property batchCount : 50 -- can be set via ZOOM_MANAGE_BATCH_SIZE environment variable
-- Top level commands the script understands
property knownCommands : {Â
"help", "reset", "roster", "hands", "camera_off", "admit", "server", "dashboard", Â
"breakout", "camera_off", "camera_on", "phone", "no_audio", "muted", "unmuted", Â
"rename", "co-host", "host"}
-- zoomRosterDebug uses the ZOOM_DEBUG environment variable.
-- If it is set to true, filtered rosters (like "hands") will be
-- output to the console (stderr) in addition to going in the log.
property zoomRosterDebug : missing value
-- renameParticipantsFile is a mapping of users to rename upon entry to the meeting.
-- If set, we use it in the "roster" command to check for and rename
-- participants. It references the ZOOM_RENAME_FILE environment
property renameParticipantsFile : missing value
property renameMappings : missing value -- the mappings found in the above file
-- Path to cliclick. See https://github.com/BlueM/cliclick
property cliclick : "/opt/homebrew/bin/cliclick"
property environmentDocs: Â
"Environment Varibles:" & linefeed Â
& " - ZOOM_DEBUG: Set this to any value to see rosters and filtered lists output on the console." & linefeed Â
& " - ZOOM_RENAME_FILE: Path to a file of participant rename mappings. See rename/README.md file for details." & linefeed
on usageMessage(section)
set tid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ":"
set path_parts to every text item of ((path to me) as string)
set AppleScript's text item delimiters to tid
if section is "top" then
return linefeed & "Usage: " & (item -1 of path_parts) & " [command]" & linefeed Â
& " help - show usage message." & linefeed & linefeed Â
& " server - start/stop the backend server. Starts the server in its own terminal." & linefeed Â
& " dashboard - open the Zoom Meeting Tracker dashboard." & linefeed Â
& " reset - reset the tracking database." & linefeed & linefeed Â
& " roster (default action) - get current roster." & linefeed Â
& linefeed Â
& " hands - get the current hands raised." & linefeed Â
& " camera_off - get the list of camera off participants." & linefeed Â
& " camera_on - get the list of participants who are on video." & linefeed Â
& " no_audio - get the list of participants not connected to audio." & linefeed Â
& " muted - get the list of participants who are muted." & linefeed Â
& " unmuted - get the list of participants who are unmuted." & linefeed Â
& " phone - get the list of participants dialing in by phone." & linefeed Â
& linefeed Â
& " admit - admit everyone in the Waiting Room." & linefeed Â
& " breakout - create a set of named breakout rooms." & linefeed Â
& linefeed Â
& " rename - rename a participant." & linefeed Â
& " co-host - make a participant a co-host." & linefeed Â
& " host - make a participant the meeting host (NOTE: You will not be Host or Co-Host)" & linefeed Â
& linefeed & environmentDocs
else if section is "breakout" then
return linefeed & "Usage: " & (item -1 of path_parts) & " breakout [create | get | help] filename" & linefeed Â
& " help - print the breakout related help message." & linefeed Â
& " create - Opens up the named file and creates the rooms named there." & linefeed Â
& " get - Get the named rooms and write them into filename." & linefeed
else if section is "rename" then
return linefeed & "Usage: " & (item -1 of path_parts) & " rename 'Original Name' 'New Name'" & linefeed
else if section is "co-host" then
set more to " You can also withdraw co-host by re-running the command with the \"(Co-host)\" name." & linefeed
return linefeed & "Usage: " & (item -1 of path_parts) & " co-host 'Participant Name'" & linefeed & more
else if section is "host" then
set more to " Warning: Once you do this, you are no longer Host or Co-Host." & linefeed
return linefeed & "Usage: " & (item -1 of path_parts) & " host 'Participant Name'" & linefeed & more
else if section is "server" then
set more to " The start action launches the server in its own Terminal window." & linefeed
return linefeed & "Usage: " & (item -1 of path_parts) & " server [start|stop]" & linefeed & more
end if
end usageMessage
on todayYMD()
-- Return the current date in the format YYYYMMDD.
set [_day, _month, _year] to [day, month, year] of (current date)
set _month to _month * 1 --> 3
set _month to text -1 thru -2 of ("0" & _month) --> "03"
set _day to text -1 thru -2 of ("0" & _day)
set result to _year & _month & _day
return result
end todayYMD
on setUpLogFiles()
-- setUpLogFiles handler:
-- Creates a folder called "logs" in the same directory as the program if it does not already exist.
-- It then creates two files, logFile and meetingRoster, in the logs folder, with a prefix of the current date.
tell application "Finder"
set _currentDirectory to container of (path to me)
if not (exists folder "logs" of _currentDirectory) then
make new folder at _currentDirectory with properties {name:"logs"}
end if
end tell
set _currentDirectory to POSIX path of (_currentDirectory as text)
set _prefix to my todayYMD()
set topLevelDirectory to _currentDirectory
set logFile to POSIX file (topLevelDirectory & "logs/" & _prefix & "-" & logFile)
set meetingRoster to POSIX file (topLevelDirectory & "logs/" & _prefix & "-" & meetingRoster)
set filteredRoster to POSIX file (topLevelDirectory & "logs/" & _prefix & "-" & filteredRoster)
my setupEnvVariables()
end setUpLogFiles
on setupEnvVariables()
-- zoomRosterDebug is false if ZOOM_DEBUG is not set
set zoomRosterDebug to do shell script "echo ${ZOOM_DEBUG:-unset}"
set zoomRosterDebug to (zoomRosterDebug is not "unset") -- set to false or true
-- batchCount is set to value of ZOOM_MANAGER_BATCH_SIZE (50 if not set)
set batchCount to do shell script "echo ${ZOOM_MANAGE_BATCH_SIZE:-" & batchCount & "}"
-- renameParticipantsFile points to a mapping file of old to new names with "->" in between them.
set renameParticipantsFile to do shell script "echo ${ZOOM_RENAME_FILE:-}"
if renameParticipantsFile is "" then set renameParticipantsFile to missing value
end setupEnvVariables
on filterFileContents(fileLines)
-- Given a set of fileLines (usually gathered from "paragraphs of fileContents" idiom)
-- Return a new list, ignoring all empty lines and lines starting with "#"
set _filtered to {}
repeat with _item in fileLines
set _s to _item as string
if _s is not "" and _s does not start with "#" then
if _s starts with "\\" then set _s to text 2 through -1 of _s
set end of _filtered to _s
end if
end repeat
return _filtered
end filterFileContents
on splitText(theText, theDelimiter)
set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to theDelimiter
set textItems to every text item of theText
set AppleScript's text item delimiters to oldDelimiters -- Restore the original delimiters
return textItems
end splitText
on loadRenameMappings()
if renameParticipantsFile is missing value then return
if not renameParticipantsFile starts with "/" then
set renameParticipantsFile to topLevelDirectory & renameParticipantsFile
end if
tell application "System Events"
if (not (exists file renameParticipantsFile)) then
error ("Missing ZOOM_RENAME_FILE file: " & renameParticipantsFile) number -1
end if
set _size to size of (info for POSIX file renameParticipantsFile)
if _size is 0 then
error ("Empty ZOOM_RENAME_FILE file: " & renameParticipantsFile) number -2
end if
end tell
set _rFile to POSIX file renameParticipantsFile
set _file to open for access file _rFile
set fileContents to read _file
close access _rFile
set mappings to my filterFileContents(paragraphs of fileContents)
set renameMappings to {}
repeat with _item in mappings
set end of renameMappings to my splitText(_item, "->")
end repeat
end loadRenameMappings
on lookupRenaming(oldName)
if renameMappings is missing value then return
repeat with _i in renameMappings
if item 1 of _i as text is oldName as text then return _i
end repeat
return missing value
end lookupRenaming
on formatDateTime(theDateTime)
(*
This code takes a date and time as input and formats it into a string in the form of MM/DD/YYYY HH:MM:SS.
It does this by extracting the day, month, year, hours, minutes, and seconds from the input and
adding a leading zero if necessary.
*)
set [_day, _month, _year, _hours, _minutes, _seconds] to [day, month, year, hours, minutes, seconds] of theDateTime
set _month to _month * 1 --> 3
set _month to text -1 thru -2 of ("0" & _month) --> "03"
set _day to text -1 thru -2 of ("0" & _day)
set _hours to text -1 thru -2 of ("0" & _hours) --> "05"
set _minutes to text -1 thru -2 of ("0" & _minutes)
set _seconds to text -1 thru -2 of ("0" & _seconds)
set result to _month & "/" & _day & "/" & _year & " " & _hours & ":" & _minutes & ":" & _seconds
return result
end formatDateTime
on appendToFile(theFile, message, logToConsole)
-- Write the message to theFile. logToConsole is a boolean indicating whether to log to console.
write message & linefeed to theFile starting at eof
if logToConsole then log message
end appendToFile
on logMessage(message, filePath)
-- Usage:
-- set logFilePath to (path to desktop as string) & "logfile.txt" -- Path to the log file
-- my logMessage("This is a log message.", logFilePath)
try
-- Open the log file for writing, creating it if it doesn't exist
set _log to open for access file filePath with write permission
-- Construct the log entry
set logEntry to (my formatDateTime(current date)) & " " & message
-- Write the log entry to the file
my appendToFile(_log, logEntry, false)
-- Close the log file
close access _log
on error errMsg number errNum
-- If an error occurs, close the log file (if it's open)
try
close access _log
end try
-- Optionally, report the error in some way
set _msg to "Error: " & errMsg & " (" & errNum & ")"
log _msg
display dialog _msg
end try
end logMessage
on checkZoomRunning()
tell application "System Events"
set _isRunning to (name of every process) contains appName
end tell
if not _isRunning then
tell application appName to activate
delay 1.0
end if
set appVersion to version of application appName
end checkZoomRunning
on findStatusMenu(nameSubstring)
-- Find a status menu of Zoom that contains nameSubstring
set _ret to missing value
tell application "System Events" to tell process appName
set menuItems to every menu item of menu 1 of menu bar 2
repeat with m in menuItems
if (exists name of m) and name of m contains nameSubstring then
set _ret to m
exit repeat
end if
end repeat
end tell
return _ret
end findStatusMenu
on clickStatusMenu(nameSubstring)
-- Click a menu item in status menu. Returns true if done, false otherwise.
tell application "System Events" to tell process appName
set m to my findStatusMenu(nameSubstring)
if m is not missing value then
click m
delay 1
end if
end tell
return (m is not missing value)
end clickStatusMenu
on enableParticipantsWindow()
(*
This code is used to click the "Manage Participants" menu item of the status menu bar.
A delay of 1 second is added after the click.
*)
my clickStatusMenu("Participants")
my logMessage("Participants panel started.", logFile)
end enableParticipantsWindow
on findSubWindow(nameOfApp, subWinName)
-- Look for a sub-window of nameOfApp whose name contains subWinName
tell application "System Events" to tell process nameOfApp
set _wins to windows
set _ret to missing value
repeat with w in _wins
if name of w starts with subWinName then
set _ret to w
exit repeat
end if
end repeat
end tell
return _ret
end findSubWindow
on findParticipantsWindow()
tell application "System Events" to tell process appName
if exists scroll area 1 of window meetingWindow then
set participantWindow to window meetingWindow -- Participants panel.
else
set participantWindow to my findSubWindow(appName, "Participants")
end if
end tell
end findParticipantsWindow
on startParticipantWindow()
tell application "System Events" to tell process appName
my findParticipantsWindow()
if participantWindow is missing value then
my enableParticipantsWindow() -- participant window started
end if
my findParticipantsWindow()
end tell
end startParticipantWindow
on checkZoomMeetingRunning()
-- Make sure we are in a Zoom meeting
tell application "System Events" to tell process appName
set _startMeetingMenu to my findStatusMenu("Join a Meeting")
if _startMeetingMenu is not missing value then
set _msg to "The " & scriptName & " application needs a Zoom Meeting. Please join a meeting first."
error (_msg) number -3
end if
end tell
end checkZoomMeetingRunning
on appLogMessage(message)
my logMessage("=== " & scriptName & ": " & message, logFile)
end appLogMessage
on resetTrackigData()
do shell script "curl -X POST " & trackerURL & "/reset"
end resetTrackigData
on replaceText(theText, searchStr, replacementStr)
set theText to theText as text
if theText does not contain searchStr then
return theText -- return the original string if it doesn't contain the search string
end if
set oldDelims to AppleScript's text item delimiters -- save old delimiters
set AppleScript's text item delimiters to the searchStr
set theTextItems to every text item of theText
set AppleScript's text item delimiters to the replacementStr
set theText to theTextItems as string
set AppleScript's text item delimiters to oldDelims -- restore old delimiters
return theText
end replaceText
on trackList(_nameList, apiEndpoint)
if (count _nameList) is 0 then
return
end if
set _sanitized to {}
repeat with _p in _nameList
set _p to my replaceText(_p, "'", "-") -- Hack for people with single-quotes in their name
set _sanitized to _sanitized & {_p}
end repeat
set cmd to "curl -X PUT -H 'Content-Type: application/json' -d ' ["
set qList to {}
repeat with n in _sanitized
set qList to qList & {"\"" & n & "\""}
end repeat
set _tid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ","
set cmd to cmd & (qList as string) & "]' " & trackerURL & apiEndpoint
set AppleScript's text item delimiters to _tid
do shell script cmd
-- log cmd
end trackList
on trackListBatched(_nameList, apiEndpoint)
set _nameCount to count _nameList
repeat with i from 1 to _nameCount by batchCount
set endIndex to i + batchCount - 1
if endIndex > _nameCount then set endIndex to _nameCount
my trackList(items i thru endIndex of _nameList, apiEndpoint)
end repeat
end trackListBatched
on trackJoined(_nameList)
my trackListBatched(_nameList, "/joined_list")
end trackJoined
on trackWaiting(_nameList)
my trackListBatched(_nameList, "/waiting_list")
end trackWaiting
on runBackendServer()
-- check if the server is already running
set serverRunning to false
try
set jsonOutput to do shell script "curl -X GET " & trackerURL & "/health -H 'accept: application/json'"
if jsonOutput starts with "{" then set serverRunning to true
on error errMessage number errNum
error errMessage number errNum
end try
if serverRunning then
error "Server is already running. " & jsonOutput number -1
end if
set titleString to "\\033]0;" & scriptName & " Backend Server\\007"
set cmd to "echo -n -e \"" & titleString & "\";cd " & topLevelDirectory & "backend; . ../.venv/bin/activate; ./server.py; exit"
do shell script "open -a Terminal " & topLevelDirectory
tell application "Terminal"
activate
do script cmd in window 1
end tell
end runBackendServer
on killBackendServer()
tell application "System Events"
set _title to scriptName & " Backend Server"
set _win to missing value
try
set _win to first window of application process "Terminal" whose name contains _title
end try
if _win is missing value then
log "No backend server window found. Nothing to do."
return
end if
click UI Element 2 of _win
delay 0.2
set {_x, _y} to position of _win
my clickMouse(_x, _y)
keystroke "\r"
my clickMouse(_x, _y)
keystroke "\r"
end tell
end killBackendServer
on openDashboard()
do shell script "open " & topLevelDirectory & "frontend/index.html"
end openDashboard
on writeToRoster(message, filePath)
-- Usage:
-- set logFilePath to (path to desktop as string) & "roster.txt" -- Path to the roster file
-- my writeTeRoster("John Smith", logFilePath)
try
-- Open the log file for writing, creating it if it doesn't exist
set _log to open for access file filePath with write permission
my appendToFile(_log, message, zoomRosterDebug)
-- Close the log file
close access _log
on error errMsg number errNum
-- If an error occurs, close the log file (if it's open)
try
close access _log
end try
-- Optionally, report the error in some way
set _msg to "Error: " & errMsg & " (" & errNum & ")"
log _msg
display dialog _msg
end try
end writeToRoster
on writeToRosterPart(namesList, spacesPrefix)
-- Write a list of names to the roster.txt file
-- prepend each with the spacesPrefix
if (count namesList) is 0 then return
set i to 1
repeat with n in namesList
my writeToRoster(spacesPrefix & i & ". " & n, meetingRoster)
set i to i + 1
end repeat
end writeToRosterPart
on generateRoster()
set _joinedList to {}
set _waitingList to {}
set _joinedPrefix to "Joined "
set _waitingPrefix to "Waiting Room "
set _notJoinedPrefix to "Not Joined"
my loadRenameMappings()
tell application "System Events" to tell process appName
tell outline 1 of scroll area 1 of participantWindow
set allParticipantNames to get value of static text of UI element of rows
set _num to (count allParticipantNames)
set _inWaitingList to false
set _inJoinedList to false
repeat with i from 1 to _num
set _pName to item i of allParticipantNames as string
if _pName starts with _waitingPrefix then
set _inWaitingList to true
end if
if _pName starts with _joinedPrefix then
set _inWaitingList to false
set _inJoinedList to true
end if
if _pName starts with _notJoinedPrefix then exit repeat
if (i is 1) and (not _inWaitingList) and (not _inJoinedList) then
set _joinedList to allParticipantNames
exit repeat
end if
if _inWaitingList then
if not (_pName starts with _waitingPrefix) then
set _waitingList to _waitingList & {_pName}
end if
else
if not (_pName starts with _joinedPrefix) then
set _joinedList to _joinedList & {_pName}
end if
end if
end repeat
end tell
end tell
set _intro to "=== " & (my formatDateTime(current date)) & " ==="
my writeToRoster(_intro, meetingRoster)
if (count _waitingList) > 0 then
my writeToRoster("Waiting Room:", meetingRoster)
my writeToRosterPart(_waitingList, " ")
my writeToRoster("Joined:", meetingRoster)
my writeToRosterPart(_joinedList, " ")
else
my writeToRosterPart(_joinedList, "")
end if
set _num to (count _waitingList) + (count _joinedList)
if _num > 1 then
set plural to "s"
else
set plural to ""
end if
set _summary to "=== " & (_num as string) & " participant" & plural Â
& " " & (my formatDateTime(current date)) & " ==="
my writeToRoster(_summary, meetingRoster)
my trackWaiting(_waitingList)
my trackJoined(_joinedList)
-- Now, run our rename procedure if needed.
if renameMappings is missing value then return
set _renameToDo to {}
set _candidates to {}
repeat with _i in renameMappings
set end of _candidates to item 1 of _i as text
end repeat
repeat with participant in _joinedList
if participant as text is in _candidates then set end of _renameToDo to participant as text
end repeat
-- Now we go through each participant in _renameToDo and finish setting up the renaming.
repeat with _name in _renameToDo
set _res to my lookupRenaming(_name)
if _res is not missing value then
my renameParticipant(item 1 of _res, item 2 of _res, false)
end if
delay 0.4
end repeat
end generateRoster
on joinStringList(_strings)
set _tid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ":"
set _ret to _strings as string
set AppleScript's text item delimiters to _tid
return _ret
end joinStringList
on generateFilteredRoster(filterString)
-- Look for filter strings in the description strings in participants window
set _intro to "=== " & filterString & " " & (my formatDateTime(current date)) & " ==="
my writeToRoster(_intro, filteredRoster)
set _num to 0
tell application "System Events" to tell process appName
tell outline 1 of scroll area 1 of participantWindow
set allParticipantNames to get value of static text of UI element of rows
set numPart to (count allParticipantNames)
set allRows to rows
repeat with i from 1 to numPart
set pName to item i of allParticipantNames as string
set aRow to item i of allRows
set allElem to UI elements of (item 1 of UI element of aRow)
set allDescriptions to description of UI elements of (item 1 of UI element of aRow)
set joinedDescription to my joinStringList(allDescriptions)
if joinedDescription contains filterString then
set _num to _num + 1
set _msgToLog to (_num as string) & ". " & pName
my writeToRoster(_msgToLog, filteredRoster)
end if
end repeat
end tell
end tell
if _num > 1 then
set plural to "s"
else
set plural to ""
end if
set _summary to "=== " & filterString & " " & (_num as string) & " participant" & plural & Â
" " & (my formatDateTime(current date)) & " ==="
my writeToRoster(_summary, filteredRoster)
end generateFilteredRoster
on howManyWaiting(waitingRoomText)
-- Take a string like "Waiting Room (3)" and return the number in the parentheses
set {tid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "("}
set thePart to text item 2 of waitingRoomText
set AppleScript's text item delimiters to ")"
set theValue to text item 1 of thePart
set AppleScript's text item delimiters to tid -- restore original text item delimiters
return theValue as integer
end howManyWaiting
on clickSubElement(target, searchString)
-- Search for a button by description that is a
-- sub element of the target. This works for windows, or
-- other UI elements (outlines, scroll areas, etc.)
tell application "System Events" to tell process appName
set elems to every UI element of target
repeat with e in elems
if description of e is searchString as text then
click e
exit repeat
end if
end repeat
end tell
end clickSubElement
on letPeopleIn()
tell application "System Events" to tell process appName
tell outline 1 of scroll area 1 of participantWindow
-- Fetch all the rows in the Participants list.
set participantRows to every row
-- Check to see if Waiting Room exists.
set firstRowName to (get value of static text of UI element of row 1) as string
if firstRowName does not start with "Waiting Room " then
return -- Nothing to do here
end if
set allParticipantNames to get value of static text of UI element of rows
set _waitingNumber to my howManyWaiting(firstRowName)
if _waitingNumber > 1 then
-- We will find and click the "Admit All" button.
my clickSubElement(item 1 of UI element of row 1, "Admit All")
else
-- We will click the Admit button on row 2 (the participant)
my clickSubElement(item 1 of UI element of row 2, "Admit")
end if
delay 1
-- Now, we log all the people we admitted.
-- First, gather the list of people.
set myList to {}
repeat with _name in allParticipantNames
set _name to _name as text
if _name starts with "Joined " then
exit repeat
else
if not (_name starts with "Waiting Room ") then
set myList to myList & {_name}
my logMessage("Waiting Room: Admitted " & _name, logFile)
end if
end if
end repeat
my trackListBatched(myList, "/waiting_list")
end tell
end tell
end letPeopleIn
on createBreakoutRooms(breakoutFilename)
if breakoutFilename starts with "/" then
set borFilePath to breakoutFilename
else
set borFilePath to topLevelDirectory & breakoutFilename
end if
tell application "System Events"
if (not (exists file borFilePath)) then
error ("Missing breakout file: " & borFilePath) number -1
end if
set _size to size of (info for POSIX file borFilePath)
if _size is 0 then
error ("Empty breakout file: " & borFilePath) number -2
end if
end tell
set bFile to POSIX file (borFilePath)
set _file to open for access file bFile
set fileContents to read _file
close access _file
set rooms to my filterFileContents(paragraphs of fileContents)
set roomCount to (count rooms)
if roomCount is 0 then
return "No rooms are defined in the file " & borFilePath & " - exiting."
end if
set bor_clicked to my clickStatusMenu("Breakout")
if not bor_clicked then
return "Breakout room functionality not available. Make sure breakout rooms are enabled and you are a host or co-host."
end if
set _borWin to my findSubWindow(appName, "Breakout")
if name of _borWin contains "In Progress" then
return "Breakout rooms already started. Please close them first."
else
tell application "System Events" to tell process appName
click _borWin
if exists text field 1 of _borWin then
click text field 1 of _borWin
delay 0.5
keystroke " " & roomCount
delay 0.5
my clickSubElement(_borWin, "Assign manually")
my clickSubElement(_borWin, "Create")
else
my clickSubElement(_borWin, "Recreate")
delay 0.5
set _menuWinName to "Menu window"
if exists window _menuWinName then
key code 125
key code 125
keystroke " "
end if
keystroke "\t" & roomCount
delay 0.5
set w to window "All existing rooms will be replaced."
my clickSubElement(w, "Assign manually")
my clickSubElement(w, "Recreate")
end if
end tell
end if
-- Now, with the right number of rooms, we can rename them all.
-- We can use the _borWin window handle we already have.
tell application "System Events" to tell process appName to tell _borWin
tell table 1 of scroll area 1 of group 1
set _rows to rows
set i to 0
repeat with _r in _rows
set i to i + 1
click UI element 2 of UI element 1 of _r
delay 0.2
keystroke item i of rooms
delay 0.2
keystroke "\r"
delay 0.2
end repeat
end tell
end tell
end createBreakoutRooms
on getBreakoutRooms(breakoutFilename)
if breakoutFilename starts with "/" then
set borFilePath to breakoutFilename
else
set borFilePath to topLevelDirectory & breakoutFilename
end if
tell application "System Events"
if (exists file borFilePath) then
error ("Will not overwrite file: " & borFilePath) number -3
end if
end tell
set bFile to POSIX file (borFilePath)
set _file to open for access file bFile with write permission
set bor_clicked to my clickStatusMenu("Breakout")
if not bor_clicked then
return "Breakout room functionality not available. Make sure breakout rooms are enabled and you are a host or co-host."
end if
set _borWin to my findSubWindow(appName, "Breakout")
set _title to (name of _borWin)
set logEntry to "# === " & (my formatDateTime(current date)) & " " & _title
my appendToFile(_file, logEntry, zoomRosterDebug)
tell application "System Events" to tell process appName to tell _borWin
tell table 1 of scroll area 1 of group 1
set _rows to rows
set rooms to {}
repeat with _r in _rows
set _room to description of UI element 1 of UI element 1 of _r as string
if _room is "text"
set _room to "# " & name of UI element 1 of UI element 1 of _r as string
end if
set end of rooms to _room
end repeat
end tell
end tell
repeat with eachRoom in rooms
my appendToFile(_file, eachRoom as text, zoomRosterDebug)
end repeat
close access _file
end getBreakoutRooms
on moveMouse(x, y)
do shell script cliclick & " m:=" & x & ",=" & y
end moveMouse
on clickMouse(x, y)
do shell script cliclick & " c:=" & x & ",=" & y
end clickMouse
on useParticipantMenu(cmd, oldName, newName, verbose)
set _canRenameOthers to false
set _renamingMe to false
tell application "System Events" to tell process appName
tell outline 1 of scroll area 1 of participantWindow
set allParticipantNames to get value of static text of UI element of rows
set _myName to item 1 of allParticipantNames as string
set _renamingMe to (_myName is oldName)
set _userIsHost to (_myName contains "(Host, me)")
set _userIsCoHost to (_myName contains "(Co-host, me)")
set _canRenameOthers to _userIsHost or _userIsCoHost
end tell
end tell
if cmd is "rename" then
if not _renamingMe and not _canRenameOthers then
if verbose then
log "ERROR: You need to be Host or Co-Host to rename other partcipants."
return
end if
end if
if not _renamingMe and oldName ends with "(Host)" or oldName ends with "(Co-host)" then
log "ERROR: You can not rename a Host or Co-host."
return
end if
else if cmd is "co-host" then
if not _userIsHost then
log "ERROR: You need to be Host to grant or withdraw Co-Host to other partcipants."
return
end if
else if cmd is "host" then
if not _userIsHost then
log "ERROR: You need to be Host to reilnquish Host to other partcipants."
return
end if
end if
tell application appName to activate
tell application "System Events" to tell process appName
tell outline 1 of scroll area 1 of participantWindow
repeat with _r in rows
if (get value of static text of UI element of _r) as text is oldName then
set {_x, _y} to position of UI element 1 of UI element 1 of _r
my moveMouse(_x, _y)
set {_x2, _y2} to position of UI element 3 of UI element 1 of _r
set _drag to (_x2 - _x) as integer
do shell script cliclick & " c:+" & _drag & ",+0"
delay 0.5
tell menu 1 of UI element 1 of _r
if cmd is "rename" then
click menu item "Rename"
delay 0.5
keystroke newName
keystroke "\r"
my logMessage("Renamed: " & oldName & " => " & newName, logFile)
exit repeat
else if cmd is "co-host" then
if exists menu item "Make Co-host" then
click menu item "Make Co-host"
delay 0.5
keystroke "\r"
my logMessage("Made co-host: " & oldName, logFile)
else if exists menu item "Withdraw Co-host Permission" then
click menu item "Withdraw Co-host Permission"
my logMessage("Revoked co-host: " & oldName, logFile)
end if
exit repeat
else if cmd is "host" then
if exists menu item "Make Host" then
click menu item "Make Host"
delay 0.5
keystroke "\r"
my logMessage("Gave host to: " & oldName, logFile)
end if
end if
end tell
end if
end repeat
end tell
end tell
end useParticipantMenu
on renameParticipant(oldName, newName, verbose)
my useParticipantMenu("rename", oldName, newName, verbose)
end renameParticipant
on coHostParticipant(participantName)
my useParticipantMenu("co-host", participantName, "", true)
end coHostParticipant
on giveHostToParticipant(participantName)
my useParticipantMenu("host", participantName, "", true)
end coHostParticipant
on doBreakoutRooms(borCommand, filename)
if borCommand is "create" then
return my createBreakoutRooms(filename)
end if
if borCommand is "get" then
return my getBreakoutRooms(filename)
end if
log "Error: Unknown breakout subcommand: " & borCommand
return my usageMessage("breakout")
end doBreakoutRooms
on run argv
set argCount to (count argv)
if (argCount > 0) then
set arg to item 1 of argv
else
set arg to "roster"
end if
if arg is not in knownCommands then log "Error: unknown command: " & arg
if arg is "help" or arg is not in knownCommands then
return my usageMessage("top")
else if arg is "reset" then
return my resetTrackigData()
end if
my setUpLogFiles()
if arg is "server" then
if argCount is not 2 then
return my usageMessage("server")
end if
set _action to (item 2 of argv)
if _action is not in {"start", "stop"} then
log "Error: unknown action for \"server\" command: " & _action
return my usageMessage("server")
end if
if _action is "start" then return my runBackendServer()
-- Only possible action now is "stop"
return my killBackendServer()
end if
if arg is "dashboard" then
return my openDashboard()
end if
my appLogMessage("START " & arg)
try
my checkZoomRunning()
my logMessage("Zoom Version: " & appVersion, logFile)
my checkZoomMeetingRunning()
my startParticipantWindow()
if arg is "roster" then
my generateRoster()
else if arg is "hands" then
my generateFilteredRoster("Hand raised")
else if arg is "camera_off" then
my generateFilteredRoster("Video off")
else if arg is "camera_on" then
my generateFilteredRoster("Video on")
else if arg is "phone" then
my generateFilteredRoster("Telephone")
else if arg is "no_audio" then
my generateFilteredRoster("No Audio")
else if arg is "muted" then
my generateFilteredRoster(" muted")
else if arg is "unmuted" then
my generateFilteredRoster(" unmuted")
else if arg is "admit" then
my letPeopleIn()
else if arg is "breakout" then
if argCount is not 3 then
log my usageMessage("breakout")
else
log my doBreakoutRooms(item 2 of argv, item 3 of argv)
end if
else if arg is "rename" then
if argCount is not 3 then
log my usageMessage("rename")
else
log my renameParticipant(item 2 of argv, item 3 of argv, true)
end if
else if arg is "co-host" then