Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions docs/automation-design.md
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.
Comment on lines +177 to +179
Copy link

Copilot AI Mar 20, 2026

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.lastAction includes participant identifiers derived from display names and is written via innerHTML in the userscript. Either adjust this documentation claim or update the implementation to escape/avoid HTML injection so the statement is accurate.

Suggested change
- 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.
- The debug panel uses `innerHTML` with string interpolation for internal
status messages; values are numeric counters or strings taken from the Zoom
DOM (for example, participant identifiers derived from display names).

Copilot uses AI. Check for mistakes.
- 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 |
127 changes: 127 additions & 0 deletions docs/testing-checklist.md
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.
Loading
Loading