forked from lightpanda-io/browser
-
Notifications
You must be signed in to change notification settings - Fork 0
Add Zoom Web co-host automation: auto-grant Multi-Pin on raised hand #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
2
commits into
main
Choose a base branch
from
copilot/zoom-cohost-multipin-automation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,210
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| # Zoom Host Tools — Automation Design | ||
|
|
||
| ## Overview | ||
|
|
||
| `zoom-host-tools.user.js` is a Tampermonkey userscript that automates routine | ||
| Host / Co-Host tasks inside the **Zoom Web** client (`*.zoom.us/wc/*` and | ||
| `*.zoom.us/j/*`). It runs entirely in the browser — no backend, no external | ||
| services, no Zoom API credentials required. | ||
|
|
||
| The script is structured in **three phases**: | ||
|
|
||
| | Phase | Status | Description | | ||
| |-------|--------|-------------| | ||
| | 1 | **Fully implemented** | Detect raised hands → auto-grant Multi-Pin | | ||
| | 2 | Scaffold | After Multi-Pin grant, check camera state; optionally send a reminder | | ||
| | 3 | Scaffold | Monitor chat for spam/suspicious links | | ||
|
|
||
| --- | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| zoom-host-tools.user.js | ||
| │ | ||
| ├── CONFIG Global tuning knobs (intervals, retries, debug flag) | ||
| ├── STATE Runtime state (processed participants set, debug stats) | ||
| │ | ||
| ├── Logging layer log(level, ...args) — respects CONFIG.DEBUG | ||
| │ | ||
| ├── Selector layer SELECTORS map + resolveElement() + resolveAllElements() | ||
| │ └── Falls back through candidates array; logs selector mismatches as warnings | ||
| │ | ||
| ├── Participant identity getParticipantId(row) — stable ID, name, or fingerprint | ||
| │ | ||
| ├── Phase 1 — Multi-Pin | ||
| │ ├── hasRaisedHand(row) | ||
| │ ├── alreadyProcessed(participantId) | ||
| │ ├── grantMultiPin(row, participantId) ← async, handles menu open/click/retry | ||
| │ └── scanParticipants() ← called every SCAN_INTERVAL_MS | ||
| │ | ||
| ├── Phase 2 scaffold | ||
| │ └── checkCameraStatus(row, participantId) ← called after every grant | ||
| │ | ||
| ├── Phase 3 scaffold | ||
| │ ├── checkMessageForSpam(text, sender) | ||
| │ └── startChatMonitor() ← MutationObserver on chat container | ||
| │ | ||
| ├── Debug panel createDebugPanel() / updateDebugPanel() | ||
| │ | ||
| └── Entry point waitForZoomReady() → startMainLoop() | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## DOM Strategy | ||
|
|
||
| ### Selector Mapping Layer | ||
|
|
||
| All CSS selectors are defined in two places that must be kept in sync: | ||
|
|
||
| 1. **`/selectors/zoom-dom-selectors.json`** — canonical source of truth. Edit | ||
| this file to update selectors after a Zoom UI change. The JSON contains | ||
| multiple `candidates` per element, plus human-readable fallback notes. | ||
|
|
||
| 2. **`SELECTORS` constant in `zoom-host-tools.user.js`** — an inline copy of | ||
| the candidate arrays so the userscript works as a single self-contained file. | ||
|
|
||
| When Zoom changes its DOM, update the JSON first, then copy the relevant | ||
| `candidates` arrays into the `SELECTORS` constant in the script. | ||
|
|
||
| ### Fallback Strategy (per element) | ||
|
|
||
| ``` | ||
| Priority 1 → data-testid attribute selectors (most stable; Zoom uses these internally) | ||
| Priority 2 → explicit aria-label attribute (accessibility labels; change less often) | ||
| Priority 3 → class name selectors (liable to change with UI rebuilds) | ||
| Priority 4 → text content / aria-label keywords (last resort; language-dependent) | ||
| Priority 5 → structural DOM traversal (absolute fallback) | ||
| ``` | ||
|
|
||
| `resolveElement()` iterates the `candidates` array and returns the first match. | ||
| A `[WARN]` log is emitted for any selector that throws (malformed selector) and | ||
| a `[DEBUG]` log for every successful match, making selector debugging easy. | ||
|
|
||
| --- | ||
|
|
||
| ## Assumptions | ||
|
|
||
| 1. **Permissions** — The script assumes the logged-in Zoom user has Host or | ||
| Co-Host permissions. It does not verify this; attempting actions without | ||
| permissions will simply result in the menu item being absent or greyed out. | ||
|
|
||
| 2. **Participant panel must be open** — The script can only scan participants | ||
| when the Participants panel is visible. If it is closed, `getParticipantListContainer()` | ||
| returns `null` and the scan is skipped silently. | ||
|
|
||
| 3. **Hover reveals menu button** — Zoom hides the "…" (More) button until the | ||
| participant row is hovered. The script dispatches synthetic `mouseover` / | ||
| `mouseenter` / `mousemove` events to reveal it before looking for the button. | ||
| This approach may break if Zoom adds a CSP or replaces hover with a click | ||
| trigger. | ||
|
|
||
| 4. **Single-session state** — `STATE.processedParticipants` is an in-memory | ||
| `Set`. It is reset when the page reloads (e.g. the user rejoins the meeting). | ||
| This is intentional: a participant who left and rejoin should be re-evaluated. | ||
|
|
||
| 5. **Multi-Pin menu item text** — The "Allow to Multi-Pin" text is used as the | ||
| final fallback. If Zoom localises this string, `MULTIPIN_TEXT_KEYWORDS` in | ||
| the script must be updated. | ||
|
|
||
| 6. **Camera detection is uncertain** — Zoom Web does not expose a reliable | ||
| DOM-level camera on/off indicator in all versions. Phase 2's camera check is | ||
| best-effort; it logs the result but does not act on uncertainty. | ||
|
|
||
| --- | ||
|
|
||
| ## Extension Points | ||
|
|
||
| ### Adding a new Phase 2 action | ||
|
|
||
| After `grantMultiPin()` calls `checkCameraStatus()`, add your logic inside that | ||
| function: | ||
|
|
||
| ```js | ||
| if (cameraOff) { | ||
| // Phase 2: send one-time chat message | ||
| await sendChatMessage(participantName, 'Please turn your camera on to use Multi-Pin.'); | ||
| } | ||
| ``` | ||
|
|
||
| Implement `sendChatMessage(target, message)` using the chat input selectors in | ||
| `SELECTORS.chatInput` and `SELECTORS.chatSendButton`. | ||
|
|
||
| ### Adding a Phase 3 moderation action | ||
|
|
||
| Inside `checkMessageForSpam()`, replace the `// TODO` comment with a call to | ||
| your moderation function: | ||
|
|
||
| ```js | ||
| if (pattern.test(text)) { | ||
| log('warn', `Spam detected from "${sender}": ${text}`); | ||
| await moderateUser(sender); // e.g. mute, remove, or warn | ||
| } | ||
| ``` | ||
|
|
||
| ### Adding new spam patterns | ||
|
|
||
| Edit `CONFIG.SPAM_PATTERNS` at the top of the script. Each entry is a | ||
| `RegExp`: | ||
|
|
||
| ```js | ||
| SPAM_PATTERNS: [ | ||
| /https?:\/\//i, | ||
| /t\.me\//i, | ||
| /yournewthing\.com/i, // <-- add here | ||
| ], | ||
| ``` | ||
|
|
||
| ### Updating selectors after a Zoom UI change | ||
|
|
||
| 1. Open the Zoom Web client in Chrome DevTools. | ||
| 2. Inspect the affected element. | ||
| 3. Update the `candidates` array for that element in | ||
| `/selectors/zoom-dom-selectors.json`. | ||
| 4. Copy the updated array into the matching entry in the `SELECTORS` constant | ||
| in `zoom-host-tools.user.js`. | ||
| 5. Reload the script in Tampermonkey and verify in the browser console that the | ||
| `[DEBUG] Selector matched` log points at your new selector. | ||
|
|
||
| --- | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| - The script runs entirely in the browser under the user's existing Zoom session. | ||
| - No credentials are stored or transmitted. | ||
| - No external resources are loaded. | ||
| - The debug panel uses `innerHTML` with string interpolation; all values | ||
| inserted are numeric counters or sanitised strings from the DOM — no user | ||
| input is ever inserted raw. | ||
| - DOM events dispatched (`mouseover`, `keydown`) are standard synthetic events | ||
| and do not exfiltrate data. | ||
|
|
||
| --- | ||
|
|
||
| ## Files | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `scripts/zoom-host-tools.user.js` | Main Tampermonkey userscript | | ||
| | `selectors/zoom-dom-selectors.json` | Selector map and fallback notes | | ||
| | `docs/automation-design.md` | This file — architecture and assumptions | | ||
| | `docs/testing-checklist.md` | Manual test plan | | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| # Zoom Host Tools — Manual Testing Checklist | ||
|
|
||
| Use this checklist to verify that the script is working correctly in a live or | ||
| test Zoom Web meeting. All tests assume you are logged in with **Host** or | ||
| **Co-Host** permissions. | ||
|
|
||
| --- | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - [ ] Tampermonkey (or Violentmonkey) extension installed in Chrome / Firefox / Edge | ||
| - [ ] `zoom-host-tools.user.js` installed in Tampermonkey and enabled | ||
| - [ ] A test Zoom Web meeting open at `https://*.zoom.us/wc/*` or `https://*.zoom.us/j/*` | ||
| - [ ] At least two participants in the meeting (one as host/co-host, one as a test participant) | ||
| - [ ] Browser DevTools console open (F12 → Console) so you can read script logs | ||
|
|
||
| --- | ||
|
|
||
| ## 1 — Script Load | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 1.1 | Open a Zoom Web meeting URL | Script matches the `@match` URL patterns | | | ||
| | 1.2 | Check browser console | `[ZoomHostTools] [INFO] Waiting for Zoom Web to be ready…` log appears | | | ||
| | 1.3 | Wait ~5 seconds after meeting loads | `[ZoomHostTools] [INFO] Zoom Web ready. Starting automation.` log appears | | | ||
| | 1.4 | Check bottom-right of page | A small dark debug panel labelled "🔧 ZoomHostTools" appears | | | ||
| | 1.5 | Open `about:blank` in a new tab | No script logs appear (URL does not match `@match`) | | | ||
|
|
||
| --- | ||
|
|
||
| ## 2 — Raised Hand Detection | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 2.1 | Open the Participants panel in Zoom | Debug panel "Scans" counter increments every ~2.5 seconds | | | ||
| | 2.2 | Test participant raises their hand | Console log: `[ZoomHostTools] [INFO] Detected raised hand: name:<ParticipantName>` | | | ||
| | 2.3 | Test participant lowers their hand | On the next scan, no raised-hand log for that participant | | | ||
| | 2.4 | Close the Participants panel | No error logs appear; scans continue silently | | | ||
|
|
||
| --- | ||
|
|
||
| ## 3 — Multi-Pin Grant (Phase 1 — Core Feature) | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 3.1 | Test participant raises their hand (first time) | Console: `[INFO] Granting multipin for participant: name:<ParticipantName>` | | | ||
| | 3.2 | Same scan | Participant's action menu opens briefly and closes | | | ||
| | 3.3 | Same scan | Console: `[INFO] Multi-Pin granted successfully for participant: name:<ParticipantName>` | | | ||
| | 3.4 | Verify in Zoom | Participant now has Multi-Pin permission (visible in Zoom participant list or menu) | | | ||
| | 3.5 | Debug panel | "Grants attempted" counter increments by 1 | | | ||
|
|
||
| --- | ||
|
|
||
| ## 4 — Deduplication (No Repeated Grants) | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 4.1 | Keep test participant's hand raised after grant | On next scan, console: `[DEBUG] Participant already processed, skipping: name:<ParticipantName>` | | | ||
| | 4.2 | Test participant lowers and re-raises hand | Participant is already in the processed set — no repeated grant | | | ||
| | 4.3 | Reload the page and repeat 3.1 | Grant executes again (state was reset on reload — expected) | | | ||
|
|
||
| --- | ||
|
|
||
| ## 5 — Error Handling and Selector Failures | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 5.1 | Temporarily change a selector in `SELECTORS` to `"[data-testid='does-not-exist']"` | Console: `[WARN] Failed to find menu button for participant: …` — script does NOT crash | | | ||
| | 5.2 | Open a non-Zoom HTTPS page | No logs, no debug panel (URL does not match) | | | ||
| | 5.3 | Revoke Co-Host before a grant attempt | Menu opens; "Allow to Multi-Pin" is absent; console: `[WARN] Failed to find Multi-Pin menu item for participant: …` | | | ||
| | 5.4 | Use DevTools to delete the participant list container from the DOM | Console: `[DEBUG] Participant list container not found` — script continues to run | | | ||
|
|
||
| --- | ||
|
|
||
| ## 6 — Selector Debugging Workflow | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 6.1 | Enable `CONFIG.DEBUG = true` | All `[DEBUG]` logs visible in console | | | ||
| | 6.2 | Trigger a participant scan | Console shows `[DEBUG] Selector matched: "<selector>"` for each element found | | | ||
| | 6.3 | Update a selector to a new value in the script | Console immediately shows the new selector in the "matched" log | | | ||
| | 6.4 | View `selectors/zoom-dom-selectors.json` | File contains `candidates` arrays and `fallbackStrategy` notes for every element | | | ||
|
|
||
| --- | ||
|
|
||
| ## 7 — Phase 2 Camera Check (Scaffold Verification) | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 7.1 | Grant Multi-Pin to a participant with camera ON | Console: `[INFO] Camera appears ON for participant: …` OR `[DEBUG] Camera status: unable to detect…` | | | ||
| | 7.2 | Grant Multi-Pin to a participant with camera OFF | Console: `[INFO] Camera is OFF for participant: …` OR `[DEBUG] Camera status: unable to detect…` | | | ||
| | 7.3 | Review Phase 2 TODO comments in script | `checkCameraStatus()` contains clear `// TODO (Phase 2):` comments describing next steps | | | ||
|
|
||
| --- | ||
|
|
||
| ## 8 — Phase 3 Chat Monitoring (Scaffold Verification) | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 8.1 | Send a normal chat message | No warning log | | | ||
| | 8.2 | Send a chat message containing `https://example.com` | Console: `[WARN] Potential spam detected from "…": …` | | | ||
| | 8.3 | Send a message containing `t.me/something` | Console: `[WARN] Potential spam detected from "…": …` | | | ||
| | 8.4 | Send a message containing `discord.gg/invite` | Console: `[WARN] Potential spam detected from "…": …` | | | ||
| | 8.5 | Review Phase 3 TODO comments in script | `checkMessageForSpam()` has clear `// TODO (Phase 3):` comments for future moderation actions | | | ||
|
|
||
| --- | ||
|
|
||
| ## 9 — Extension and Maintainability | ||
|
|
||
| | # | Test | Expected Result | Pass/Fail | | ||
| |---|------|-----------------|-----------| | ||
| | 9.1 | Read `docs/automation-design.md` | Architecture, assumptions, and extension points are clearly documented | | | ||
| | 9.2 | Find all `// TODO` comments in the script | Each TODO belongs to a clearly labelled Phase (2 or 3) | | | ||
| | 9.3 | Identify where to add a new spam pattern | `CONFIG.SPAM_PATTERNS` array at the top of the script — one line to add | | | ||
| | 9.4 | Identify where to update a broken selector | `SELECTORS` constant in the script and matching entry in `selectors/zoom-dom-selectors.json` | | | ||
|
|
||
| --- | ||
|
|
||
| ## Notes | ||
|
|
||
| - Zoom Web's DOM structure can change with any Zoom update. If any test in | ||
| section 3 fails, compare the live DOM in DevTools against the selectors in | ||
| `SELECTORS` and update the `candidates` arrays. | ||
| - Camera detection (section 7) is explicitly best-effort. Failure to detect | ||
| camera state is not a bug — see Phase 2 design notes in `automation-design.md`. | ||
| - All Phase 3 chat tests only verify logging; no automated moderation action | ||
| should be triggered at this stage. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security note says the debug panel only interpolates “sanitised strings from the DOM — no user input is ever inserted raw”, but
STATE.stats.lastActionincludes participant identifiers derived from display names and is written viainnerHTMLin the userscript. Either adjust this documentation claim or update the implementation to escape/avoid HTML injection so the statement is accurate.