commit 6d29ee148532961efccdfe7e8f150a722afc26fc Author: Kibi Kelburton Date: Wed Sep 30 23:28:43 2026 +0200 Initial commit: mumh5, a modern Mumble client Electron desktop app with a Svelte 5 interface for any Mumble server. - Mumble protocol core: TLS, handshake, channels, users, text, plugin data, client-side pacing of Murmur's rate limits - Voice: WebCodecs Opus over the TCP tunnel, voice activity, push to talk, always-on, devices, per-user volume and local mute - Several servers at once, voice on one; server rail with icons and ordering - Chat: channels, direct messages, side chat, file sharing through f0ckm, inline images without it, click-to-play YouTube - Profiles with rich descriptions, registration, rename, nicknames, connection information and moderation menus - Identity wizard, multiple identities, PKCS#12 import/export, desktop Mumble certificate import, certificate pinning and viewer - Tray icon with voice state - Unit, server and end-to-end tests against a real Murmur Co-Authored-By: Claude Opus 5.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2aa68c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +dist-web/ +dist-electron/ +release/ +*.log diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6d14157 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# CLAUDE.md + +## Style rules + +- No rounded corners anywhere. No `border-radius` (a global `border-radius: 0 !important` backs this up in `src/app.css`). +- No emojis in code, UI text, comments, commit messages or docs. Emoji inside chat content from users is fine. +- UI copy is short, plain and honest. Don't claim something works if it is not tested. Explanations go in tooltips or help text, not in big callouts. +- Colors come from the CSS variables in `src/app.css`; icons from `src/lib/icons.ts` via ``. + +## Project overview + +mumh5 is a Mumble client: Electron desktop app (Windows, macOS, Linux), Svelte 5 + TypeScript + Vite UI, Discord-like layout, responsive down to phone width. It must stay compatible with stock Murmur servers and with people on the desktop Mumble client. A browser build (through a self-hosted WebSocket proxy with a server allowlist) is planned. + +File sharing uploads to a f0ckm instance (`../f0ckm`, `src/chat_upload_handler.mjs` there) with an upload-only key; links are posted in chat. + +## Commands + +```bash +npm run dev # Vite + Electron, restarts Electron when main/preload change +npm run check # svelte-check; must report 0 errors and 0 warnings +npm test # unit tests; server tests too when MUMBLE_TEST_HOST is set +npm run build # renderer to dist/, main+preload to dist-electron/ (esbuild) +npm run test:e2e # drives the built app (needs npm run build first) +npm run screenshots # regenerates docs/screenshots (see test/e2e/screenshots.ts) +npm run proto # regenerate src/core/mumble-pb.js and mumble-udp-pb.js from proto/ +``` + +Test server (autoban off because tests reconnect a lot; SuperUser for channel setup): + +```bash +docker run -d --name mumh5-test-murmur -p 64739:64738 -p 64739:64738/udp \ + -e MUMBLE_CONFIG_AUTOBANATTEMPTS=0 -e MUMBLE_SUPERUSER_PASSWORD=testsuper mumblevoip/mumble-server +MUMBLE_TEST_HOST=localhost:64739 MUMBLE_SUPERUSER_PASSWORD=testsuper npm run test:e2e +``` + +A reused test server keeps registrations and channels from earlier runs; tests must use unique names or clean up (the E2E removes its channels first). + +## Architecture + +- `electron/` (Node, main process): window, TLS sockets to Mumble servers (`tls-transport.ts`), identities and PKCS#12 (`identity.ts`, `identity-store.ts`), certificate parsing (`certs.ts`), tray (`tray.ts`). The renderer only gets the narrow `window.mumh5Native` API from `preload.ts` (context isolation, sandbox). +- `src/core/` (browser-safe TypeScript, also runs in Node for tests): framing and codec (`proto.ts`), the Mumble client state machine (`client.ts`), voice packet formats (`voice-packet.ts`). No DOM, no Electron, no Node imports here. +- `src/lib/`: app state. `session.svelte.ts` has one `Session` per server plus the `sessions` manager; `session` is a Proxy to the active one. `audio/voice.svelte.ts` is the voice engine (WebCodecs Opus, capture and playback AudioWorklets). `html.ts` sanitizes incoming HTML and serializes outgoing rich text. +- `src/ui/`: Svelte components. `App.svelte` owns layout and global dialogs (`ui.svelte.ts` store). + +Voice runs on one server at a time (where you last joined a channel); background servers are auto self-deafened and restored when voice returns. + +## Conventions + +- Svelte 5 runes only. Comment density: short comments that explain why, not what. +- The protobuf codec is generated static code (no eval) because the CSP forbids `unsafe-eval`. Field names keep snake_case from the .proto. +- TypeScript files imported by Node tests use `.ts` extensions and erasable syntax only (no parameter properties, no enums). +- New features get an E2E step in `test/e2e/app.e2e.ts`; protocol logic gets unit or server tests. +- Privacy rule: opening a chat must never contact hosts the user did not choose. Remote media only from the upload host and user-listed hosts; YouTube is click-to-play (`youtube-nocookie.com`). + +## Svelte reactivity pitfalls (all hit before) + +- `$state` wraps assigned objects in proxies: `this.server !== server` compares proxy to raw object and is always true. Use ids or counters. +- `(obj[key] ??= []).push(x)` pushes to the raw array; assign first, then push through `obj[key]`. +- The Mumble client mutates its objects in place. Session getters return shallow copies and read `tick`, otherwise views don't update. + +## Mumble protocol pitfalls (all hit before) + +- Messages or comments longer than the server's text limit (5000) are parsed by Murmur as XML; anything not well-formed is denied as TextTooLong. Always emit XHTML (`
`, ``, `toMumbleHtml`). +- Murmur silently drops own UserState, TextMessage, ChannelState, ACL and Version past a leaky bucket (burst 5, 1/s). The client paces these; don't bypass `send()`. +- 1.5 servers use the protobuf UDP voice format (type byte 0 + MumbleUDP.Audio) with clients announcing 1.5; older ones the legacy format. The UDPTunnel TCP body is the raw voice packet, not a protobuf message. Sequence numbers count 10 ms frames. +- Long comments and descriptions arrive as a hash only; a new hash invalidates the old text; fetch with RequestBlob. +- Renaming while connected is not possible; mumh5 reconnects with the new name. + +## Running the app in this environment + +- The shell has `ELECTRON_RUN_AS_NODE=1` set; strip it when launching Electron (the dev script and tests do). +- The desktop is Wayland and Electron picks it automatically, so windows appear on the user's screen even with `DISPLAY=:99`. For hidden runs: `Xvfb :99`, `--ozone-platform=x11`, and unset `WAYLAND_DISPLAY` and `XDG_SESSION_TYPE`. +- The user often has their own mumh5 (`npm run dev`) running. Only stop processes started with a `mumh5-e2e` profile; never kill Electron processes in general. +- `pkill -f pattern` matches the invoking shell; use `pkill -f "[p]attern"`. + +## Commits + +End commit messages with `Co-Authored-By: Claude ` style attribution (the exact line comes from the session). No emojis in commit messages. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b47a350 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Kibi Kelburton + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..aeabde0 --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +# mumh5 + +**Mumble, the way it should feel in 2026.** + +mumh5 is a modern client for [Mumble](https://www.mumble.info/), the free, open source, low-latency voice chat. It gives Mumble the kind of interface people know from Discord: servers on the left, channels and people in the middle, chat that can do more than text. Underneath, it's plain Mumble, so it connects to **any existing Mumble server** without changing anything on the server side. + +Your community keeps its own server, its own rules and its own data. You get an app that's nice to use. + +![mumh5 main window: channel tree, chat with a YouTube card, member list](docs/screenshots/main.png) + +--- + +## Why mumh5 + +Mumble has the best foundation of any voice chat: tiny latency, strong encryption, self-hosting, no accounts, no ads, no tracking. What held it back was the desktop client, which still looks and behaves like 2010. + +mumh5 keeps the foundation and replaces the experience. + +- **Works with the servers you already use.** No plugins, no server changes, no migration. Friends on the regular Mumble client see you and hear you like anyone else. +- **Feels familiar.** A server rail, a channel tree, a real chat with message grouping, profiles, right-click menus everywhere. Anyone who has used Discord knows where things are. +- **Chat that is actually useful.** Drop in files, paste screenshots, share videos and YouTube links. Formatting works, links work, and everything stays compatible with the desktop client. +- **Several servers at once.** Stay in the text chat of every server you like, with unread badges, while voice runs where you actually are. +- **Private by default.** Opening a chat never contacts servers you didn't choose. No remote images or trackers, and YouTube stays click-to-play. +- **Runs everywhere.** Windows, macOS and Linux, with a layout that also works on narrow windows and small screens. + +### mumh5 compared to the Mumble desktop client + +| | Mumble desktop | mumh5 | +|---|---|---| +| Interface | Qt widgets, dense dialogs | Modern layout: server rail, channel tree, chat, side panels | +| Several servers at the same time | One connection | Many; voice on one, text on all, unread badges | +| Sharing files | Images only, pasted inline and heavily limited | Any allowed file through your own [f0ckm](#file-sharing-with-f0ckm) host, inline players for images, video and audio. Images still work without one | +| YouTube links | Plain links | Click-to-play player (privacy mode), optional thumbnails | +| Chat per channel | One shared log for everything | Separate chats per channel and person, plus a side chat for any channel next to your own | +| Descriptions | Rich text editor | Rich text editor with pictures, and a live size counter against the server limit | +| Identities | One certificate per installation | Several identities, a different one per server if you like; imports your desktop certificate | +| Server certificate trust | Remembered after a warning dialog | Pinned on first use, green header for CA-signed servers, full chain viewer | +| Fast actions in a row | The server silently drops them past its rate limit | Paced to the server's limit, nothing gets lost | +| Right-click on mute or deafen | Nothing | Quick device and volume options | + +**Where the desktop client is still ahead, today:** UDP voice transport (mumh5 sends voice through the encrypted TCP connection for now, which adds a little delay on bad networks), system-wide push-to-talk, whisper and shout, positional audio, the in-game overlay, recording and the ACL editor. These are on the roadmap below. + +--- + +## Features + +### Voice +- Opus voice with voice activity detection, push to talk or always-on +- Live level meter with an adjustable threshold, so you set it once and forget it +- Noise suppression, echo cancellation and automatic gain control +- Microphone and output device selection, input and output volume +- Per-person volume (0 to 300%) and "mute for me" +- Speaking indicators in the channel tree, member list and your own panel +- Bitrate automatically limited to what the server allows +- Right-click the mute or deafen button for quick device and volume options +- Hear-yourself microphone test + +![Voice settings](docs/screenshots/voice-settings.png) + +### Channels and people +- Channel tree that starts collapsed; channels with people in them open up +- Single click previews a channel's description, double click joins +- Side chat: write in any channel without leaving yours +- Profiles with rich descriptions (formatting, lists, links, pictures), local nicknames, registration status +- Change your name (mumh5 reconnects for you) and register on a server with one click +- Connection information: ping, packet loss, client version and, for admins, the user's certificate chain +- Moderation from the right-click menu: move, server mute and deafen, priority speaker, kick, ban, register + +![Profile with a formatted description](docs/screenshots/profile.png) + +### Chat +- Messages grouped by author, direct messages, unread badges +- File sharing: drag and drop, paste, or the attach button, with upload progress +- Inline images, video and audio players for shared media +- YouTube links as click-to-play players +- Incoming HTML is sanitized; remote images from unknown hosts are never loaded +- Messages stay compatible with the desktop client, including picture previews + +### Servers and identity +- Server rail with custom icons, drag-and-drop ordering, connection status and unread counts +- Several servers connected at once; voice follows the channel you join +- Identity setup wizard: create a new certificate, import a `.p12`, or reuse your desktop Mumble identity +- Password-protected backups of your identity +- Trust on first use for server certificates, a green header for CA-signed ones, and a certificate viewer +- Tray icon that shows whether you are talking, muted or deafened + +

mumh5 in a narrow window

+ +--- + +## Getting started + +There are no release builds yet. For now, build it yourself; it takes a few minutes. + +You need [Node.js](https://nodejs.org/) 22 or newer and git. + +```bash +git clone gitea@git.lat:kibi/mumh5.git +cd mumh5 +npm install +npm run dev +``` + +On first start, mumh5 asks you to set up your identity. If you already use Mumble on this computer, pick "Use my Mumble desktop identity" to keep your registrations. Then add a server with the plus button. + +### Building installers + +```bash +npm run dist:linux # AppImage and .deb +npm run dist:win # NSIS installer +npm run dist:mac # .dmg +``` + +Installers are written to `release/`. Build each platform on that platform for the best results. + +--- + +## File sharing with f0ckm + +Mumble can only carry small inline images. mumh5 can share any file by uploading it to a f0ckm instance (a self-hosted imageboard by the same author) and posting the link. Other mumh5 users get inline players; desktop Mumble users get a normal link plus a small preview. + +f0ckm keeps these uploads separate from its imageboard, respects its own list of allowed file types, and deletes them after 30 days by default. + +1. On your f0ckm server, create an upload-only key: + ```bash + node scripts/chat-upload-key.mjs create mumh5 + ``` +2. In mumh5, open **Settings, Chat and files**, enter the f0ckm address and the key, and press **Test key**. + +Without an upload host, mumh5 still sends images, scaled to fit the server's limit, the same way the desktop client does. + +--- + +## Privacy and security + +- **Your identity is a certificate** that stays on your computer, stored with owner-only permissions. mumh5 reminds you to back it up. +- **Server certificates are pinned** the first time you connect. If one changes, mumh5 stops before sending your password and asks you. +- **Chats don't phone home.** Media is only loaded from your upload host and hosts you add yourself. YouTube is click-to-play in privacy mode. +- **Strict content security policy** in the app, and all message HTML is sanitized. + +--- + +## Roadmap + +- UDP voice with Mumble's OCB2-AES128 encryption, for the lowest latency +- System-wide push to talk +- Whisper and shout +- Rich chat between mumh5 users: replies, reactions, edits, typing indicators +- Video and screen sharing between mumh5 users +- A browser version, through a small self-hosted WebSocket proxy +- Release builds for all platforms + +--- + +## Development + +```bash +npm run dev # app with hot reload +npm run check # type check +npm test # unit tests +``` + +Tests against a real Mumble server, including an end-to-end test that drives the app: + +```bash +docker run -d --name mumh5-test-murmur -p 64739:64738 -p 64739:64738/udp \ + -e MUMBLE_CONFIG_AUTOBANATTEMPTS=0 -e MUMBLE_SUPERUSER_PASSWORD=testsuper mumblevoip/mumble-server +MUMBLE_TEST_HOST=localhost:64739 npm test +npm run build +MUMBLE_TEST_HOST=localhost:64739 MUMBLE_SUPERUSER_PASSWORD=testsuper npm run test:e2e +``` + +On Linux the end-to-end test forces X11, so it can run on a virtual display: `Xvfb :99 & DISPLAY=:99 npm run test:e2e`. See [CLAUDE.md](CLAUDE.md) for the architecture and project conventions. + +``` +electron/ main process: window, TLS sockets, identities, tray +src/core/ Mumble protocol: framing, codec, client state, voice packets +src/lib/ app state, voice engine, sanitizing, uploads +src/ui/ Svelte components +proto/ Mumble.proto and MumbleUDP.proto from upstream +test/ unit, server and end-to-end tests +``` + +--- + +## AI disclosure and credits + +mumh5 is built by **Kibi** together with **Claude**, Anthropic's AI model, working in Claude Code. + +Kibi sets the direction: what mumh5 should be, how it should look and feel, which trade-offs to make, and whether a result is good enough. Claude writes most of the code, the tests and this documentation, and checks each feature against a real Mumble server before calling it done. Commits written with Claude carry a `Co-Authored-By: Claude` line. + +We say this openly because you should know how the software you run was made. Features are covered by automated tests, including end-to-end tests against a real Mumble server. If something is wrong, please open an issue. + +## License + +[MIT](LICENSE). The Mumble protocol definitions in `proto/`, and the code generated from them, are licensed by The Mumble Developers under a BSD license; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). + +mumh5 is not affiliated with the Mumble project. Mumble is a trademark of its respective owners. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..7dbc45a --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,43 @@ +# Third-party notices + +## Mumble protocol definitions + +`proto/Mumble.proto` and `proto/MumbleUDP.proto` are copied from the Mumble project +(https://github.com/mumble-voip/mumble). `src/core/mumble-pb.js` and `src/core/mumble-udp-pb.js` +are generated from them. They are covered by the following license: + +``` +Copyright (C) The Mumble Developers + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of The Mumble Developers nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +## npm dependencies + +Runtime and build dependencies (Electron, Svelte, protobufjs, node-forge, DOMPurify and +others) are distributed under their own licenses, found in each package under `node_modules/`. diff --git a/docs/screenshots/main.png b/docs/screenshots/main.png new file mode 100644 index 0000000..ebe6049 Binary files /dev/null and b/docs/screenshots/main.png differ diff --git a/docs/screenshots/mobile.png b/docs/screenshots/mobile.png new file mode 100644 index 0000000..d9f612a Binary files /dev/null and b/docs/screenshots/mobile.png differ diff --git a/docs/screenshots/profile.png b/docs/screenshots/profile.png new file mode 100644 index 0000000..5789236 Binary files /dev/null and b/docs/screenshots/profile.png differ diff --git a/docs/screenshots/voice-settings.png b/docs/screenshots/voice-settings.png new file mode 100644 index 0000000..06e470e Binary files /dev/null and b/docs/screenshots/voice-settings.png differ diff --git a/electron/certs.ts b/electron/certs.ts new file mode 100644 index 0000000..fe1057f --- /dev/null +++ b/electron/certs.ts @@ -0,0 +1,39 @@ +import { X509Certificate } from 'node:crypto'; + +export interface CertDetails { + subject: string; + issuer: string; + validFrom: string; + validTo: string; + serialNumber: string; + fingerprint: string; // SHA-1, the hash Mumble uses for users + fingerprint256: string; + subjectAltName: string; + keyType: string; + keyBits: number | null; + ca: boolean; + selfSigned: boolean; + pem: string; +} + +// Plain, IPC-safe summary of a certificate for the viewer +export function describeCert(input: string | Buffer | X509Certificate): CertDetails { + const c = input instanceof X509Certificate ? input : new X509Certificate(input); + const key = c.publicKey; + const d = key.asymmetricKeyDetails; + return { + subject: c.subject, + issuer: c.issuer, + validFrom: new Date(c.validFrom).toISOString(), + validTo: new Date(c.validTo).toISOString(), + serialNumber: c.serialNumber, + fingerprint: c.fingerprint, + fingerprint256: c.fingerprint256, + subjectAltName: c.subjectAltName ?? '', + keyType: key.asymmetricKeyType ?? 'unknown', + keyBits: d?.modulusLength ?? null, + ca: c.ca, + selfSigned: c.checkIssued(c), + pem: c.toString() + }; +} diff --git a/electron/identity-store.ts b/electron/identity-store.ts new file mode 100644 index 0000000..1b26428 --- /dev/null +++ b/electron/identity-store.ts @@ -0,0 +1,161 @@ +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { generateIdentity, identityFromP12, identityToP12, certCommonName, type Identity } from './identity.ts'; +import { describeCert, type CertDetails } from './certs.ts'; + +export interface StoredIdentity extends Identity { + id: string; + name: string; + createdAt: string; +} + +interface StoreFile { + identities: StoredIdentity[]; + defaultId: string | null; + setupDone: boolean; +} + +export interface IdentitySummary { + id: string; + name: string; + fingerprint: string; + isDefault: boolean; + cert: CertDetails; +} + +// Where the desktop Mumble client (1.5+) keeps its settings, including its certificate +export function mumbleSettingsPaths(): string[] { + const home = os.homedir(); + return [ + path.join(process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming'), 'Mumble', 'Mumble', 'mumble_settings.json'), + path.join(home, 'Library', 'Application Support', 'Mumble', 'Mumble', 'mumble_settings.json'), + path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, '.config'), 'Mumble', 'Mumble', 'mumble_settings.json') + ]; +} + +// Client identities (certificates), stored with owner-only permissions in the app's data folder +export class IdentityStore { + private data: StoreFile | null = null; + private file: string; + private legacyFile: string; + + constructor(dir: string) { + this.file = path.join(dir, 'identities.json'); + this.legacyFile = path.join(dir, 'identity.json'); + } + + private async load(): Promise { + if (this.data) return this.data; + try { + this.data = JSON.parse(await fs.readFile(this.file, 'utf8')) as StoreFile; + } catch { + this.data = { identities: [], defaultId: null, setupDone: false }; + // Earlier versions generated one identity silently; keep it, but still run the setup wizard + try { + const legacy = JSON.parse(await fs.readFile(this.legacyFile, 'utf8')) as Identity; + const id = { ...legacy, id: randomUUID(), name: certCommonName(legacy.certPem) || 'My identity', createdAt: new Date().toISOString() }; + this.data.identities.push(id); + this.data.defaultId = id.id; + await this.save(); + } catch { /* no legacy identity */ } + } + return this.data; + } + + private async save(): Promise { + await fs.writeFile(this.file, JSON.stringify(this.data), { mode: 0o600 }); + } + + private summary(d: StoreFile, i: StoredIdentity): IdentitySummary { + return { id: i.id, name: i.name, fingerprint: i.fingerprint, isDefault: i.id === d.defaultId, cert: describeCert(i.certPem) }; + } + + async list(): Promise<{ identities: IdentitySummary[]; setupDone: boolean }> { + const d = await this.load(); + return { identities: d.identities.map(i => this.summary(d, i)), setupDone: d.setupDone }; + } + + // The identity to connect with: the requested one, else the default, else a new one + async get(id?: string | null): Promise { + const d = await this.load(); + const found = d.identities.find(i => i.id === id) ?? d.identities.find(i => i.id === d.defaultId) ?? d.identities[0]; + if (found) return found; + const created = await this.add(generateIdentity(os.userInfo().username), os.userInfo().username); + return d.identities.find(i => i.id === created.id)!; + } + + private async add(identity: Identity, name: string): Promise { + const d = await this.load(); + const existing = d.identities.find(i => i.fingerprint === identity.fingerprint); + if (existing) throw new Error(`This certificate is already stored as "${existing.name}".`); + const stored: StoredIdentity = { ...identity, id: randomUUID(), name: name.trim() || 'Identity', createdAt: new Date().toISOString() }; + d.identities.push(stored); + d.defaultId ??= stored.id; + await this.save(); + return this.summary(d, stored); + } + + create(name: string, email: string): Promise { + return this.add(generateIdentity(name, email), name); + } + + importP12(bytes: Uint8Array, password: string, name: string): Promise { + const identity = identityFromP12(bytes, password); + return this.add(identity, name || certCommonName(identity.certPem)); + } + + // Finds the desktop client's certificate; returns its name if one exists + async findMumbleCertificate(): Promise<{ path: string; name: string } | null> { + for (const p of mumbleSettingsPaths()) { + try { + const settings = JSON.parse(await fs.readFile(p, 'utf8')); + if (typeof settings.certificate !== 'string' || !settings.certificate) continue; + const identity = identityFromP12(new Uint8Array(Buffer.from(settings.certificate, 'base64')), ''); + return { path: p, name: certCommonName(identity.certPem) || 'Mumble certificate' }; + } catch { /* not there or unreadable */ } + } + return null; + } + + async importFromMumble(name: string): Promise { + const found = await this.findMumbleCertificate(); + if (!found) throw new Error('No certificate from the desktop Mumble client was found.'); + const settings = JSON.parse(await fs.readFile(found.path, 'utf8')); + return this.importP12(new Uint8Array(Buffer.from(settings.certificate, 'base64')), '', name || found.name); + } + + async exportP12(id: string, password: string): Promise<{ bytes: Uint8Array; name: string }> { + const d = await this.load(); + const i = d.identities.find(x => x.id === id); + if (!i) throw new Error('Identity not found.'); + return { bytes: identityToP12(i, password, i.name), name: i.name }; + } + + async setDefault(id: string): Promise { + const d = await this.load(); + if (d.identities.some(i => i.id === id)) d.defaultId = id; + await this.save(); + } + + async rename(id: string, name: string): Promise { + const d = await this.load(); + const i = d.identities.find(x => x.id === id); + if (i && name.trim()) i.name = name.trim(); + await this.save(); + } + + async remove(id: string): Promise { + const d = await this.load(); + d.identities = d.identities.filter(i => i.id !== id); + if (d.defaultId === id) d.defaultId = d.identities[0]?.id ?? null; + await this.save(); + } + + async finishSetup(): Promise { + const d = await this.load(); + d.setupDone = true; + await this.save(); + } +} diff --git a/electron/identity.ts b/electron/identity.ts new file mode 100644 index 0000000..a61a293 --- /dev/null +++ b/electron/identity.ts @@ -0,0 +1,73 @@ +import forge from 'node-forge'; +import { createHash } from 'node:crypto'; + +export interface Identity { + certPem: string; + keyPem: string; + fingerprint: string; +} + +// Self-signed client certificate, the same kind the desktop Mumble client generates. +// Mumble servers identify registered users by the hash of this certificate. +export function generateIdentity(name: string, email = ''): Identity { + const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 }); + const cert = forge.pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = '01' + forge.util.bytesToHex(forge.random.getBytesSync(8)); + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 20); + const attrs: { name: string; value: string }[] = [{ name: 'commonName', value: name || 'mumh5 user' }]; + if (email) attrs.push({ name: 'emailAddress', value: email }); + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.setExtensions([ + { name: 'basicConstraints', cA: false }, + { name: 'keyUsage', digitalSignature: true, keyEncipherment: true }, + { name: 'extKeyUsage', clientAuth: true } + ]); + cert.sign(keys.privateKey, forge.md.sha256.create()); + const certPem = forge.pki.certificateToPem(cert); + return { certPem, keyPem: forge.pki.privateKeyToPem(keys.privateKey), fingerprint: certFingerprint(certPem) }; +} + +// SHA-1 of the DER certificate, the hash Mumble shows and stores for users. +export function certFingerprint(certPem: string): string { + const der = forge.asn1.toDer(forge.pki.certificateToAsn1(forge.pki.certificateFromPem(certPem))).getBytes(); + return createHash('sha1').update(Buffer.from(der, 'binary')).digest('hex'); +} + +// Read a PKCS#12 file (.p12/.pfx, as exported by the desktop Mumble client). +export function identityFromP12(bytes: Uint8Array, password: string): Identity { + let p12: forge.pkcs12.Pkcs12Pfx; + try { + const der = forge.util.binary.raw.encode(bytes); + p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(der), password); + } catch { + throw new Error('Could not open the file. Wrong password, or not a PKCS#12 certificate.'); + } + const bags = (type: string) => p12.getBags({ bagType: type })[type] ?? []; + const cert = bags(forge.pki.oids.certBag).find(b => b.cert)?.cert; + const key = [...bags(forge.pki.oids.pkcs8ShroudedKeyBag), ...bags(forge.pki.oids.keyBag)].find(b => b.key)?.key; + if (!cert || !key) throw new Error('The file does not contain both a certificate and its private key.'); + const certPem = forge.pki.certificateToPem(cert); + return { certPem, keyPem: forge.pki.privateKeyToPem(key), fingerprint: certFingerprint(certPem) }; +} + +// PKCS#12 with 3DES, which the desktop Mumble client and other tools can import. +export function identityToP12(id: Identity, password: string, friendlyName: string): Uint8Array { + const asn1 = forge.pkcs12.toPkcs12Asn1( + forge.pki.privateKeyFromPem(id.keyPem), + [forge.pki.certificateFromPem(id.certPem)], + password, + { algorithm: '3des', friendlyName } + ); + return new Uint8Array(Buffer.from(forge.asn1.toDer(asn1).getBytes(), 'binary')); +} + +// Common name of a certificate, used as the default identity name +export function certCommonName(certPem: string): string { + const cn = forge.pki.certificateFromPem(certPem).subject.getField('CN'); + return cn ? String(cn.value) : ''; +} + diff --git a/electron/main.ts b/electron/main.ts new file mode 100644 index 0000000..bd83022 --- /dev/null +++ b/electron/main.ts @@ -0,0 +1,152 @@ +import { app, BrowserWindow, dialog, ipcMain, shell, session, systemPreferences, type WebContents } from 'electron'; +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { IdentityStore } from './identity-store.ts'; +import { describeCert } from './certs.ts'; +import * as tray from './tray.ts'; +import { openTls } from './tls-transport.ts'; + +const devUrl = process.env.VITE_DEV_SERVER_URL; + +let identityStore: IdentityStore | null = null; +const identities = () => (identityStore ??= new IdentityStore(app.getPath('userData'))); + +// ─── Mumble TLS connections, one per renderer request ───────────────────────── + +type Conn = ReturnType & { owner: WebContents }; +const conns = new Map(); + +// ─── Identities (client certificates) ───────────────────────────────────────── + +ipcMain.handle('identities:list', () => identities().list()); +ipcMain.handle('identities:create', (_e, name: string, email: string) => identities().create(String(name), String(email ?? ''))); +ipcMain.handle('identities:import', (_e, bytes: Uint8Array, password: string, name: string) => + identities().importP12(new Uint8Array(bytes), String(password ?? ''), String(name ?? ''))); +ipcMain.handle('identities:findMumble', () => identities().findMumbleCertificate().then(f => f && { name: f.name })); +ipcMain.handle('identities:importMumble', (_e, name: string) => identities().importFromMumble(String(name ?? ''))); +ipcMain.handle('identities:setDefault', (_e, id: string) => identities().setDefault(String(id))); +ipcMain.handle('identities:rename', (_e, id: string, name: string) => identities().rename(String(id), String(name))); +ipcMain.handle('identities:remove', (_e, id: string) => identities().remove(String(id))); +ipcMain.handle('identities:finishSetup', () => identities().finishSetup()); + +// Saves a password-protected .p12 backup where the user chooses; returns the path or null +ipcMain.handle('identities:export', async (e, id: string, password: string) => { + const { bytes, name } = await identities().exportP12(String(id), String(password ?? '')); + const win = BrowserWindow.fromWebContents(e.sender); + const opts = { + title: 'Save identity backup', + defaultPath: path.join(app.getPath('documents'), `${name.replace(/[^\w.-]+/g, '_') || 'identity'}.p12`), + filters: [{ name: 'PKCS#12 certificate', extensions: ['p12', 'pfx'] }] + }; + const res = win ? await dialog.showSaveDialog(win, opts) : await dialog.showSaveDialog(opts); + if (res.canceled || !res.filePath) return null; + await fs.writeFile(res.filePath, bytes, { mode: 0o600 }); + return res.filePath; +}); + +// The renderer picks the connection id and subscribes before calling open, so no event can be missed +ipcMain.handle('mumble:open', async (e, connId: string, host: string, port: number, identityId?: string) => { + if (typeof connId !== 'string' || conns.has(connId)) throw new Error('Invalid connection id'); + const id = await identities().get(identityId); + const owner = e.sender; + const emit = (channel: string, ...args: unknown[]) => { if (!owner.isDestroyed()) owner.send(channel, connId, ...args); }; + const conn = openTls(String(host), Number(port) || 64738, id.certPem, id.keyPem, { + onSecure: info => emit('mumble:secure', info), + onData: chunk => emit('mumble:data', chunk), + onClose: reason => { conns.delete(connId); emit('mumble:close', reason); } + }); + conns.set(connId, Object.assign(conn, { owner })); + owner.once('destroyed', () => conn.close()); +}); + +ipcMain.on('mumble:send', (e, connId: string, bytes: Uint8Array) => { + const conn = conns.get(connId); + if (conn && conn.owner === e.sender) conn.send(bytes); +}); + +ipcMain.on('mumble:close', (e, connId: string) => { + const conn = conns.get(connId); + if (conn && conn.owner === e.sender) conn.close(); +}); + +// Parses DER certificates (e.g. a user's chain from UserStats) for the viewer +ipcMain.handle('certs:describe', (_e, ders: Uint8Array[]) => + (Array.isArray(ders) ? ders.slice(0, 8) : []).flatMap(d => { try { return [describeCert(Buffer.from(d))]; } catch { return []; } })); + +ipcMain.on('tray:update', (_e, state: tray.TrayState) => tray.update(state)); + +ipcMain.handle('platform:info', () => ({ os: process.platform, osVersion: os.release() })); + +// ─── Window ──────────────────────────────────────────────────────────────────── + +let mainWindow: BrowserWindow | null = null; + +function createWindow() { + const win = new BrowserWindow({ + width: 1280, + height: 800, + minWidth: 360, + minHeight: 480, + backgroundColor: '#111214', + title: 'mumh5', + autoHideMenuBar: true, + webPreferences: { + preload: path.join(__dirname, 'preload.cjs'), + contextIsolation: true, + sandbox: true, + nodeIntegration: false, + // Voice must play without a click first + autoplayPolicy: 'no-user-gesture-required', + // Keep audio and timers running at full speed when the window is in the background + backgroundThrottling: false + } + }); + + // Links from chat open in the system browser, never inside the app + win.webContents.setWindowOpenHandler(({ url }) => { + if (/^https?:\/\//i.test(url)) shell.openExternal(url); + return { action: 'deny' }; + }); + win.webContents.on('will-navigate', (e, url) => { + if (devUrl && url.startsWith(devUrl)) return; + e.preventDefault(); + if (/^https?:\/\//i.test(url)) shell.openExternal(url); + }); + + mainWindow = win; + win.on('close', e => { + if (tray.shouldHideOnClose()) { + e.preventDefault(); + win.hide(); + } + }); + win.on('show', () => tray.onWindowVisibility()); + win.on('hide', () => tray.onWindowVisibility()); + win.on('closed', () => { if (mainWindow === win) mainWindow = null; }); + + if (devUrl) win.loadURL(devUrl); + else win.loadFile(path.join(__dirname, '../dist/index.html')); +} + +app.whenReady().then(async () => { + // macOS asks once per app for microphone access + if (process.platform === 'darwin') await systemPreferences.askForMediaAccess('microphone').catch(() => false); + // YouTube refuses embeds without a referrer (error 153); a file:// app sends none + session.defaultSession.webRequest.onBeforeSendHeaders({ urls: ['https://www.youtube-nocookie.com/*'] }, (details, cb) => { + if (!details.requestHeaders.Referer) details.requestHeaders.Referer = 'https://mumh5.app/'; + cb({ requestHeaders: details.requestHeaders }); + }); + + // Microphone, camera and screen capture for voice and video; nothing else + session.defaultSession.setPermissionRequestHandler((_wc, permission, cb) => { + cb(['media', 'display-capture', 'clipboard-sanitized-write', 'notifications'].includes(permission)); + }); + createWindow(); + await tray.setupTray(() => mainWindow, action => mainWindow?.webContents.send('tray:action', action)); + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/electron/preload.ts b/electron/preload.ts new file mode 100644 index 0000000..ad21dc4 --- /dev/null +++ b/electron/preload.ts @@ -0,0 +1,36 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +type Listener = (connId: string, ...args: any[]) => void; +const listeners = { secure: new Set(), data: new Set(), close: new Set() }; +for (const key of Object.keys(listeners) as (keyof typeof listeners)[]) { + ipcRenderer.on(`mumble:${key}`, (_e, connId: string, ...args: any[]) => { + for (const fn of listeners[key]) fn(connId, ...args); + }); +} + +// The only native surface the renderer gets +contextBridge.exposeInMainWorld('mumh5Native', { + platformInfo: () => ipcRenderer.invoke('platform:info'), + updateTray: (state: unknown) => ipcRenderer.send('tray:update', state), + onTrayAction: (fn: (action: string) => void) => { ipcRenderer.on('tray:action', (_e, a: string) => fn(a)); }, + describeCerts: (ders: Uint8Array[]) => ipcRenderer.invoke('certs:describe', ders), + identities: { + list: () => ipcRenderer.invoke('identities:list'), + create: (name: string, email: string) => ipcRenderer.invoke('identities:create', name, email), + importP12: (bytes: Uint8Array, password: string, name: string) => ipcRenderer.invoke('identities:import', bytes, password, name), + findMumble: () => ipcRenderer.invoke('identities:findMumble'), + importMumble: (name: string) => ipcRenderer.invoke('identities:importMumble', name), + exportP12: (id: string, password: string) => ipcRenderer.invoke('identities:export', id, password), + setDefault: (id: string) => ipcRenderer.invoke('identities:setDefault', id), + rename: (id: string, name: string) => ipcRenderer.invoke('identities:rename', id, name), + remove: (id: string) => ipcRenderer.invoke('identities:remove', id), + finishSetup: () => ipcRenderer.invoke('identities:finishSetup') + }, + open: (connId: string, host: string, port: number, identityId?: string) => ipcRenderer.invoke('mumble:open', connId, host, port, identityId), + send: (connId: string, bytes: Uint8Array) => ipcRenderer.send('mumble:send', connId, bytes), + close: (connId: string) => ipcRenderer.send('mumble:close', connId), + on: (event: keyof typeof listeners, fn: Listener) => { + listeners[event].add(fn); + return () => listeners[event].delete(fn); + } +}); diff --git a/electron/tls-transport.ts b/electron/tls-transport.ts new file mode 100644 index 0000000..e9eaf9e --- /dev/null +++ b/electron/tls-transport.ts @@ -0,0 +1,50 @@ +import tls from 'node:tls'; +import { describeCert, type CertDetails } from './certs.ts'; + +export interface TlsHandlers { + onSecure(info: { fingerprint: string; authorized: boolean; authError: string | null; chain: CertDetails[] }): void; + onData(chunk: Uint8Array): void; + onClose(reason: string): void; +} + +// Opens a TLS connection to a Mumble server. Most servers use self-signed certificates, +// so verification is left to the caller (trust on first use via the sha256 fingerprint). +export function openTls(host: string, port: number, cert: string, key: string, h: TlsHandlers) { + let closeReason = 'Connection closed'; + const socket = tls.connect({ + host, port, cert, key, + rejectUnauthorized: false, + servername: /^[\d.:]+$/.test(host) ? undefined : host + }); + socket.setNoDelay(true); + socket.setKeepAlive(true, 30000); + socket.setTimeout(20000, () => { + closeReason = 'Connection timed out'; + socket.destroy(); + }); + socket.on('secureConnect', () => { + socket.setTimeout(0); + // Leaf first, then the issuers the server sent (the root links to itself) + const chain: CertDetails[] = []; + let cur = socket.getPeerCertificate(true) as tls.DetailedPeerCertificate | undefined; + const seen = new Set(); + while (cur?.raw && !seen.has(cur.fingerprint256) && chain.length < 8) { + seen.add(cur.fingerprint256); + chain.push(describeCert(cur.raw)); + cur = cur.issuerCertificate; + } + h.onSecure({ + chain, + fingerprint: chain[0]?.fingerprint256 ?? '', + authorized: socket.authorized, + authError: socket.authorizationError ? String(socket.authorizationError) : null + }); + }); + socket.on('data', (chunk: Buffer) => h.onData(new Uint8Array(chunk))); + socket.on('error', (e: Error) => { closeReason = e.message; }); + socket.on('close', () => h.onClose(closeReason)); + return { + send(bytes: Uint8Array) { if (!socket.destroyed) socket.write(bytes); }, + close() { closeReason = 'Disconnected'; socket.destroy(); } + }; +} diff --git a/electron/tray.ts b/electron/tray.ts new file mode 100644 index 0000000..b4c171a --- /dev/null +++ b/electron/tray.ts @@ -0,0 +1,97 @@ +import { app, BrowserWindow, Menu, Tray, nativeImage } from 'electron'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +// State the renderer reports; the tray mirrors it +export interface TrayState { + icon: string; // PNG data URL drawn by the renderer + tooltip: string; + connected: boolean; + server: string; + muted: boolean; + deafened: boolean; + mode: 'vad' | 'ptt' | 'continuous'; +} + +let tray: Tray | null = null; +let state: TrayState | null = null; +let closeToTray = true; +let quitting = false; +const prefsFile = () => path.join(app.getPath('userData'), 'tray.json'); + +export async function setupTray(getWindow: () => BrowserWindow | null, send: (action: string) => void): Promise { + try { closeToTray = JSON.parse(await fs.readFile(prefsFile(), 'utf8')).closeToTray !== false; } catch { /* defaults */ } + + const show = () => { + const win = getWindow(); + if (!win) return; + if (win.isMinimized()) win.restore(); + win.show(); + win.focus(); + }; + + const rebuild = () => { + if (!tray || !state) return; + const win = getWindow(); + const s = state; + const menu = Menu.buildFromTemplate([ + { label: win?.isVisible() ? 'Hide mumh5' : 'Show mumh5', click: () => (win?.isVisible() ? win.hide() : show()) }, + { type: 'separator' }, + { label: s.connected ? `Connected: ${s.server}` : 'Not connected', enabled: false }, + { label: 'Mute', type: 'checkbox', checked: s.muted, enabled: s.connected, click: () => send('toggle-mute') }, + { label: 'Deafen', type: 'checkbox', checked: s.deafened, enabled: s.connected, click: () => send('toggle-deafen') }, + { + label: 'Transmit', submenu: [ + { label: 'Voice activity', type: 'radio', checked: s.mode === 'vad', click: () => send('mode:vad') }, + { label: 'Push to talk', type: 'radio', checked: s.mode === 'ptt', click: () => send('mode:ptt') }, + { label: 'Always on', type: 'radio', checked: s.mode === 'continuous', click: () => send('mode:continuous') } + ] + }, + { label: 'Voice settings...', click: () => { show(); send('settings'); } }, + { label: 'Disconnect', enabled: s.connected, click: () => send('disconnect') }, + { type: 'separator' }, + { + label: 'Close to tray', type: 'checkbox', checked: closeToTray, click: item => { + closeToTray = item.checked; + fs.writeFile(prefsFile(), JSON.stringify({ closeToTray })).catch(() => {}); + } + }, + { label: 'Quit mumh5', click: () => { quitting = true; app.quit(); } } + ]); + tray.setContextMenu(menu); + // Inspection hook for the end-to-end tests (tray menus cannot be clicked by automation) + (globalThis as any).__mumh5Tray = { + state: { ...s, icon: s.icon.slice(0, 22) }, + tooltip: s.tooltip, + items: menu.items.map(i => ({ label: i.label, checked: i.checked, enabled: i.enabled })), + click: (label: string) => { const item = menu.items.find(i => i.label === label); item?.click(undefined, getWindow() ?? undefined, getWindow()?.webContents); } + }; + }; + + app.on('before-quit', () => { quitting = true; }); + + // Called for every state change from the renderer + update = (next: TrayState) => { + state = next; + const image = nativeImage.createFromDataURL(next.icon); + if (!tray) { + tray = new Tray(image); + tray.on('click', show); + } else { + tray.setImage(image); + } + tray.setToolTip(next.tooltip); + rebuild(); + }; + + // Keep the Show/Hide label right + onWindowVisibility = rebuild; +} + +export let update: (s: TrayState) => void = () => {}; +export let onWindowVisibility: () => void = () => {}; + +// Closing the window hides it while the tray is there, unless the user quits +export function shouldHideOnClose(): boolean { + return !!tray && closeToTray && !quitting && process.platform !== 'darwin'; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..57d7b98 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + mumh5 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4bcc2fe --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5823 @@ +{ + "name": "mumh5", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mumh5", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "dompurify": "^3.4.16", + "node-forge": "^1.4.0", + "protobufjs": "^8.8.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@tsconfig/svelte": "^5.0.8", + "@types/node": "^26.6.3", + "@types/node-forge": "^1.3.14", + "electron": "^44.5.1", + "electron-builder": "^26.15.3", + "esbuild": "^0.28.2", + "playwright-core": "^1.63.0", + "protobufjs-cli": "^2.7.0", + "svelte": "^5.57.1", + "svelte-check": "^4.7.6", + "typescript": "^6.0.3", + "vite": "^8.3.1" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", + "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", + "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.12.tgz", + "integrity": "sha512-TuB0x50EoAvEX/UEWITd8Mkn3WhiTjSvbTMCLj0BhsQEl5iUzjXdA0bETEVpTk+5TGTLR6QktI9H4hLviVeaAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.18.1" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.10.0.tgz", + "integrity": "sha512-GhokD41lV4gQrrLYm3wCkHfBOnJrnhDMgt4XeMW8gzfE1UdJqIuSwsE+ggf82XBjUXRJylcm+KIGQFa4utIVLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.3.tgz", + "integrity": "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.1.tgz", + "integrity": "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^1.0.0", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tsconfig/svelte": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", + "integrity": "sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", + "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dompurify": { + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "44.5.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-44.5.1.tgz", + "integrity": "sha512-lx7AYoFIiyvEvgvpyWO9kOC60xtNPpiPZAth6Fmg9NutzpCAMLwpUlaBY2EXfZyIgUdzEmLMj9KvK0Vrokl1yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", + "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrap": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.4.0.tgz", + "integrity": "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/markdown-it": { + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-abi": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.35.0.tgz", + "integrity": "sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.1.tgz", + "integrity": "sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "website" + ], + "dependencies": { + "@noble/hashes": "1.8.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/protobufjs": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", + "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs-cli": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-2.7.0.tgz", + "integrity": "sha512-wjrZPibf/U73dDJMh9jCqjyeDH7t6+OMD16vKp/Z1CY7yyVE/pX5a9EvCq89p3ADHo+KyaLknA5lxyaaM252KA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "escodegen": "^2.0.0", + "espree": "^9.6.1", + "estraverse": "^5.1.0", + "glob": "^8.1.0", + "jsdoc": "^4.0.5", + "minimist": "^1.2.8", + "tmp": "^0.2.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts", + "protoc-gen-pbjs": "bin/protoc-gen-pbjs" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^8.8.0" + } + }, + "node_modules/protobufjs-cli/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/protobufjs-cli/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/protobufjs-cli/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "5.57.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.1.tgz", + "integrity": "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.6.0", + "@sveltejs/acorn-typescript": "^1.0.13", + "@types/estree": "^1.0.9", + "acorn": "^8.18.0", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.9.2", + "esm-env": "^1.2.1", + "esrap": "^2.3.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.6.tgz", + "integrity": "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.3", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz", + "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..51e3272 --- /dev/null +++ b/package.json @@ -0,0 +1,89 @@ +{ + "name": "mumh5", + "productName": "mumh5", + "version": "0.1.0", + "description": "Modern Mumble client with rich chat, file sharing and video", + "license": "MIT", + "type": "module", + "main": "dist-electron/main.cjs", + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "vite build && node scripts/build-electron.mjs", + "build:web": "vite build --mode web --outDir dist-web", + "check": "svelte-check --tsconfig ./tsconfig.json", + "test": "node --test test/*.test.ts", + "dist": "npm run build && electron-builder", + "dist:linux": "npm run build && electron-builder --linux", + "dist:win": "npm run build && electron-builder --win", + "dist:mac": "npm run build && electron-builder --mac", + "test:e2e": "node test/e2e/app.e2e.ts", + "proto": "pbjs -t static-module -w es6 --keep-case --no-delimited --no-service --no-comments --force-number proto/Mumble.proto -o src/core/mumble-pb.js && pbjs -t static-module -w es6 --keep-case --no-delimited --no-service --no-comments --force-number proto/MumbleUDP.proto -o src/core/mumble-udp-pb.js", + "screenshots": "node test/e2e/screenshots.ts" + }, + "build": { + "appId": "dev.f0ck.mumh5", + "productName": "mumh5", + "files": [ + "dist/**", + "dist-electron/**", + "package.json", + "LICENSE", + "THIRD_PARTY_NOTICES.md" + ], + "directories": { + "output": "release" + }, + "linux": { + "target": [ + "AppImage", + "deb" + ], + "category": "Network" + }, + "win": { + "target": [ + "nsis" + ] + }, + "mac": { + "target": [ + "dmg" + ], + "category": "public.app-category.social-networking", + "extendInfo": { + "NSMicrophoneUsageDescription": "mumh5 uses the microphone for voice chat.", + "NSCameraUsageDescription": "mumh5 uses the camera for video chat." + } + } + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@tsconfig/svelte": "^5.0.8", + "@types/node": "^26.6.3", + "@types/node-forge": "^1.3.14", + "electron": "^44.5.1", + "electron-builder": "^26.15.3", + "esbuild": "^0.28.2", + "playwright-core": "^1.63.0", + "protobufjs-cli": "^2.7.0", + "svelte": "^5.57.1", + "svelte-check": "^4.7.6", + "typescript": "^6.0.3", + "vite": "^8.3.1" + }, + "dependencies": { + "dompurify": "^3.4.16", + "node-forge": "^1.4.0", + "protobufjs": "^8.8.0" + }, + "allowScripts": { + "electron@44.5.1": true, + "esbuild@0.28.2": true, + "electron-winstaller": false + }, + "author": "Kibi Kelburton", + "repository": { + "type": "git", + "url": "gitea@git.lat:kibi/mumh5.git" + } +} diff --git a/proto/Mumble.proto b/proto/Mumble.proto new file mode 100644 index 0000000..deb6c17 --- /dev/null +++ b/proto/Mumble.proto @@ -0,0 +1,639 @@ +// Copyright The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +syntax = "proto2"; + +package MumbleProto; + +option optimize_for = SPEED; + +message Version { + // Legacy version number format. + optional uint32 version_v1 = 1; + // New version number format. + // Necessary since patch level may exceed 255. (See https://github.com/mumble-voip/mumble/issues/5827) + optional uint64 version_v2 = 5; + + // Client release name. + optional string release = 2; + // Client OS name. + optional string os = 3; + // Client OS version. + optional string os_version = 4; +} + +// Not used. Not even for tunneling UDP through TCP. +message UDPTunnel { + // Not used. + required bytes packet = 1; +} + +// Used by the client to send the authentication credentials to the server. +message Authenticate { + // UTF-8 encoded username. + optional string username = 1; + // Server or user password. + optional string password = 2; + // Additional access tokens for server ACL groups. + repeated string tokens = 3; + // A list of CELT bitstream version constants supported by the client. + repeated int32 celt_versions = 4; + optional bool opus = 5 [default = false]; + // 0 = REGULAR, 1 = BOT + optional int32 client_type = 6 [default = 0]; +} + +// Sent by the client to notify the server that the client is still alive. +// Server must reply to the packet with the same timestamp and its own +// good/late/lost/resync numbers. None of the fields is strictly required. +message Ping { + // Client timestamp. Server should not attempt to decode. + optional uint64 timestamp = 1; + // The amount of good packets received. + optional uint32 good = 2; + // The amount of late packets received. + optional uint32 late = 3; + // The amount of packets never received. + optional uint32 lost = 4; + // The amount of nonce resyncs. + optional uint32 resync = 5; + // The total amount of UDP packets received. + optional uint32 udp_packets = 6; + // The total amount of TCP packets received. + optional uint32 tcp_packets = 7; + // UDP ping average. + optional float udp_ping_avg = 8; + // UDP ping variance. + optional float udp_ping_var = 9; + // TCP ping average. + optional float tcp_ping_avg = 10; + // TCP ping variance. + optional float tcp_ping_var = 11; +} + +// Sent by the server when it rejects the user connection. +message Reject { + enum RejectType { + // The rejection reason is unknown (details should be available + // in Reject.reason). + None = 0; + // The client attempted to connect with an incompatible version. + WrongVersion = 1; + // The user name supplied by the client was invalid. + InvalidUsername = 2; + // The client attempted to authenticate as a user with a password but it + // was wrong. + WrongUserPW = 3; + // The client attempted to connect to a passworded server but the password + // was wrong. + WrongServerPW = 4; + // Supplied username is already in use. + UsernameInUse = 5; + // Server is currently full and cannot accept more users. + ServerFull = 6; + // The user did not provide a certificate but one is required. + NoCertificate = 7; + AuthenticatorFail = 8; + // The server is currently not accepting new connections + NoNewConnections = 9; + } + // Rejection type. + optional RejectType type = 1; + // Human readable rejection reason. + optional string reason = 2; +} + +// ServerSync message is sent by the server when it has authenticated the user +// and finished synchronizing the server state. +message ServerSync { + // The session of the current user. + optional uint32 session = 1; + // Maximum bandwidth that the user should use. + optional uint32 max_bandwidth = 2; + // Server welcome text. + optional string welcome_text = 3; + // Current user permissions in the root channel. + // Note: The permissions data type usually is uin32 (e.g. in PermissionQuery and PermissionDenied messages). Here + // it is uint64 because of an oversight in the past. Nonetheless it should never exceed the uin32 range. + // See also: https://github.com/mumble-voip/mumble/issues/5139 + optional uint64 permissions = 4; +} + +// Sent by the client when it wants a channel removed. Sent by the server when +// a channel has been removed and clients should be notified. +message ChannelRemove { + required uint32 channel_id = 1; +} + +// Used to communicate channel properties between the client and the server. +// Sent by the server during the login process or when channel properties are +// updated. Client may use this message to update said channel properties. +message ChannelState { + // Unique ID for the channel within the server. + optional uint32 channel_id = 1; + // channel_id of the parent channel. + optional uint32 parent = 2; + // UTF-8 encoded channel name. + optional string name = 3; + // A collection of channel id values of the linked channels. Absent during + // the first channel listing. + repeated uint32 links = 4; + // UTF-8 encoded channel description. Only if the description is less than + // 128 bytes + optional string description = 5; + // A collection of channel_id values that should be added to links. + repeated uint32 links_add = 6; + // A collection of channel_id values that should be removed from links. + repeated uint32 links_remove = 7; + // True if the channel is temporary. + optional bool temporary = 8 [default = false]; + // Position weight to tweak the channel position in the channel list. + optional int32 position = 9 [default = 0]; + // SHA1 hash of the description if the description is 128 bytes or more. + optional bytes description_hash = 10; + // Maximum number of users allowed in the channel. If this value is zero, + // the maximum number of users allowed in the channel is given by the + // server's "usersperchannel" setting. + optional uint32 max_users = 11; + // Whether this channel has enter restrictions (ACL denying ENTER) set + optional bool is_enter_restricted = 12; + // Whether the receiver of this msg is considered to be able to enter this channel + optional bool can_enter = 13; +} + +// Used to communicate user leaving or being kicked. May be sent by the client +// when it attempts to kick a user. Sent by the server when it informs the +// clients that a user is not present anymore. +message UserRemove { + // The user who is being kicked, identified by their session, not present + // when no one is being kicked. + required uint32 session = 1; + // The user who initiated the removal. Either the user who performs the kick + // or the user who is currently leaving. + optional uint32 actor = 2; + // Reason for the kick, stored as the ban reason if the user is banned. + optional string reason = 3; + // True if the user shall be banned, false if the user shall be kicked + optional bool ban = 4; + // True if the user shall be banned by certificate. + optional bool ban_certificate = 5; + // True if the user shall be banned by IP. + optional bool ban_ip = 6; +} + +// Sent by the server when it communicates new and changed users to client. +// First seen during login procedure. May be sent by the client when it wishes +// to alter its state. +message UserState { + message VolumeAdjustment { + optional uint32 listening_channel = 1; + optional float volume_adjustment = 2; + } + + // Unique user session ID of the user whose state this is, may change on + // reconnect. + optional uint32 session = 1; + // The session of the user who is updating this user. + optional uint32 actor = 2; + // User name, UTF-8 encoded. + optional string name = 3; + // Registered user ID if the user is registered. + optional uint32 user_id = 4; + // Channel on which the user is. + optional uint32 channel_id = 5; + // True if the user is muted by admin. + optional bool mute = 6; + // True if the user is deafened by admin. + optional bool deaf = 7; + // True if the user has been suppressed from talking by a reason other than + // being muted. + optional bool suppress = 8; + // True if the user has muted self. + optional bool self_mute = 9; + // True if the user has deafened self. + optional bool self_deaf = 10; + // User image if it is less than 128 bytes. + optional bytes texture = 11; + // The positional audio plugin identifier. + // Positional audio information is only sent to users who share + // identical plugin contexts. + // + // This value is not transmitted to clients. + optional bytes plugin_context = 12; + // The user's plugin-specific identity. + // This value is not transmitted to clients. + optional string plugin_identity = 13; + // User comment if it is less than 128 bytes. + optional string comment = 14; + // The hash of the user certificate. + optional string hash = 15; + // SHA1 hash of the user comment if it 128 bytes or more. + optional bytes comment_hash = 16; + // SHA1 hash of the user picture if it 128 bytes or more. + optional bytes texture_hash = 17; + // True if the user is a priority speaker. + optional bool priority_speaker = 18; + // True if the user is currently recording. + optional bool recording = 19; + // A list of temporary access tokens to be respected when processing this request. + repeated string temporary_access_tokens = 20; + // A list of channels the user wants to start listening to. + repeated uint32 listening_channel_add = 21; + // a list of channels the user does no longer want to listen to. + repeated uint32 listening_channel_remove = 22; + // A list of volume adjustments the user has applied to listeners + repeated VolumeAdjustment listening_volume_adjustment = 23; +} + +// Relays information on the bans. The client may send the BanList message to +// either modify the list of bans or query them from the server. The server +// sends this list only after a client queries for it. +message BanList { + message BanEntry { + // Banned IP address. + required bytes address = 1; + // The length of the subnet mask for the ban. + required uint32 mask = 2; + // User name for identification purposes (does not affect the ban). + optional string name = 3; + // The certificate hash of the banned user. + optional string hash = 4; + // Reason for the ban (does not affect the ban). + optional string reason = 5; + // Ban start time. + optional string start = 6; + // Ban duration in seconds. + optional uint32 duration = 7; + } + // List of ban entries currently in place. + repeated BanEntry bans = 1; + // True if the server should return the list, false if it should replace old + // ban list with the one provided. + optional bool query = 2 [default = false]; +} + +// Used to send and broadcast text messages. +message TextMessage { + // The message sender, identified by its session. + optional uint32 actor = 1; + // Target users for the message, identified by their session. + repeated uint32 session = 2; + // The channels to which the message is sent, identified by their + // channel_ids. + repeated uint32 channel_id = 3; + // The root channels when sending message recursively to several channels, + // identified by their channel_ids. + repeated uint32 tree_id = 4; + // The UTF-8 encoded message. May be HTML if the server allows. + required string message = 5; +} + +message PermissionDenied { + enum DenyType { + // Operation denied for other reason, see reason field. + Text = 0; + // Permissions were denied. + Permission = 1; + // Cannot modify SuperUser. + SuperUser = 2; + // Invalid channel name. + ChannelName = 3; + // Text message too long. + TextTooLong = 4; + // The flux capacitor was spelled wrong. + H9K = 5; + // Operation not permitted in temporary channel. + TemporaryChannel = 6; + // Operation requires certificate. + MissingCertificate = 7; + // Invalid username. + UserName = 8; + // Channel is full. + ChannelFull = 9; + // Channels are nested too deeply. + NestingLimit = 10; + // Maximum channel count reached. + ChannelCountLimit = 11; + // Amount of listener objects for this channel has been reached + ChannelListenerLimit = 12; + // Amount of listener proxies for the user has been reached + UserListenerLimit = 13; + } + // The denied permission when type is Permission. + optional uint32 permission = 1; + // channel_id for the channel where the permission was denied when type is + // Permission. + optional uint32 channel_id = 2; + // The user who was denied permissions, identified by session. + optional uint32 session = 3; + // Textual reason for the denial. + optional string reason = 4; + // Type of the denial. + optional DenyType type = 5; + // The name that is invalid when type is UserName. + optional string name = 6; +} + +message ACL { + message ChanGroup { + // Name of the channel group, UTF-8 encoded. + required string name = 1; + // True if the group has been inherited from the parent (Read only). + optional bool inherited = 2 [default = true]; + // True if the group members are inherited. + optional bool inherit = 3 [default = true]; + // True if the group can be inherited by sub channels. + optional bool inheritable = 4 [default = true]; + // Users explicitly included in this group, identified by user_id. + repeated uint32 add = 5; + // Users explicitly removed from this group in this channel if the group + // has been inherited, identified by user_id. + repeated uint32 remove = 6; + // Users inherited, identified by user_id. + repeated uint32 inherited_members = 7; + } + message ChanACL { + // True if this ACL applies to the current channel. + optional bool apply_here = 1 [default = true]; + // True if this ACL applies to the sub channels. + optional bool apply_subs = 2 [default = true]; + // True if the ACL has been inherited from the parent. + optional bool inherited = 3 [default = true]; + // ID of the user that is affected by this ACL. + optional uint32 user_id = 4; + // ID of the group that is affected by this ACL. + optional string group = 5; + // Bit flag field of the permissions granted by this ACL. + optional uint32 grant = 6; + // Bit flag field of the permissions denied by this ACL. + optional uint32 deny = 7; + } + // Channel ID of the channel this message affects. + required uint32 channel_id = 1; + // True if the channel inherits its parent's ACLs. + optional bool inherit_acls = 2 [default = true]; + // User group specifications. + repeated ChanGroup groups = 3; + // ACL specifications. + repeated ChanACL acls = 4; + // True if the message is a query for ACLs instead of setting them. + optional bool query = 5 [default = false]; +} + +// Client may use this message to refresh its registered user information. The +// client should fill the IDs or Names of the users it wants to refresh. The +// server fills the missing parts and sends the message back. +message QueryUsers { + // user_ids. + repeated uint32 ids = 1; + // User names in the same order as ids. + repeated string names = 2; +} + +// Used to initialize and resync the UDP encryption. Either side may request a +// resync by sending the message without any values filled. The resync is +// performed by sending the message with only the client or server nonce +// filled. +message CryptSetup { + // Encryption key. + optional bytes key = 1; + // Client nonce. + optional bytes client_nonce = 2; + // Server nonce. + optional bytes server_nonce = 3; +} + +// Used to add or remove custom context menu item on client-side. +message ContextActionModify { + enum Context { + // Action is applicable to the server. + Server = 0x01; + // Action can target a Channel. + Channel = 0x02; + // Action can target a User. + User = 0x04; + } + enum Operation { + Add = 0; + Remove = 1; + } + // The action identifier. Used later to initiate an action. + required string action = 1; + // The display name of the action. + optional string text = 2; + // Context bit flags defining where the action should be displayed. + // Flags can be OR-ed to combine different types. + optional uint32 context = 3; + // Choose either to add or to remove the context action. + // Note: This field only exists after Mumble 1.2.4-beta1 release. + // The message will be recognized as Add regardless of this field + // before said release. + optional Operation operation = 4; +} + +// Sent by the client when it wants to initiate a Context action. +message ContextAction { + // The target User for the action, identified by session. + optional uint32 session = 1; + // The target Channel for the action, identified by channel_id. + optional uint32 channel_id = 2; + // The action that should be executed. + required string action = 3; +} + +// Lists the registered users. +message UserList { + message User { + // Registered user ID. + required uint32 user_id = 1; + // Registered user name. + optional string name = 2; + optional string last_seen = 3; + optional uint32 last_channel = 4; + } + // A list of registered users. + repeated User users = 1; +} + +// Sent by the client when it wants to register or clear whisper targets. +// +// Note: The first available target ID is 1 as 0 is reserved for normal +// talking. Maximum target ID is 30. +message VoiceTarget { + message Target { + // Users that are included as targets. + repeated uint32 session = 1; + // Channel that is included as a target. + optional uint32 channel_id = 2; + // ACL group that is included as a target. + optional string group = 3; + // True if the voice should follow links from the specified channel. + optional bool links = 4 [default = false]; + // True if the voice should also be sent to children of the specific + // channel. + optional bool children = 5 [default = false]; + } + // Voice target ID. + optional uint32 id = 1; + // The receivers that this voice target includes. + repeated Target targets = 2; +} + +// Sent by the client when it wants permissions for a certain channel. Sent by +// the server when it replies to the query or wants the user to resync all +// channel permissions. +message PermissionQuery { + // channel_id of the channel for which the permissions are queried. + optional uint32 channel_id = 1; + // Channel permissions. + optional uint32 permissions = 2; + // True if the client should drop its current permission information for all + // channels. + optional bool flush = 3 [default = false]; +} + +// Sent by the server to notify the users of the version of the CELT codec they +// should use. This may change during the connection when new users join. +message CodecVersion { + // The version of the CELT Alpha codec. + required int32 alpha = 1; + // The version of the CELT Beta codec. + required int32 beta = 2; + // True if the user should prefer Alpha over Beta. + required bool prefer_alpha = 3 [default = true]; + optional bool opus = 4 [default = false]; +} + +// Used to communicate user stats between the server and clients. +message UserStats { + message Stats { + // The amount of good packets received. + optional uint32 good = 1; + // The amount of late packets received. + optional uint32 late = 2; + // The amount of packets never received. + optional uint32 lost = 3; + // The amount of nonce resyncs. + optional uint32 resync = 4; + } + + message RollingStats { + // Rolling packet statistics time window as defined on the server. + optional uint32 time_window = 1; + // Rolling packet statistics for packets received from the client. + optional Stats from_client = 2; + // Rolling packet statistics for packets sent by the server. + optional Stats from_server = 3; + } + + // User whose stats these are. + optional uint32 session = 1; + // True if the message contains only mutable stats (packets, ping). + optional bool stats_only = 2 [default = false]; + // Full user certificate chain of the user certificate in DER format. + repeated bytes certificates = 3; + // Packet statistics for packets received from the client. + optional Stats from_client = 4; + // Packet statistics for packets sent by the server. + optional Stats from_server = 5; + + // Amount of UDP packets sent. + optional uint32 udp_packets = 6; + // Amount of TCP packets sent. + optional uint32 tcp_packets = 7; + // UDP ping average. + optional float udp_ping_avg = 8; + // UDP ping variance. + optional float udp_ping_var = 9; + // TCP ping average. + optional float tcp_ping_avg = 10; + // TCP ping variance. + optional float tcp_ping_var = 11; + + // Client version. + optional Version version = 12; + // A list of CELT bitstream version constants supported by the client of this + // user. + repeated int32 celt_versions = 13; + // Client IP address. + optional bytes address = 14; + // Bandwidth used by this client. + optional uint32 bandwidth = 15; + // Connection duration. + optional uint32 onlinesecs = 16; + // Duration since last activity. + optional uint32 idlesecs = 17; + // True if the user has a strong certificate. + optional bool strong_certificate = 18 [default = false]; + optional bool opus = 19 [default = false]; + + // Rolling packet statistics + optional RollingStats rolling_stats = 20; +} + +// Used by the client to request binary data from the server. By default large +// comments or textures are not sent within standard messages but instead the +// hash is. If the client does not recognize the hash it may request the +// resource when it needs it. The client does so by sending a RequestBlob +// message with the correct fields filled with the user sessions or channel_ids +// it wants to receive. The server replies to this by sending a new +// UserState/ChannelState message with the resources filled even if they would +// normally be transmitted as hashes. +message RequestBlob { + // sessions of the requested UserState textures. + repeated uint32 session_texture = 1; + // sessions of the requested UserState comments. + repeated uint32 session_comment = 2; + // channel_ids of the requested ChannelState descriptions. + repeated uint32 channel_description = 3; +} + +// Sent by the server when it informs the clients on server configuration +// details. +message ServerConfig { + // The maximum bandwidth the clients should use. + optional uint32 max_bandwidth = 1; + // Server welcome text. + optional string welcome_text = 2; + // True if the server allows HTML. + optional bool allow_html = 3; + // Maximum text message length. + optional uint32 message_length = 4; + // Maximum image message length. + optional uint32 image_message_length = 5; + // The maximum number of users allowed on the server. + optional uint32 max_users = 6; + // Whether using Mumble's recording feature is allowed on the server + optional bool recording_allowed = 7; +} + +// Sent by the server to inform the clients of suggested client configuration +// specified by the server administrator. +message SuggestConfig { + // Suggested client version in the legacy format. + optional uint32 version_v1 = 1; + // Suggested client version in the new format. + // Necessary since patch level may exceed 255. (See https://github.com/mumble-voip/mumble/issues/5827) + optional uint64 version_v2 = 4; + + // True if the administrator suggests positional audio to be used on this + // server. + optional bool positional = 2; + // True if the administrator suggests push to talk to be used on this server. + optional bool push_to_talk = 3; +} + +// Used to send plugin messages between clients +message PluginDataTransmission { + // The session ID of the client this message was sent from + optional uint32 senderSession = 1; + // The session IDs of the clients that should receive this message + repeated uint32 receiverSessions = 2 [packed = true]; + // The data that is sent + optional bytes data = 3; + // The ID of the sent data. This will be used by plugins to check whether they will + // process it or not + optional string dataID = 4; +} diff --git a/proto/MumbleUDP.proto b/proto/MumbleUDP.proto new file mode 100644 index 0000000..cb685d1 --- /dev/null +++ b/proto/MumbleUDP.proto @@ -0,0 +1,85 @@ +// Copyright The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +syntax = "proto3"; + +package MumbleUDP; + +option optimize_for = SPEED; + +message Audio { + oneof Header { + // When this audio is sent by the client to the server, this is set to the target of the audio data. This target + // is a number in the range [0, 2^{32} - 1], where 0 means "normal talking", 2^{5} - 1 means "server loopback" + // and all other targets are understood as shout/whisper targets that have previously been registered via a + // VoiceTarget message (via TCP). + uint32 target = 1; + // When this audio is sent by the server to the client, this indicates the context in which the audio has been sent. + // 0: Normal speech + // 1: Shout to channel + // 2: Whisper to user + // 3: Received via channel listener + uint32 context = 2; + }; + + // The session of the client (sender) this audio was originally sent from. This field is not required when sending + // audio to the server, but will always be set when receiving audio from the server. + uint32 sender_session = 3; + + // The number of the first contained audio frame (indicating the position of that frame in the overall audio stream) + uint64 frame_number = 4; + + // The actual voice data payload in the Opus format. + bytes opus_data = 5; + + // Optional positional data indicating the speaker's position in a virtual world (in meters). This "list" is really + // expected to be an array of size 3 containing the X, Y and Z coordinates of the position (in that order). + repeated float positional_data = 6; + + // A volume adjustment determined by the server for this audio packet. It is up to the client to apply this adjustment to + // the resulting audio (or not). Note: A value of 0 means that this field is unset. + float volume_adjustment = 7; + + // Note that we skip the field indices up to (including) 15 in order to have them available for future extensions of the + // protocol with fields that are encountered very often. The reason is that all field indices <= 15 require only a single + // byte of encoding overhead, whereas the once > 15 require (at least) two bytes. The reason lies in the Protobuf encoding + // scheme that uses 1 bit for a varint continuation flag, 3 bit to encode a field's type and the remaining 4 bit of the + // first byte are thus available for the field index. Therefore the first 2^4 = 16 field indices (aka values 0 to 15) can + // be encoded using only a single byte. For details see https://developers.google.com/protocol-buffers/docs/encoding + + // A flag indicating whether this audio packet represents the end of transmission for the current audio stream + bool is_terminator = 16; +} + +/** + * Ping message for checking UDP connectivity (and roundtrip ping) and potentially obtaining further server + * details (e.g. version). + */ +message Ping { + // Timestamp as encoded by the client. A server is not supposed to attempt to decode or modify this field. Therefore, + // clients may choose an arbitrary format for this timestamp (as long as it fits into a uint64 field). + uint64 timestamp = 1; + + // A flag set by the sending client, if it wants to obtain additional information about the server. + bool request_extended_information = 2; + + + // Below are the fields for the "additional information" that are filled out by the server on request. + + // The version of the server in the new version format. + // The new protobuf Ping packet introduced with 1.5 drops support for the legacy version format + // since both server and client have to support this new format. + // (See https://github.com/mumble-voip/mumble/issues/5827) + uint64 server_version_v2 = 3; + + // The amount of users currently connected to the server + uint32 user_count = 4; + + // The maximum amount of users permitted on this server + uint32 max_user_count = 5; + + // The maximum bandwidth each user is allowed to use for sending audio to the server + uint32 max_bandwidth_per_user = 6; +} diff --git a/scripts/build-electron.mjs b/scripts/build-electron.mjs new file mode 100644 index 0000000..95199a1 --- /dev/null +++ b/scripts/build-electron.mjs @@ -0,0 +1,17 @@ +import * as esbuild from 'esbuild'; + +// Bundles the Electron main and preload scripts to CommonJS in dist-electron/. +export const options = { + entryPoints: { main: 'electron/main.ts', preload: 'electron/preload.ts' }, + outdir: 'dist-electron', + outExtension: { '.js': '.cjs' }, + bundle: true, + platform: 'node', + format: 'cjs', + target: 'node22', + external: ['electron'], + sourcemap: true, + logLevel: 'info' +}; + +if (import.meta.url === `file://${process.argv[1]}`) await esbuild.build(options); diff --git a/scripts/dev.mjs b/scripts/dev.mjs new file mode 100644 index 0000000..ce4ff2b --- /dev/null +++ b/scripts/dev.mjs @@ -0,0 +1,28 @@ +import { spawn } from 'node:child_process'; +import * as esbuild from 'esbuild'; +import { createServer } from 'vite'; +import electronPath from 'electron'; +import { options } from './build-electron.mjs'; + +// Vite dev server for the renderer, esbuild watch for main/preload, Electron restarted on main changes. +const server = await createServer(); +await server.listen(); +const url = server.resolvedUrls.local[0]; + +let child = null; +const startElectron = () => { + if (child) { + child.removeAllListeners('exit'); + child.kill(); + } + // ELECTRON_RUN_AS_NODE (set by some editor terminals) would start Electron as plain Node + const { ELECTRON_RUN_AS_NODE, ...env } = process.env; + child = spawn(electronPath, ['.'], { stdio: 'inherit', env: { ...env, VITE_DEV_SERVER_URL: url } }); + child.on('exit', () => { server.close(); process.exit(0); }); +}; + +const ctx = await esbuild.context({ + ...options, + plugins: [{ name: 'restart', setup(b) { b.onEnd(r => { if (!r.errors.length) startElectron(); }); } }] +}); +await ctx.watch(); diff --git a/src/App.svelte b/src/App.svelte new file mode 100644 index 0000000..2d8bd42 --- /dev/null +++ b/src/App.svelte @@ -0,0 +1,95 @@ + + + + +
+ {#if layout !== 'narrow' || navOpen} + + {#if layout === 'narrow'}{/if} + {/if} + + (navOpen = true)} onpanel={togglePanel} /> + + {#if showPanel} + {#if layout === 'wide'} + {}} onclose={() => (ui.panelOpen = false)} /> + {:else} + +
(ui.panelOpen = false)} />
+ {/if} + {/if} +
+ +{#if editing} (editing = null)} />{/if} +{#if ui.settingsOpen} { ui.settingsOpen = false; ui.settingsTab = 'voice'; }} />{/if} +{#if ui.quickAudio}{/if} +{#if ui.userInfo != null && session.status === 'connected'} + {#key ui.userInfo} (ui.userInfo = null)} />{/key} +{/if} +{#if session.certPrompt}{/if} +{#if ui.cert} (ui.cert = null)} />{/if} +{#if identities.loaded && !identities.setupDone} {}} /> +{:else if ui.wizard} (ui.wizard = null)} />{/if} +{#if ui.prompt} (ui.prompt = null)} />{/if} + + + diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..082cbd9 --- /dev/null +++ b/src/app.css @@ -0,0 +1,81 @@ +:root { + --bg-0: #0d0e10; + --bg-1: #151619; + --bg-2: #1b1c20; + --bg-3: #24262b; + --bg-hover: #2a2c32; + --bg-active: #33363d; + --line: #2c2e34; + --text: #e3e4e8; + --text-dim: #9a9ca5; + --text-faint: #686a73; + --accent: #7c6cff; + --accent-hover: #8f81ff; + --accent-text: #ffffff; + --speaking: #3bd16f; + --danger: #f0504a; + --warn: #e8b23c; + --link: #6fb3ff; + + --font: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, sans-serif; + --mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace; + --fs: 15px; + + color-scheme: dark; +} + +*, *::before, *::after { box-sizing: border-box; border-radius: 0 !important; } + +html, body, #app { height: 100%; margin: 0; } + +body { + background: var(--bg-2); + color: var(--text); + font: var(--fs)/1.45 var(--font); + overflow: hidden; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--link); text-decoration: none; } +a:hover { text-decoration: underline; } + +button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; } +button:disabled { cursor: default; opacity: 0.5; } + +input, textarea, select { + font: inherit; + color: var(--text); + background: var(--bg-0); + border: 1px solid var(--line); + padding: 8px 10px; + outline: none; + width: 100%; +} +input:focus, textarea:focus, select:focus { border-color: var(--accent); } + +label { display: block; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); margin: 14px 0 6px; } + +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 8px; + padding: 8px 16px; font-weight: 600; background: var(--bg-3); border: 1px solid var(--line); +} +.btn:hover:not(:disabled) { background: var(--bg-hover); } +.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); } +.btn.primary:hover:not(:disabled) { background: var(--accent-hover); } +.btn.danger { background: transparent; border-color: var(--danger); color: var(--danger); } + +.icon-btn { + display: inline-flex; align-items: center; justify-content: center; + width: 32px; height: 32px; color: var(--text-dim); +} +.icon-btn:hover { background: var(--bg-hover); color: var(--text); } +.icon-btn.on { color: var(--danger); } + +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-thumb { background: var(--bg-3); border: 2px solid transparent; background-clip: padding-box; } +::-webkit-scrollbar-thumb:hover { background-color: var(--bg-active); } +::-webkit-scrollbar-track { background: transparent; } + +:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } + +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } diff --git a/src/core/client.ts b/src/core/client.ts new file mode 100644 index 0000000..a8546eb --- /dev/null +++ b/src/core/client.ts @@ -0,0 +1,482 @@ +import { Emitter } from './emitter.ts'; +import { FrameReader, frame, type Codec, type MessageName } from './proto.ts'; +import type { Transport } from './transport.ts'; + +export const CLIENT_RELEASE = 'mumh5 0.1.0'; +const VERSION = { major: 1, minor: 5, patch: 0 }; +const PING_INTERVAL_MS = 15000; + +// Murmur drops these messages without telling the client when they come too fast +// (leaky bucket, defaults messageburst=5 and messagelimit=1/s). Mirroring the bucket and +// queueing keeps quick actions from getting lost. Slightly slower refill for safety. +const RATE_BURST = 5; +const RATE_REFILL_MS = 1100; +const RATE_LIMITED = new Set(['TextMessage', 'ChannelState', 'ACL', 'Version']); + +export interface Channel { + id: number; + parent: number | null; + name: string; + description: string; + descriptionHash: Uint8Array | null; + position: number; + temporary: boolean; + links: Set; + maxUsers: number; + canEnter: boolean; +} + +export interface User { + session: number; + name: string; + userId: number | null; + channelId: number; + mute: boolean; + deaf: boolean; + suppress: boolean; + selfMute: boolean; + selfDeaf: boolean; + prioritySpeaker: boolean; + recording: boolean; + comment: string; + commentHash: Uint8Array | null; + texture: Uint8Array | null; + textureHash: Uint8Array | null; + hash: string; +} + +export interface TextMessage { + actor: number | null; + sessions: number[]; + channels: number[]; + trees: number[]; + html: string; + time: number; +} + +export interface ServerConfig { + allowHtml: boolean; + messageLength: number; + imageMessageLength: number; + maxUsers: number; + recordingAllowed: boolean; +} + +export interface Denial { + type: number; + permission: number; + reason: string; + channelId: number | null; + session: number | null; + name: string; +} + +// ACL permission bits, as used in PermissionDenied.permission +export const PERMISSIONS: Record = { + 0x1: 'Write ACL', 0x2: 'Traverse', 0x4: 'Enter', 0x8: 'Speak', 0x10: 'Mute/Deafen', 0x20: 'Move', + 0x40: 'Make channel', 0x80: 'Link channel', 0x100: 'Whisper', 0x200: 'Text message', + 0x400: 'Make temporary channel', 0x800: 'Listen', 0x10000: 'Kick', 0x20000: 'Ban', + 0x40000: 'Register', 0x80000: 'Register self', 0x100000: 'Reset user content' +}; + +// Human-readable text for a PermissionDenied message +export function describeDenial(d: Denial, channelName?: string): string { + switch (d.type) { + case 0: return d.reason || 'Denied by the server'; + case 1: { + const what = PERMISSIONS[d.permission] ?? 'this action'; + return `You do not have permission for ${what}${channelName ? ` in ${channelName}` : ''}`; + } + case 2: return 'The SuperUser cannot be modified'; + case 3: return 'Invalid channel name'; + case 4: return 'Message too long for this server'; + case 6: return 'Not allowed in a temporary channel'; + case 7: return 'This needs a registered certificate'; + case 8: return `Invalid user name${d.name ? `: ${d.name}` : ''}`; + case 9: return 'The channel is full'; + case 10: return 'Channels are nested too deeply'; + case 11: return 'The server has reached its channel limit'; + default: return d.reason || 'Denied by the server'; + } +} + +export interface ConnectOptions { + username: string; + password?: string; + tokens?: string[]; + os?: string; + osVersion?: string; +} + +type ClientEvents = { + synced: () => void; + channel: (ch: Channel) => void; + channelRemove: (id: number) => void; + user: (user: User, changed: string[], actor: number | null, isNew: boolean) => void; + userRemove: (user: User, actor: number | null, reason: string, ban: boolean) => void; + text: (msg: TextMessage) => void; + pluginData: (sender: number, dataId: string, data: Uint8Array) => void; + permissionDenied: (msg: Denial) => void; + reject: (type: number, reason: string) => void; + voice: (packet: Uint8Array) => void; + userStats: (stats: any) => void; + ping: (rttMs: number) => void; + close: (reason: string) => void; + message: (name: MessageName, msg: any) => void; +}; + +export class MumbleClient extends Emitter { + readonly channels = new Map(); + readonly users = new Map(); + session: number | null = null; + synced = false; + welcomeText = ''; + maxBandwidth = 0; + serverVersion = ''; + // Numeric server version (major << 16 | minor << 8 | patch), 0 until known + serverVersionNum = 0; + rtt = 0; + config: ServerConfig = { allowHtml: true, messageLength: 5000, imageMessageLength: 131072, maxUsers: 0, recordingAllowed: true }; + + private transport: Transport | null = null; + private reader = new FrameReader(); + private pingTimer: ReturnType | null = null; + private closed = false; + private tokens = RATE_BURST; + private lastRefill = Date.now(); + private queue: Uint8Array[] = []; + private queueTimer: ReturnType | null = null; + + private codec: Codec; + + constructor(codec: Codec) { + super(); + this.codec = codec; + } + + get self(): User | null { + return this.session == null ? null : this.users.get(this.session) ?? null; + } + + connect(transport: Transport, opts: ConnectOptions): void { + this.transport = transport; + transport.onData = chunk => { + try { + this.reader.push(chunk, (id, body) => this.handleFrame(id, body)); + } catch (e) { + this.disconnect(`Protocol error: ${(e as Error).message}`); + } + }; + transport.onClose = reason => this.handleClose(reason); + + const { major, minor, patch } = VERSION; + this.send('Version', { + version_v1: (major << 16) | (minor << 8) | patch, + version_v2: major * 2 ** 48 + minor * 2 ** 32 + patch * 2 ** 16, + release: CLIENT_RELEASE, + os: opts.os ?? 'unknown', + os_version: opts.osVersion ?? '' + }); + this.send('Authenticate', { + username: opts.username, + password: opts.password ?? '', + tokens: opts.tokens ?? [], + opus: true, + client_type: 0 + }); + this.pingTimer = setInterval(() => this.ping(), PING_INTERVAL_MS); + } + + disconnect(reason = 'Disconnected'): void { + this.transport?.close(); + this.handleClose(reason); + } + + send(name: MessageName, payload: Record): void { + if (!this.transport || this.closed) return; + const bytes = this.codec.encode(name, payload); + // Only changes to our own user state count against the bucket + const ownState = name === 'UserState' && (payload.session == null || payload.session === this.session); + if (RATE_LIMITED.has(name) || ownState) this.sendPaced(bytes); + else this.transport.send(bytes); + } + + // Messages still waiting for the rate limit + get queued(): number { + return this.queue.length; + } + + private refill(): void { + const now = Date.now(); + const gained = Math.floor((now - this.lastRefill) / RATE_REFILL_MS); + if (gained > 0) { + this.tokens = Math.min(RATE_BURST, this.tokens + gained); + this.lastRefill += gained * RATE_REFILL_MS; + } + if (this.tokens === RATE_BURST) this.lastRefill = now; + } + + private sendPaced(bytes: Uint8Array): void { + this.queue.push(bytes); + this.drain(); + } + + private drain(): void { + if (this.queueTimer || this.closed) return; + this.refill(); + while (this.queue.length && this.tokens > 0) { + this.tokens--; + this.transport?.send(this.queue.shift()!); + } + if (this.queue.length) { + this.queueTimer = setTimeout(() => { this.queueTimer = null; this.drain(); }, RATE_REFILL_MS - (Date.now() - this.lastRefill) + 5); + } + } + + // ─── Actions ─────────────────────────────────────────────────────────────── + + sendText(target: { channels?: number[]; users?: number[]; trees?: number[] }, html: string): void { + this.send('TextMessage', { + session: target.users ?? [], + channel_id: target.channels ?? [], + tree_id: target.trees ?? [], + message: html + }); + } + + joinChannel(channelId: number): void { + if (this.session == null) return; + this.send('UserState', { session: this.session, channel_id: channelId }); + } + + setSelfMute(mute: boolean): void { + // Unmuting also undeafens, matching the desktop client + this.send('UserState', mute ? { self_mute: true } : { self_mute: false, self_deaf: false }); + } + + setSelfDeaf(deaf: boolean): void { + // Deafening implies muting + this.send('UserState', deaf ? { self_mute: true, self_deaf: true } : { self_deaf: false }); + } + + setComment(comment: string): void { + if (this.session == null) return; + this.send('UserState', { session: this.session, comment }); + } + + kick(session: number, reason: string, ban = false): void { + this.send('UserRemove', { session, reason, ban }); + } + + // Server-side (admin) state of another user + setUserState(session: number, state: { mute?: boolean; deaf?: boolean; priority_speaker?: boolean; channel_id?: number }): void { + this.send('UserState', { session, ...state }); + } + + // Connection details of a user; the server decides how much we may see + requestUserStats(session: number, statsOnly = false): void { + this.send('UserStats', { session, stats_only: statsOnly }); + } + + // Fetch comments/descriptions/textures that the server only announced by hash. + requestBlob(req: { textures?: number[]; comments?: number[]; descriptions?: number[] }): void { + this.send('RequestBlob', { + session_texture: req.textures ?? [], + session_comment: req.comments ?? [], + channel_description: req.descriptions ?? [] + }); + } + + sendPluginData(receivers: number[], dataId: string, data: Uint8Array): void { + if (this.session == null || !receivers.length) return; + this.send('PluginDataTransmission', { + senderSession: this.session, + receiverSessions: receivers, + data, + dataID: dataId + }); + } + + // Voice over TCP: the UDPTunnel body is the raw voice packet, not a protobuf message + sendVoiceTunnel(packet: Uint8Array): void { + if (!this.transport || this.closed) return; + this.transport.send(frame(1, packet)); + } + + // 1.5+ servers talk to 1.5+ clients in the protobuf voice format + get protobufVoice(): boolean { + return this.serverVersionNum >= 0x010500; + } + + // Registers a user (ourselves or, with the Register permission, someone else) on the server + register(session: number): void { + this.send('UserState', { session, user_id: 0 }); + } + + // ─── Incoming ────────────────────────────────────────────────────────────── + + private ping(): void { + this.send('Ping', { timestamp: Date.now() }); + } + + private handleClose(reason: string): void { + if (this.closed) return; + this.closed = true; + if (this.pingTimer) clearInterval(this.pingTimer); + if (this.queueTimer) clearTimeout(this.queueTimer); + this.pingTimer = null; + this.queueTimer = null; + this.queue = []; + this.emit('close', reason); + } + + private handleFrame(typeId: number, body: Uint8Array): void { + // UDPTunnel carries a raw voice packet, not a protobuf message + if (typeId === 1) { + this.emit('voice', body.slice()); + return; + } + const decoded = this.codec.decode(typeId, body); + if (!decoded) return; + const { name, msg } = decoded; + + switch (name) { + case 'Version': + this.serverVersion = msg.release ?? ''; + if (msg.version_v2) { + const v = Number(msg.version_v2); + this.serverVersionNum = (Math.floor(v / 2 ** 48) << 16) | ((Math.floor(v / 2 ** 32) & 0xffff) << 8) | (Math.floor(v / 2 ** 16) & 0xffff); + } else if (msg.version_v1) { + this.serverVersionNum = msg.version_v1; + } + break; + case 'Ping': + if (msg.timestamp) { + this.rtt = Date.now() - Number(msg.timestamp); + this.emit('ping', this.rtt); + } + break; + case 'Reject': + this.emit('reject', msg.type ?? 0, msg.reason ?? 'Rejected'); + this.disconnect(msg.reason || 'Connection rejected'); + break; + case 'ServerSync': + this.session = msg.session; + this.maxBandwidth = msg.max_bandwidth ?? 0; + this.welcomeText = msg.welcome_text ?? ''; + this.synced = true; + this.ping(); + this.emit('synced'); + break; + case 'ServerConfig': + if (msg.allow_html != null) this.config.allowHtml = msg.allow_html; + if (msg.message_length != null) this.config.messageLength = msg.message_length; + if (msg.image_message_length != null) this.config.imageMessageLength = msg.image_message_length; + if (msg.max_users != null) this.config.maxUsers = msg.max_users; + if (msg.recording_allowed != null) this.config.recordingAllowed = msg.recording_allowed; + break; + case 'ChannelState': + this.applyChannelState(msg); + break; + case 'ChannelRemove': + this.channels.delete(msg.channel_id); + this.emit('channelRemove', msg.channel_id); + break; + case 'UserState': + this.applyUserState(msg); + break; + case 'UserRemove': { + const user = this.users.get(msg.session); + if (user) { + this.users.delete(msg.session); + this.emit('userRemove', user, msg.actor ?? null, msg.reason ?? '', !!msg.ban); + } + break; + } + case 'TextMessage': + this.emit('text', { + actor: msg.actor ?? null, + sessions: msg.session ?? [], + channels: msg.channel_id ?? [], + trees: msg.tree_id ?? [], + html: msg.message ?? '', + time: Date.now() + }); + break; + case 'PermissionDenied': + this.emit('permissionDenied', { + type: msg.type ?? 0, + permission: msg.permission ?? 0, + reason: msg.reason ?? '', + channelId: msg.channel_id ?? null, + session: msg.session ?? null, + name: msg.name ?? '' + }); + break; + case 'UserStats': + this.emit('userStats', msg); + break; + case 'PluginDataTransmission': + if (msg.senderSession != null && msg.dataID) { + this.emit('pluginData', msg.senderSession, msg.dataID, msg.data ?? new Uint8Array(0)); + } + break; + } + this.emit('message', name, msg); + } + + private applyChannelState(msg: any): void { + const id: number = msg.channel_id; + let ch = this.channels.get(id); + if (!ch) { + ch = { + id, parent: null, name: '', description: '', descriptionHash: null, position: 0, + temporary: false, links: new Set(), maxUsers: 0, canEnter: true + }; + this.channels.set(id, ch); + } + if (msg.parent != null) ch.parent = msg.parent; + if (msg.name != null) ch.name = msg.name; + if (msg.description_hash != null && msg.description == null) ch.description = ''; + if (msg.description != null) ch.description = msg.description; + if (msg.description_hash != null) ch.descriptionHash = msg.description_hash; + if (msg.position != null) ch.position = msg.position; + if (msg.temporary != null) ch.temporary = msg.temporary; + if (msg.max_users != null) ch.maxUsers = msg.max_users; + if (msg.can_enter != null) ch.canEnter = msg.can_enter; + if (msg.links?.length) ch.links = new Set(msg.links); + for (const l of msg.links_add ?? []) ch.links.add(l); + for (const l of msg.links_remove ?? []) ch.links.delete(l); + this.emit('channel', ch); + } + + private applyUserState(msg: any): void { + const session: number = msg.session ?? this.session; + let user = this.users.get(session); + const isNew = !user; + if (!user) { + user = { + session, name: '', userId: null, channelId: 0, mute: false, deaf: false, suppress: false, + selfMute: false, selfDeaf: false, prioritySpeaker: false, recording: false, + comment: '', commentHash: null, texture: null, textureHash: null, hash: '' + }; + this.users.set(session, user); + } + const fields: [string, keyof User][] = [ + ['name', 'name'], ['user_id', 'userId'], ['channel_id', 'channelId'], ['mute', 'mute'], + ['deaf', 'deaf'], ['suppress', 'suppress'], ['self_mute', 'selfMute'], ['self_deaf', 'selfDeaf'], + ['priority_speaker', 'prioritySpeaker'], ['recording', 'recording'], ['comment', 'comment'], + ['comment_hash', 'commentHash'], ['texture', 'texture'], ['texture_hash', 'textureHash'], ['hash', 'hash'] + ]; + // A new hash without text means the text changed and must be fetched (RequestBlob) + if (msg.comment_hash != null && msg.comment == null) user.comment = ''; + const changed: string[] = []; + for (const [src, dst] of fields) { + if (msg[src] != null) { + (user as any)[dst] = msg[src]; + changed.push(dst); + } + } + this.emit('user', user, changed, msg.actor ?? null, isNew); + } +} diff --git a/src/core/emitter.ts b/src/core/emitter.ts new file mode 100644 index 0000000..fab07ac --- /dev/null +++ b/src/core/emitter.ts @@ -0,0 +1,15 @@ +// Minimal typed event emitter that works in both browser and Node. +export class Emitter void>> { + private handlers = new Map>(); + + on(event: K, fn: Events[K]): () => void { + let set = this.handlers.get(event); + if (!set) this.handlers.set(event, (set = new Set())); + set.add(fn); + return () => set!.delete(fn); + } + + emit(event: K, ...args: Parameters): void { + for (const fn of this.handlers.get(event) ?? []) fn(...args); + } +} diff --git a/src/core/mumble-pb.d.ts b/src/core/mumble-pb.d.ts new file mode 100644 index 0000000..ec12da0 --- /dev/null +++ b/src/core/mumble-pb.d.ts @@ -0,0 +1,2 @@ +// Types for the generated static module; the codec in proto.ts adds the real shape. +export const MumbleProto: Record; diff --git a/src/core/mumble-pb.js b/src/core/mumble-pb.js new file mode 100644 index 0000000..25773af --- /dev/null +++ b/src/core/mumble-pb.js @@ -0,0 +1,8465 @@ +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-mixed-operators, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars, default-case, jsdoc/require-param*/ +import $protobuf from "protobufjs/minimal.js"; + +const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; +const $Object = $util.global.Object, $undefined = $util.global.undefined, $Error = $util.global.Error, $RangeError = $util.global.RangeError, $TypeError = $util.global.TypeError, $parseInt = $util.global.parseInt, $String = $util.global.String, $Number = $util.global.Number, $BigInt = $util.global.BigInt, $Array = $util.global.Array, $Boolean = $util.global.Boolean, $isFinite = $util.global.isFinite; + +const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); + +export const MumbleProto = $root.MumbleProto = (() => { + + const MumbleProto = {}; + + MumbleProto.Version = (function() { + + const Version = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + Version.prototype.version_v1 = 0; + Version.prototype.version_v2 = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + Version.prototype.release = ""; + Version.prototype.os = ""; + Version.prototype.os_version = ""; + + Version.create = function(properties) { + return new Version(properties); + }; + + Version.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.version_v1 != null && $Object.hasOwnProperty.call(message, "version_v1")) + writer.uint32(8).uint32(message.version_v1); + if (message.release != null && $Object.hasOwnProperty.call(message, "release")) + writer.uint32(18).string(message.release); + if (message.os != null && $Object.hasOwnProperty.call(message, "os")) + writer.uint32(26).string(message.os); + if (message.os_version != null && $Object.hasOwnProperty.call(message, "os_version")) + writer.uint32(34).string(message.os_version); + if (message.version_v2 != null && $Object.hasOwnProperty.call(message, "version_v2")) + writer.uint32(40).uint64(message.version_v2); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + Version.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.Version(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.version_v1 = reader.uint32(); + continue; + } + case 5: { + if (wireType !== 0) + break; + message.version_v2 = reader.uint64(); + continue; + } + case 2: { + if (wireType !== 2) + break; + message.release = reader.string(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.os = reader.string(); + continue; + } + case 4: { + if (wireType !== 2) + break; + message.os_version = reader.string(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + Version.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.version_v1 != null && $Object.hasOwnProperty.call(message, "version_v1")) + if (!$util.isInteger(message.version_v1)) + return "version_v1: integer expected"; + if (message.version_v2 != null && $Object.hasOwnProperty.call(message, "version_v2")) + if (!$util.isInteger(message.version_v2) && !(message.version_v2 && $util.isInteger(message.version_v2.low) && $util.isInteger(message.version_v2.high))) + return "version_v2: integer|Long expected"; + if (message.release != null && $Object.hasOwnProperty.call(message, "release")) + if (!$util.isString(message.release)) + return "release: string expected"; + if (message.os != null && $Object.hasOwnProperty.call(message, "os")) + if (!$util.isString(message.os)) + return "os: string expected"; + if (message.os_version != null && $Object.hasOwnProperty.call(message, "os_version")) + if (!$util.isString(message.os_version)) + return "os_version: string expected"; + return null; + }; + + Version.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.Version) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.Version: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.Version(); + if (object.version_v1 != null) + message.version_v1 = object.version_v1 >>> 0; + if (object.version_v2 != null) + if ($util.Long) + message.version_v2 = $util.Long.fromValue(object.version_v2, true); + else if (typeof object.version_v2 === "string") + message.version_v2 = $parseInt(object.version_v2, 10); + else if (typeof object.version_v2 === "number") + message.version_v2 = object.version_v2; + else if (typeof object.version_v2 === "object") + message.version_v2 = new $util.LongBits(object.version_v2.low >>> 0, object.version_v2.high >>> 0).toNumber(true); + if (object.release != null) + message.release = $String(object.release); + if (object.os != null) + message.os = $String(object.os); + if (object.os_version != null) + message.os_version = $String(object.os_version); + return message; + }; + + Version.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.version_v1 = 0; + object.release = ""; + object.os = ""; + object.os_version = ""; + if ($util.Long) { + let long = new $util.Long(0, 0, true); + object.version_v2 = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long; + } else + object.version_v2 = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0; + } + if (message.version_v1 != null && $Object.hasOwnProperty.call(message, "version_v1")) + object.version_v1 = message.version_v1; + if (message.release != null && $Object.hasOwnProperty.call(message, "release")) + object.release = message.release; + if (message.os != null && $Object.hasOwnProperty.call(message, "os")) + object.os = message.os; + if (message.os_version != null && $Object.hasOwnProperty.call(message, "os_version")) + object.os_version = message.os_version; + if (message.version_v2 != null && $Object.hasOwnProperty.call(message, "version_v2")) + if (typeof $BigInt !== "undefined" && options.longs === $BigInt) + object.version_v2 = typeof message.version_v2 === "number" ? $BigInt(message.version_v2) : $util.Long.fromBits(message.version_v2.low >>> 0, message.version_v2.high >>> 0, true).toBigInt(); + else if (typeof message.version_v2 === "number") + object.version_v2 = options.longs === $String ? $String(message.version_v2) : message.version_v2; + else + object.version_v2 = options.longs === $String ? $util.Long.prototype.toString.call(message.version_v2) : options.longs === $Number ? new $util.LongBits(message.version_v2.low >>> 0, message.version_v2.high >>> 0).toNumber(true) : message.version_v2; + return object; + }; + + Version.prototype.toJSON = function() { + return Version.toObject(this, $protobuf.util.toJSONOptions); + }; + + Version.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.Version"; + }; + + return Version; + })(); + + MumbleProto.UDPTunnel = (function() { + + const UDPTunnel = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + UDPTunnel.prototype.packet = $util.newBuffer([]); + + UDPTunnel.create = function(properties) { + return new UDPTunnel(properties); + }; + + UDPTunnel.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(10).bytes(message.packet); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + UDPTunnel.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UDPTunnel(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 2) + break; + message.packet = reader.bytes(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "packet")) + throw $util.ProtocolError("missing required 'packet'", { instance: message }); + return message; + }; + + UDPTunnel.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!(message.packet && typeof message.packet.length === "number" || $util.isString(message.packet))) + return "packet: buffer expected"; + return null; + }; + + UDPTunnel.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UDPTunnel) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UDPTunnel: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UDPTunnel(); + if (object.packet != null) + if (typeof object.packet === "string") + $util.base64.decode(object.packet, message.packet = $util.newBuffer($util.base64.length(object.packet)), 0); + else if (object.packet.length >= 0) + message.packet = object.packet; + return message; + }; + + UDPTunnel.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) + if (options.bytes === $String) + object.packet = ""; + else { + object.packet = []; + if (options.bytes !== $Array) + object.packet = $util.newBuffer(object.packet); + } + if (message.packet != null && $Object.hasOwnProperty.call(message, "packet")) + object.packet = options.bytes === $String ? $util.base64.encode(message.packet, 0, message.packet.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.packet) : message.packet; + return object; + }; + + UDPTunnel.prototype.toJSON = function() { + return UDPTunnel.toObject(this, $protobuf.util.toJSONOptions); + }; + + UDPTunnel.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UDPTunnel"; + }; + + return UDPTunnel; + })(); + + MumbleProto.Authenticate = (function() { + + const Authenticate = function (properties) { + this.tokens = []; + this.celt_versions = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + Authenticate.prototype.username = ""; + Authenticate.prototype.password = ""; + Authenticate.prototype.tokens = $util.emptyArray; + Authenticate.prototype.celt_versions = $util.emptyArray; + Authenticate.prototype.opus = false; + Authenticate.prototype.client_type = 0; + + Authenticate.create = function(properties) { + return new Authenticate(properties); + }; + + Authenticate.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.username != null && $Object.hasOwnProperty.call(message, "username")) + writer.uint32(10).string(message.username); + if (message.password != null && $Object.hasOwnProperty.call(message, "password")) + writer.uint32(18).string(message.password); + if (message.tokens != null && message.tokens.length) + for (let i = 0; i < message.tokens.length; ++i) + writer.uint32(26).string(message.tokens[i]); + if (message.celt_versions != null && message.celt_versions.length) + for (let i = 0; i < message.celt_versions.length; ++i) + writer.uint32(32).int32(message.celt_versions[i]); + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + writer.uint32(40).bool(message.opus); + if (message.client_type != null && $Object.hasOwnProperty.call(message, "client_type")) + writer.uint32(48).int32(message.client_type); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + Authenticate.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.Authenticate(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 2) + break; + message.username = reader.string(); + continue; + } + case 2: { + if (wireType !== 2) + break; + message.password = reader.string(); + continue; + } + case 3: { + if (wireType !== 2) + break; + if (!(message.tokens && message.tokens.length)) + message.tokens = []; + message.tokens.push(reader.string()); + continue; + } + case 4: { + if (wireType === 2) { + if (!(message.celt_versions && message.celt_versions.length)) + message.celt_versions = []; + reader.int32s(message.celt_versions); + continue; + } + if (wireType !== 0) + break; + if (!(message.celt_versions && message.celt_versions.length)) + message.celt_versions = []; + message.celt_versions.push(reader.int32()); + continue; + } + case 5: { + if (wireType !== 0) + break; + message.opus = reader.bool(); + continue; + } + case 6: { + if (wireType !== 0) + break; + message.client_type = reader.int32(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + Authenticate.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.username != null && $Object.hasOwnProperty.call(message, "username")) + if (!$util.isString(message.username)) + return "username: string expected"; + if (message.password != null && $Object.hasOwnProperty.call(message, "password")) + if (!$util.isString(message.password)) + return "password: string expected"; + if (message.tokens != null && $Object.hasOwnProperty.call(message, "tokens")) { + if (!$Array.isArray(message.tokens)) + return "tokens: array expected"; + for (let i = 0; i < message.tokens.length; ++i) + if (!$util.isString(message.tokens[i])) + return "tokens: string[] expected"; + } + if (message.celt_versions != null && $Object.hasOwnProperty.call(message, "celt_versions")) { + if (!$Array.isArray(message.celt_versions)) + return "celt_versions: array expected"; + for (let i = 0; i < message.celt_versions.length; ++i) + if (!$util.isInteger(message.celt_versions[i])) + return "celt_versions: integer[] expected"; + } + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + if (typeof message.opus !== "boolean") + return "opus: boolean expected"; + if (message.client_type != null && $Object.hasOwnProperty.call(message, "client_type")) + if (!$util.isInteger(message.client_type)) + return "client_type: integer expected"; + return null; + }; + + Authenticate.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.Authenticate) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.Authenticate: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.Authenticate(); + if (object.username != null) + message.username = $String(object.username); + if (object.password != null) + message.password = $String(object.password); + if (object.tokens) { + if (!$Array.isArray(object.tokens)) + throw $TypeError(".MumbleProto.Authenticate.tokens: array expected"); + message.tokens = $Array(object.tokens.length); + for (let i = 0; i < object.tokens.length; ++i) + message.tokens[i] = $String(object.tokens[i]); + } + if (object.celt_versions) { + if (!$Array.isArray(object.celt_versions)) + throw $TypeError(".MumbleProto.Authenticate.celt_versions: array expected"); + message.celt_versions = $Array(object.celt_versions.length); + for (let i = 0; i < object.celt_versions.length; ++i) + message.celt_versions[i] = object.celt_versions[i] | 0; + } + if (object.opus != null) + message.opus = $Boolean(object.opus); + if (object.client_type != null) + message.client_type = object.client_type | 0; + return message; + }; + + Authenticate.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.tokens = []; + object.celt_versions = []; + } + if (options.defaults) { + object.username = ""; + object.password = ""; + object.opus = false; + object.client_type = 0; + } + if (message.username != null && $Object.hasOwnProperty.call(message, "username")) + object.username = message.username; + if (message.password != null && $Object.hasOwnProperty.call(message, "password")) + object.password = message.password; + if (message.tokens && message.tokens.length) { + object.tokens = $Array(message.tokens.length); + for (let j = 0; j < message.tokens.length; ++j) + object.tokens[j] = message.tokens[j]; + } + if (message.celt_versions && message.celt_versions.length) { + object.celt_versions = $Array(message.celt_versions.length); + for (let j = 0; j < message.celt_versions.length; ++j) + object.celt_versions[j] = message.celt_versions[j]; + } + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + object.opus = message.opus; + if (message.client_type != null && $Object.hasOwnProperty.call(message, "client_type")) + object.client_type = message.client_type; + return object; + }; + + Authenticate.prototype.toJSON = function() { + return Authenticate.toObject(this, $protobuf.util.toJSONOptions); + }; + + Authenticate.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.Authenticate"; + }; + + return Authenticate; + })(); + + MumbleProto.Ping = (function() { + + const Ping = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + Ping.prototype.timestamp = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + Ping.prototype.good = 0; + Ping.prototype.late = 0; + Ping.prototype.lost = 0; + Ping.prototype.resync = 0; + Ping.prototype.udp_packets = 0; + Ping.prototype.tcp_packets = 0; + Ping.prototype.udp_ping_avg = 0; + Ping.prototype.udp_ping_var = 0; + Ping.prototype.tcp_ping_avg = 0; + Ping.prototype.tcp_ping_var = 0; + + Ping.create = function(properties) { + return new Ping(properties); + }; + + Ping.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp")) + writer.uint32(8).uint64(message.timestamp); + if (message.good != null && $Object.hasOwnProperty.call(message, "good")) + writer.uint32(16).uint32(message.good); + if (message.late != null && $Object.hasOwnProperty.call(message, "late")) + writer.uint32(24).uint32(message.late); + if (message.lost != null && $Object.hasOwnProperty.call(message, "lost")) + writer.uint32(32).uint32(message.lost); + if (message.resync != null && $Object.hasOwnProperty.call(message, "resync")) + writer.uint32(40).uint32(message.resync); + if (message.udp_packets != null && $Object.hasOwnProperty.call(message, "udp_packets")) + writer.uint32(48).uint32(message.udp_packets); + if (message.tcp_packets != null && $Object.hasOwnProperty.call(message, "tcp_packets")) + writer.uint32(56).uint32(message.tcp_packets); + if (message.udp_ping_avg != null && $Object.hasOwnProperty.call(message, "udp_ping_avg")) + writer.uint32(69).float(message.udp_ping_avg); + if (message.udp_ping_var != null && $Object.hasOwnProperty.call(message, "udp_ping_var")) + writer.uint32(77).float(message.udp_ping_var); + if (message.tcp_ping_avg != null && $Object.hasOwnProperty.call(message, "tcp_ping_avg")) + writer.uint32(85).float(message.tcp_ping_avg); + if (message.tcp_ping_var != null && $Object.hasOwnProperty.call(message, "tcp_ping_var")) + writer.uint32(93).float(message.tcp_ping_var); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + Ping.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.Ping(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.timestamp = reader.uint64(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.good = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.late = reader.uint32(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.lost = reader.uint32(); + continue; + } + case 5: { + if (wireType !== 0) + break; + message.resync = reader.uint32(); + continue; + } + case 6: { + if (wireType !== 0) + break; + message.udp_packets = reader.uint32(); + continue; + } + case 7: { + if (wireType !== 0) + break; + message.tcp_packets = reader.uint32(); + continue; + } + case 8: { + if (wireType !== 5) + break; + message.udp_ping_avg = reader.float(); + continue; + } + case 9: { + if (wireType !== 5) + break; + message.udp_ping_var = reader.float(); + continue; + } + case 10: { + if (wireType !== 5) + break; + message.tcp_ping_avg = reader.float(); + continue; + } + case 11: { + if (wireType !== 5) + break; + message.tcp_ping_var = reader.float(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + Ping.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp")) + if (!$util.isInteger(message.timestamp) && !(message.timestamp && $util.isInteger(message.timestamp.low) && $util.isInteger(message.timestamp.high))) + return "timestamp: integer|Long expected"; + if (message.good != null && $Object.hasOwnProperty.call(message, "good")) + if (!$util.isInteger(message.good)) + return "good: integer expected"; + if (message.late != null && $Object.hasOwnProperty.call(message, "late")) + if (!$util.isInteger(message.late)) + return "late: integer expected"; + if (message.lost != null && $Object.hasOwnProperty.call(message, "lost")) + if (!$util.isInteger(message.lost)) + return "lost: integer expected"; + if (message.resync != null && $Object.hasOwnProperty.call(message, "resync")) + if (!$util.isInteger(message.resync)) + return "resync: integer expected"; + if (message.udp_packets != null && $Object.hasOwnProperty.call(message, "udp_packets")) + if (!$util.isInteger(message.udp_packets)) + return "udp_packets: integer expected"; + if (message.tcp_packets != null && $Object.hasOwnProperty.call(message, "tcp_packets")) + if (!$util.isInteger(message.tcp_packets)) + return "tcp_packets: integer expected"; + if (message.udp_ping_avg != null && $Object.hasOwnProperty.call(message, "udp_ping_avg")) + if (typeof message.udp_ping_avg !== "number") + return "udp_ping_avg: number expected"; + if (message.udp_ping_var != null && $Object.hasOwnProperty.call(message, "udp_ping_var")) + if (typeof message.udp_ping_var !== "number") + return "udp_ping_var: number expected"; + if (message.tcp_ping_avg != null && $Object.hasOwnProperty.call(message, "tcp_ping_avg")) + if (typeof message.tcp_ping_avg !== "number") + return "tcp_ping_avg: number expected"; + if (message.tcp_ping_var != null && $Object.hasOwnProperty.call(message, "tcp_ping_var")) + if (typeof message.tcp_ping_var !== "number") + return "tcp_ping_var: number expected"; + return null; + }; + + Ping.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.Ping) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.Ping: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.Ping(); + if (object.timestamp != null) + if ($util.Long) + message.timestamp = $util.Long.fromValue(object.timestamp, true); + else if (typeof object.timestamp === "string") + message.timestamp = $parseInt(object.timestamp, 10); + else if (typeof object.timestamp === "number") + message.timestamp = object.timestamp; + else if (typeof object.timestamp === "object") + message.timestamp = new $util.LongBits(object.timestamp.low >>> 0, object.timestamp.high >>> 0).toNumber(true); + if (object.good != null) + message.good = object.good >>> 0; + if (object.late != null) + message.late = object.late >>> 0; + if (object.lost != null) + message.lost = object.lost >>> 0; + if (object.resync != null) + message.resync = object.resync >>> 0; + if (object.udp_packets != null) + message.udp_packets = object.udp_packets >>> 0; + if (object.tcp_packets != null) + message.tcp_packets = object.tcp_packets >>> 0; + if (object.udp_ping_avg != null) + message.udp_ping_avg = $Number(object.udp_ping_avg); + if (object.udp_ping_var != null) + message.udp_ping_var = $Number(object.udp_ping_var); + if (object.tcp_ping_avg != null) + message.tcp_ping_avg = $Number(object.tcp_ping_avg); + if (object.tcp_ping_var != null) + message.tcp_ping_var = $Number(object.tcp_ping_var); + return message; + }; + + Ping.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + if ($util.Long) { + let long = new $util.Long(0, 0, true); + object.timestamp = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long; + } else + object.timestamp = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0; + object.good = 0; + object.late = 0; + object.lost = 0; + object.resync = 0; + object.udp_packets = 0; + object.tcp_packets = 0; + object.udp_ping_avg = 0; + object.udp_ping_var = 0; + object.tcp_ping_avg = 0; + object.tcp_ping_var = 0; + } + if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp")) + if (typeof $BigInt !== "undefined" && options.longs === $BigInt) + object.timestamp = typeof message.timestamp === "number" ? $BigInt(message.timestamp) : $util.Long.fromBits(message.timestamp.low >>> 0, message.timestamp.high >>> 0, true).toBigInt(); + else if (typeof message.timestamp === "number") + object.timestamp = options.longs === $String ? $String(message.timestamp) : message.timestamp; + else + object.timestamp = options.longs === $String ? $util.Long.prototype.toString.call(message.timestamp) : options.longs === $Number ? new $util.LongBits(message.timestamp.low >>> 0, message.timestamp.high >>> 0).toNumber(true) : message.timestamp; + if (message.good != null && $Object.hasOwnProperty.call(message, "good")) + object.good = message.good; + if (message.late != null && $Object.hasOwnProperty.call(message, "late")) + object.late = message.late; + if (message.lost != null && $Object.hasOwnProperty.call(message, "lost")) + object.lost = message.lost; + if (message.resync != null && $Object.hasOwnProperty.call(message, "resync")) + object.resync = message.resync; + if (message.udp_packets != null && $Object.hasOwnProperty.call(message, "udp_packets")) + object.udp_packets = message.udp_packets; + if (message.tcp_packets != null && $Object.hasOwnProperty.call(message, "tcp_packets")) + object.tcp_packets = message.tcp_packets; + if (message.udp_ping_avg != null && $Object.hasOwnProperty.call(message, "udp_ping_avg")) + object.udp_ping_avg = options.json && !$isFinite(message.udp_ping_avg) ? $String(message.udp_ping_avg) : message.udp_ping_avg; + if (message.udp_ping_var != null && $Object.hasOwnProperty.call(message, "udp_ping_var")) + object.udp_ping_var = options.json && !$isFinite(message.udp_ping_var) ? $String(message.udp_ping_var) : message.udp_ping_var; + if (message.tcp_ping_avg != null && $Object.hasOwnProperty.call(message, "tcp_ping_avg")) + object.tcp_ping_avg = options.json && !$isFinite(message.tcp_ping_avg) ? $String(message.tcp_ping_avg) : message.tcp_ping_avg; + if (message.tcp_ping_var != null && $Object.hasOwnProperty.call(message, "tcp_ping_var")) + object.tcp_ping_var = options.json && !$isFinite(message.tcp_ping_var) ? $String(message.tcp_ping_var) : message.tcp_ping_var; + return object; + }; + + Ping.prototype.toJSON = function() { + return Ping.toObject(this, $protobuf.util.toJSONOptions); + }; + + Ping.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.Ping"; + }; + + return Ping; + })(); + + MumbleProto.Reject = (function() { + + const Reject = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + Reject.prototype.type = 0; + Reject.prototype.reason = ""; + + Reject.create = function(properties) { + return new Reject(properties); + }; + + Reject.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.type != null && $Object.hasOwnProperty.call(message, "type")) + writer.uint32(8).int32(message.type); + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + writer.uint32(18).string(message.reason); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + Reject.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message, value; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.Reject(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + value = reader.int32(); + if ($root.MumbleProto.Reject.RejectType[value] !== $undefined) + message.type = value; + else if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + continue; + } + case 2: { + if (wireType !== 2) + break; + message.reason = reader.string(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + Reject.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.type != null && $Object.hasOwnProperty.call(message, "type")) + switch (message.type) { + default: + return "type: enum value expected"; + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + break; + } + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + if (!$util.isString(message.reason)) + return "reason: string expected"; + return null; + }; + + Reject.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.Reject) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.Reject: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.Reject(); + switch (object.type) { + case "None": + case 0: + message.type = 0; + break; + case "WrongVersion": + case 1: + message.type = 1; + break; + case "InvalidUsername": + case 2: + message.type = 2; + break; + case "WrongUserPW": + case 3: + message.type = 3; + break; + case "WrongServerPW": + case 4: + message.type = 4; + break; + case "UsernameInUse": + case 5: + message.type = 5; + break; + case "ServerFull": + case 6: + message.type = 6; + break; + case "NoCertificate": + case 7: + message.type = 7; + break; + case "AuthenticatorFail": + case 8: + message.type = 8; + break; + case "NoNewConnections": + case 9: + message.type = 9; + break; + default: + } + if (object.reason != null) + message.reason = $String(object.reason); + return message; + }; + + Reject.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.type = options.enums === $String ? "None" : 0; + object.reason = ""; + } + if (message.type != null && $Object.hasOwnProperty.call(message, "type")) + object.type = options.enums === $String ? $root.MumbleProto.Reject.RejectType[message.type] === $undefined ? message.type : $root.MumbleProto.Reject.RejectType[message.type] : message.type; + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + object.reason = message.reason; + return object; + }; + + Reject.prototype.toJSON = function() { + return Reject.toObject(this, $protobuf.util.toJSONOptions); + }; + + Reject.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.Reject"; + }; + + Reject.RejectType = (function() { + const valuesById = $Object.create(null), values = $Object.create(valuesById); + values[valuesById[0] = "None"] = 0; + values[valuesById[1] = "WrongVersion"] = 1; + values[valuesById[2] = "InvalidUsername"] = 2; + values[valuesById[3] = "WrongUserPW"] = 3; + values[valuesById[4] = "WrongServerPW"] = 4; + values[valuesById[5] = "UsernameInUse"] = 5; + values[valuesById[6] = "ServerFull"] = 6; + values[valuesById[7] = "NoCertificate"] = 7; + values[valuesById[8] = "AuthenticatorFail"] = 8; + values[valuesById[9] = "NoNewConnections"] = 9; + return values; + })(); + + return Reject; + })(); + + MumbleProto.ServerSync = (function() { + + const ServerSync = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ServerSync.prototype.session = 0; + ServerSync.prototype.max_bandwidth = 0; + ServerSync.prototype.welcome_text = ""; + ServerSync.prototype.permissions = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + ServerSync.create = function(properties) { + return new ServerSync(properties); + }; + + ServerSync.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + writer.uint32(8).uint32(message.session); + if (message.max_bandwidth != null && $Object.hasOwnProperty.call(message, "max_bandwidth")) + writer.uint32(16).uint32(message.max_bandwidth); + if (message.welcome_text != null && $Object.hasOwnProperty.call(message, "welcome_text")) + writer.uint32(26).string(message.welcome_text); + if (message.permissions != null && $Object.hasOwnProperty.call(message, "permissions")) + writer.uint32(32).uint64(message.permissions); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ServerSync.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ServerSync(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.session = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.max_bandwidth = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.welcome_text = reader.string(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.permissions = reader.uint64(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + ServerSync.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + if (!$util.isInteger(message.session)) + return "session: integer expected"; + if (message.max_bandwidth != null && $Object.hasOwnProperty.call(message, "max_bandwidth")) + if (!$util.isInteger(message.max_bandwidth)) + return "max_bandwidth: integer expected"; + if (message.welcome_text != null && $Object.hasOwnProperty.call(message, "welcome_text")) + if (!$util.isString(message.welcome_text)) + return "welcome_text: string expected"; + if (message.permissions != null && $Object.hasOwnProperty.call(message, "permissions")) + if (!$util.isInteger(message.permissions) && !(message.permissions && $util.isInteger(message.permissions.low) && $util.isInteger(message.permissions.high))) + return "permissions: integer|Long expected"; + return null; + }; + + ServerSync.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ServerSync) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ServerSync: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ServerSync(); + if (object.session != null) + message.session = object.session >>> 0; + if (object.max_bandwidth != null) + message.max_bandwidth = object.max_bandwidth >>> 0; + if (object.welcome_text != null) + message.welcome_text = $String(object.welcome_text); + if (object.permissions != null) + if ($util.Long) + message.permissions = $util.Long.fromValue(object.permissions, true); + else if (typeof object.permissions === "string") + message.permissions = $parseInt(object.permissions, 10); + else if (typeof object.permissions === "number") + message.permissions = object.permissions; + else if (typeof object.permissions === "object") + message.permissions = new $util.LongBits(object.permissions.low >>> 0, object.permissions.high >>> 0).toNumber(true); + return message; + }; + + ServerSync.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.session = 0; + object.max_bandwidth = 0; + object.welcome_text = ""; + if ($util.Long) { + let long = new $util.Long(0, 0, true); + object.permissions = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long; + } else + object.permissions = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0; + } + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + object.session = message.session; + if (message.max_bandwidth != null && $Object.hasOwnProperty.call(message, "max_bandwidth")) + object.max_bandwidth = message.max_bandwidth; + if (message.welcome_text != null && $Object.hasOwnProperty.call(message, "welcome_text")) + object.welcome_text = message.welcome_text; + if (message.permissions != null && $Object.hasOwnProperty.call(message, "permissions")) + if (typeof $BigInt !== "undefined" && options.longs === $BigInt) + object.permissions = typeof message.permissions === "number" ? $BigInt(message.permissions) : $util.Long.fromBits(message.permissions.low >>> 0, message.permissions.high >>> 0, true).toBigInt(); + else if (typeof message.permissions === "number") + object.permissions = options.longs === $String ? $String(message.permissions) : message.permissions; + else + object.permissions = options.longs === $String ? $util.Long.prototype.toString.call(message.permissions) : options.longs === $Number ? new $util.LongBits(message.permissions.low >>> 0, message.permissions.high >>> 0).toNumber(true) : message.permissions; + return object; + }; + + ServerSync.prototype.toJSON = function() { + return ServerSync.toObject(this, $protobuf.util.toJSONOptions); + }; + + ServerSync.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ServerSync"; + }; + + return ServerSync; + })(); + + MumbleProto.ChannelRemove = (function() { + + const ChannelRemove = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ChannelRemove.prototype.channel_id = 0; + + ChannelRemove.create = function(properties) { + return new ChannelRemove(properties); + }; + + ChannelRemove.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(8).uint32(message.channel_id); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ChannelRemove.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ChannelRemove(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.channel_id = reader.uint32(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "channel_id")) + throw $util.ProtocolError("missing required 'channel_id'", { instance: message }); + return message; + }; + + ChannelRemove.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!$util.isInteger(message.channel_id)) + return "channel_id: integer expected"; + return null; + }; + + ChannelRemove.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ChannelRemove) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ChannelRemove: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ChannelRemove(); + if (object.channel_id != null) + message.channel_id = object.channel_id >>> 0; + return message; + }; + + ChannelRemove.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) + object.channel_id = 0; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + object.channel_id = message.channel_id; + return object; + }; + + ChannelRemove.prototype.toJSON = function() { + return ChannelRemove.toObject(this, $protobuf.util.toJSONOptions); + }; + + ChannelRemove.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ChannelRemove"; + }; + + return ChannelRemove; + })(); + + MumbleProto.ChannelState = (function() { + + const ChannelState = function (properties) { + this.links = []; + this.links_add = []; + this.links_remove = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ChannelState.prototype.channel_id = 0; + ChannelState.prototype.parent = 0; + ChannelState.prototype.name = ""; + ChannelState.prototype.links = $util.emptyArray; + ChannelState.prototype.description = ""; + ChannelState.prototype.links_add = $util.emptyArray; + ChannelState.prototype.links_remove = $util.emptyArray; + ChannelState.prototype.temporary = false; + ChannelState.prototype.position = 0; + ChannelState.prototype.description_hash = $util.newBuffer([]); + ChannelState.prototype.max_users = 0; + ChannelState.prototype.is_enter_restricted = false; + ChannelState.prototype.can_enter = false; + + ChannelState.create = function(properties) { + return new ChannelState(properties); + }; + + ChannelState.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + writer.uint32(8).uint32(message.channel_id); + if (message.parent != null && $Object.hasOwnProperty.call(message, "parent")) + writer.uint32(16).uint32(message.parent); + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + writer.uint32(26).string(message.name); + if (message.links != null && message.links.length) + for (let i = 0; i < message.links.length; ++i) + writer.uint32(32).uint32(message.links[i]); + if (message.description != null && $Object.hasOwnProperty.call(message, "description")) + writer.uint32(42).string(message.description); + if (message.links_add != null && message.links_add.length) + for (let i = 0; i < message.links_add.length; ++i) + writer.uint32(48).uint32(message.links_add[i]); + if (message.links_remove != null && message.links_remove.length) + for (let i = 0; i < message.links_remove.length; ++i) + writer.uint32(56).uint32(message.links_remove[i]); + if (message.temporary != null && $Object.hasOwnProperty.call(message, "temporary")) + writer.uint32(64).bool(message.temporary); + if (message.position != null && $Object.hasOwnProperty.call(message, "position")) + writer.uint32(72).int32(message.position); + if (message.description_hash != null && $Object.hasOwnProperty.call(message, "description_hash")) + writer.uint32(82).bytes(message.description_hash); + if (message.max_users != null && $Object.hasOwnProperty.call(message, "max_users")) + writer.uint32(88).uint32(message.max_users); + if (message.is_enter_restricted != null && $Object.hasOwnProperty.call(message, "is_enter_restricted")) + writer.uint32(96).bool(message.is_enter_restricted); + if (message.can_enter != null && $Object.hasOwnProperty.call(message, "can_enter")) + writer.uint32(104).bool(message.can_enter); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ChannelState.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ChannelState(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.channel_id = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.parent = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.name = reader.string(); + continue; + } + case 4: { + if (wireType === 2) { + if (!(message.links && message.links.length)) + message.links = []; + reader.uint32s(message.links); + continue; + } + if (wireType !== 0) + break; + if (!(message.links && message.links.length)) + message.links = []; + message.links.push(reader.uint32()); + continue; + } + case 5: { + if (wireType !== 2) + break; + message.description = reader.string(); + continue; + } + case 6: { + if (wireType === 2) { + if (!(message.links_add && message.links_add.length)) + message.links_add = []; + reader.uint32s(message.links_add); + continue; + } + if (wireType !== 0) + break; + if (!(message.links_add && message.links_add.length)) + message.links_add = []; + message.links_add.push(reader.uint32()); + continue; + } + case 7: { + if (wireType === 2) { + if (!(message.links_remove && message.links_remove.length)) + message.links_remove = []; + reader.uint32s(message.links_remove); + continue; + } + if (wireType !== 0) + break; + if (!(message.links_remove && message.links_remove.length)) + message.links_remove = []; + message.links_remove.push(reader.uint32()); + continue; + } + case 8: { + if (wireType !== 0) + break; + message.temporary = reader.bool(); + continue; + } + case 9: { + if (wireType !== 0) + break; + message.position = reader.int32(); + continue; + } + case 10: { + if (wireType !== 2) + break; + message.description_hash = reader.bytes(); + continue; + } + case 11: { + if (wireType !== 0) + break; + message.max_users = reader.uint32(); + continue; + } + case 12: { + if (wireType !== 0) + break; + message.is_enter_restricted = reader.bool(); + continue; + } + case 13: { + if (wireType !== 0) + break; + message.can_enter = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + ChannelState.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + if (!$util.isInteger(message.channel_id)) + return "channel_id: integer expected"; + if (message.parent != null && $Object.hasOwnProperty.call(message, "parent")) + if (!$util.isInteger(message.parent)) + return "parent: integer expected"; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + if (!$util.isString(message.name)) + return "name: string expected"; + if (message.links != null && $Object.hasOwnProperty.call(message, "links")) { + if (!$Array.isArray(message.links)) + return "links: array expected"; + for (let i = 0; i < message.links.length; ++i) + if (!$util.isInteger(message.links[i])) + return "links: integer[] expected"; + } + if (message.description != null && $Object.hasOwnProperty.call(message, "description")) + if (!$util.isString(message.description)) + return "description: string expected"; + if (message.links_add != null && $Object.hasOwnProperty.call(message, "links_add")) { + if (!$Array.isArray(message.links_add)) + return "links_add: array expected"; + for (let i = 0; i < message.links_add.length; ++i) + if (!$util.isInteger(message.links_add[i])) + return "links_add: integer[] expected"; + } + if (message.links_remove != null && $Object.hasOwnProperty.call(message, "links_remove")) { + if (!$Array.isArray(message.links_remove)) + return "links_remove: array expected"; + for (let i = 0; i < message.links_remove.length; ++i) + if (!$util.isInteger(message.links_remove[i])) + return "links_remove: integer[] expected"; + } + if (message.temporary != null && $Object.hasOwnProperty.call(message, "temporary")) + if (typeof message.temporary !== "boolean") + return "temporary: boolean expected"; + if (message.position != null && $Object.hasOwnProperty.call(message, "position")) + if (!$util.isInteger(message.position)) + return "position: integer expected"; + if (message.description_hash != null && $Object.hasOwnProperty.call(message, "description_hash")) + if (!(message.description_hash && typeof message.description_hash.length === "number" || $util.isString(message.description_hash))) + return "description_hash: buffer expected"; + if (message.max_users != null && $Object.hasOwnProperty.call(message, "max_users")) + if (!$util.isInteger(message.max_users)) + return "max_users: integer expected"; + if (message.is_enter_restricted != null && $Object.hasOwnProperty.call(message, "is_enter_restricted")) + if (typeof message.is_enter_restricted !== "boolean") + return "is_enter_restricted: boolean expected"; + if (message.can_enter != null && $Object.hasOwnProperty.call(message, "can_enter")) + if (typeof message.can_enter !== "boolean") + return "can_enter: boolean expected"; + return null; + }; + + ChannelState.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ChannelState) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ChannelState: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ChannelState(); + if (object.channel_id != null) + message.channel_id = object.channel_id >>> 0; + if (object.parent != null) + message.parent = object.parent >>> 0; + if (object.name != null) + message.name = $String(object.name); + if (object.links) { + if (!$Array.isArray(object.links)) + throw $TypeError(".MumbleProto.ChannelState.links: array expected"); + message.links = $Array(object.links.length); + for (let i = 0; i < object.links.length; ++i) + message.links[i] = object.links[i] >>> 0; + } + if (object.description != null) + message.description = $String(object.description); + if (object.links_add) { + if (!$Array.isArray(object.links_add)) + throw $TypeError(".MumbleProto.ChannelState.links_add: array expected"); + message.links_add = $Array(object.links_add.length); + for (let i = 0; i < object.links_add.length; ++i) + message.links_add[i] = object.links_add[i] >>> 0; + } + if (object.links_remove) { + if (!$Array.isArray(object.links_remove)) + throw $TypeError(".MumbleProto.ChannelState.links_remove: array expected"); + message.links_remove = $Array(object.links_remove.length); + for (let i = 0; i < object.links_remove.length; ++i) + message.links_remove[i] = object.links_remove[i] >>> 0; + } + if (object.temporary != null) + message.temporary = $Boolean(object.temporary); + if (object.position != null) + message.position = object.position | 0; + if (object.description_hash != null) + if (typeof object.description_hash === "string") + $util.base64.decode(object.description_hash, message.description_hash = $util.newBuffer($util.base64.length(object.description_hash)), 0); + else if (object.description_hash.length >= 0) + message.description_hash = object.description_hash; + if (object.max_users != null) + message.max_users = object.max_users >>> 0; + if (object.is_enter_restricted != null) + message.is_enter_restricted = $Boolean(object.is_enter_restricted); + if (object.can_enter != null) + message.can_enter = $Boolean(object.can_enter); + return message; + }; + + ChannelState.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.links = []; + object.links_add = []; + object.links_remove = []; + } + if (options.defaults) { + object.channel_id = 0; + object.parent = 0; + object.name = ""; + object.description = ""; + object.temporary = false; + object.position = 0; + if (options.bytes === $String) + object.description_hash = ""; + else { + object.description_hash = []; + if (options.bytes !== $Array) + object.description_hash = $util.newBuffer(object.description_hash); + } + object.max_users = 0; + object.is_enter_restricted = false; + object.can_enter = false; + } + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + object.channel_id = message.channel_id; + if (message.parent != null && $Object.hasOwnProperty.call(message, "parent")) + object.parent = message.parent; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + object.name = message.name; + if (message.links && message.links.length) { + object.links = $Array(message.links.length); + for (let j = 0; j < message.links.length; ++j) + object.links[j] = message.links[j]; + } + if (message.description != null && $Object.hasOwnProperty.call(message, "description")) + object.description = message.description; + if (message.links_add && message.links_add.length) { + object.links_add = $Array(message.links_add.length); + for (let j = 0; j < message.links_add.length; ++j) + object.links_add[j] = message.links_add[j]; + } + if (message.links_remove && message.links_remove.length) { + object.links_remove = $Array(message.links_remove.length); + for (let j = 0; j < message.links_remove.length; ++j) + object.links_remove[j] = message.links_remove[j]; + } + if (message.temporary != null && $Object.hasOwnProperty.call(message, "temporary")) + object.temporary = message.temporary; + if (message.position != null && $Object.hasOwnProperty.call(message, "position")) + object.position = message.position; + if (message.description_hash != null && $Object.hasOwnProperty.call(message, "description_hash")) + object.description_hash = options.bytes === $String ? $util.base64.encode(message.description_hash, 0, message.description_hash.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.description_hash) : message.description_hash; + if (message.max_users != null && $Object.hasOwnProperty.call(message, "max_users")) + object.max_users = message.max_users; + if (message.is_enter_restricted != null && $Object.hasOwnProperty.call(message, "is_enter_restricted")) + object.is_enter_restricted = message.is_enter_restricted; + if (message.can_enter != null && $Object.hasOwnProperty.call(message, "can_enter")) + object.can_enter = message.can_enter; + return object; + }; + + ChannelState.prototype.toJSON = function() { + return ChannelState.toObject(this, $protobuf.util.toJSONOptions); + }; + + ChannelState.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ChannelState"; + }; + + return ChannelState; + })(); + + MumbleProto.UserRemove = (function() { + + const UserRemove = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + UserRemove.prototype.session = 0; + UserRemove.prototype.actor = 0; + UserRemove.prototype.reason = ""; + UserRemove.prototype.ban = false; + UserRemove.prototype.ban_certificate = false; + UserRemove.prototype.ban_ip = false; + + UserRemove.create = function(properties) { + return new UserRemove(properties); + }; + + UserRemove.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(8).uint32(message.session); + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + writer.uint32(16).uint32(message.actor); + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + writer.uint32(26).string(message.reason); + if (message.ban != null && $Object.hasOwnProperty.call(message, "ban")) + writer.uint32(32).bool(message.ban); + if (message.ban_certificate != null && $Object.hasOwnProperty.call(message, "ban_certificate")) + writer.uint32(40).bool(message.ban_certificate); + if (message.ban_ip != null && $Object.hasOwnProperty.call(message, "ban_ip")) + writer.uint32(48).bool(message.ban_ip); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + UserRemove.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UserRemove(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.session = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.actor = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.reason = reader.string(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.ban = reader.bool(); + continue; + } + case 5: { + if (wireType !== 0) + break; + message.ban_certificate = reader.bool(); + continue; + } + case 6: { + if (wireType !== 0) + break; + message.ban_ip = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "session")) + throw $util.ProtocolError("missing required 'session'", { instance: message }); + return message; + }; + + UserRemove.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!$util.isInteger(message.session)) + return "session: integer expected"; + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + if (!$util.isInteger(message.actor)) + return "actor: integer expected"; + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + if (!$util.isString(message.reason)) + return "reason: string expected"; + if (message.ban != null && $Object.hasOwnProperty.call(message, "ban")) + if (typeof message.ban !== "boolean") + return "ban: boolean expected"; + if (message.ban_certificate != null && $Object.hasOwnProperty.call(message, "ban_certificate")) + if (typeof message.ban_certificate !== "boolean") + return "ban_certificate: boolean expected"; + if (message.ban_ip != null && $Object.hasOwnProperty.call(message, "ban_ip")) + if (typeof message.ban_ip !== "boolean") + return "ban_ip: boolean expected"; + return null; + }; + + UserRemove.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UserRemove) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UserRemove: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UserRemove(); + if (object.session != null) + message.session = object.session >>> 0; + if (object.actor != null) + message.actor = object.actor >>> 0; + if (object.reason != null) + message.reason = $String(object.reason); + if (object.ban != null) + message.ban = $Boolean(object.ban); + if (object.ban_certificate != null) + message.ban_certificate = $Boolean(object.ban_certificate); + if (object.ban_ip != null) + message.ban_ip = $Boolean(object.ban_ip); + return message; + }; + + UserRemove.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.session = 0; + object.actor = 0; + object.reason = ""; + object.ban = false; + object.ban_certificate = false; + object.ban_ip = false; + } + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + object.session = message.session; + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + object.actor = message.actor; + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + object.reason = message.reason; + if (message.ban != null && $Object.hasOwnProperty.call(message, "ban")) + object.ban = message.ban; + if (message.ban_certificate != null && $Object.hasOwnProperty.call(message, "ban_certificate")) + object.ban_certificate = message.ban_certificate; + if (message.ban_ip != null && $Object.hasOwnProperty.call(message, "ban_ip")) + object.ban_ip = message.ban_ip; + return object; + }; + + UserRemove.prototype.toJSON = function() { + return UserRemove.toObject(this, $protobuf.util.toJSONOptions); + }; + + UserRemove.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UserRemove"; + }; + + return UserRemove; + })(); + + MumbleProto.UserState = (function() { + + const UserState = function (properties) { + this.temporary_access_tokens = []; + this.listening_channel_add = []; + this.listening_channel_remove = []; + this.listening_volume_adjustment = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + UserState.prototype.session = 0; + UserState.prototype.actor = 0; + UserState.prototype.name = ""; + UserState.prototype.user_id = 0; + UserState.prototype.channel_id = 0; + UserState.prototype.mute = false; + UserState.prototype.deaf = false; + UserState.prototype.suppress = false; + UserState.prototype.self_mute = false; + UserState.prototype.self_deaf = false; + UserState.prototype.texture = $util.newBuffer([]); + UserState.prototype.plugin_context = $util.newBuffer([]); + UserState.prototype.plugin_identity = ""; + UserState.prototype.comment = ""; + UserState.prototype.hash = ""; + UserState.prototype.comment_hash = $util.newBuffer([]); + UserState.prototype.texture_hash = $util.newBuffer([]); + UserState.prototype.priority_speaker = false; + UserState.prototype.recording = false; + UserState.prototype.temporary_access_tokens = $util.emptyArray; + UserState.prototype.listening_channel_add = $util.emptyArray; + UserState.prototype.listening_channel_remove = $util.emptyArray; + UserState.prototype.listening_volume_adjustment = $util.emptyArray; + + UserState.create = function(properties) { + return new UserState(properties); + }; + + UserState.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + writer.uint32(8).uint32(message.session); + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + writer.uint32(16).uint32(message.actor); + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + writer.uint32(26).string(message.name); + if (message.user_id != null && $Object.hasOwnProperty.call(message, "user_id")) + writer.uint32(32).uint32(message.user_id); + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + writer.uint32(40).uint32(message.channel_id); + if (message.mute != null && $Object.hasOwnProperty.call(message, "mute")) + writer.uint32(48).bool(message.mute); + if (message.deaf != null && $Object.hasOwnProperty.call(message, "deaf")) + writer.uint32(56).bool(message.deaf); + if (message.suppress != null && $Object.hasOwnProperty.call(message, "suppress")) + writer.uint32(64).bool(message.suppress); + if (message.self_mute != null && $Object.hasOwnProperty.call(message, "self_mute")) + writer.uint32(72).bool(message.self_mute); + if (message.self_deaf != null && $Object.hasOwnProperty.call(message, "self_deaf")) + writer.uint32(80).bool(message.self_deaf); + if (message.texture != null && $Object.hasOwnProperty.call(message, "texture")) + writer.uint32(90).bytes(message.texture); + if (message.plugin_context != null && $Object.hasOwnProperty.call(message, "plugin_context")) + writer.uint32(98).bytes(message.plugin_context); + if (message.plugin_identity != null && $Object.hasOwnProperty.call(message, "plugin_identity")) + writer.uint32(106).string(message.plugin_identity); + if (message.comment != null && $Object.hasOwnProperty.call(message, "comment")) + writer.uint32(114).string(message.comment); + if (message.hash != null && $Object.hasOwnProperty.call(message, "hash")) + writer.uint32(122).string(message.hash); + if (message.comment_hash != null && $Object.hasOwnProperty.call(message, "comment_hash")) + writer.uint32(130).bytes(message.comment_hash); + if (message.texture_hash != null && $Object.hasOwnProperty.call(message, "texture_hash")) + writer.uint32(138).bytes(message.texture_hash); + if (message.priority_speaker != null && $Object.hasOwnProperty.call(message, "priority_speaker")) + writer.uint32(144).bool(message.priority_speaker); + if (message.recording != null && $Object.hasOwnProperty.call(message, "recording")) + writer.uint32(152).bool(message.recording); + if (message.temporary_access_tokens != null && message.temporary_access_tokens.length) + for (let i = 0; i < message.temporary_access_tokens.length; ++i) + writer.uint32(162).string(message.temporary_access_tokens[i]); + if (message.listening_channel_add != null && message.listening_channel_add.length) + for (let i = 0; i < message.listening_channel_add.length; ++i) + writer.uint32(168).uint32(message.listening_channel_add[i]); + if (message.listening_channel_remove != null && message.listening_channel_remove.length) + for (let i = 0; i < message.listening_channel_remove.length; ++i) + writer.uint32(176).uint32(message.listening_channel_remove[i]); + if (message.listening_volume_adjustment != null && message.listening_volume_adjustment.length) + for (let i = 0; i < message.listening_volume_adjustment.length; ++i) + $root.MumbleProto.UserState.VolumeAdjustment.encode(message.listening_volume_adjustment[i], writer.uint32(186).fork(), _depth + 1).ldelim(); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + UserState.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UserState(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.session = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.actor = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.name = reader.string(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.user_id = reader.uint32(); + continue; + } + case 5: { + if (wireType !== 0) + break; + message.channel_id = reader.uint32(); + continue; + } + case 6: { + if (wireType !== 0) + break; + message.mute = reader.bool(); + continue; + } + case 7: { + if (wireType !== 0) + break; + message.deaf = reader.bool(); + continue; + } + case 8: { + if (wireType !== 0) + break; + message.suppress = reader.bool(); + continue; + } + case 9: { + if (wireType !== 0) + break; + message.self_mute = reader.bool(); + continue; + } + case 10: { + if (wireType !== 0) + break; + message.self_deaf = reader.bool(); + continue; + } + case 11: { + if (wireType !== 2) + break; + message.texture = reader.bytes(); + continue; + } + case 12: { + if (wireType !== 2) + break; + message.plugin_context = reader.bytes(); + continue; + } + case 13: { + if (wireType !== 2) + break; + message.plugin_identity = reader.string(); + continue; + } + case 14: { + if (wireType !== 2) + break; + message.comment = reader.string(); + continue; + } + case 15: { + if (wireType !== 2) + break; + message.hash = reader.string(); + continue; + } + case 16: { + if (wireType !== 2) + break; + message.comment_hash = reader.bytes(); + continue; + } + case 17: { + if (wireType !== 2) + break; + message.texture_hash = reader.bytes(); + continue; + } + case 18: { + if (wireType !== 0) + break; + message.priority_speaker = reader.bool(); + continue; + } + case 19: { + if (wireType !== 0) + break; + message.recording = reader.bool(); + continue; + } + case 20: { + if (wireType !== 2) + break; + if (!(message.temporary_access_tokens && message.temporary_access_tokens.length)) + message.temporary_access_tokens = []; + message.temporary_access_tokens.push(reader.string()); + continue; + } + case 21: { + if (wireType === 2) { + if (!(message.listening_channel_add && message.listening_channel_add.length)) + message.listening_channel_add = []; + reader.uint32s(message.listening_channel_add); + continue; + } + if (wireType !== 0) + break; + if (!(message.listening_channel_add && message.listening_channel_add.length)) + message.listening_channel_add = []; + message.listening_channel_add.push(reader.uint32()); + continue; + } + case 22: { + if (wireType === 2) { + if (!(message.listening_channel_remove && message.listening_channel_remove.length)) + message.listening_channel_remove = []; + reader.uint32s(message.listening_channel_remove); + continue; + } + if (wireType !== 0) + break; + if (!(message.listening_channel_remove && message.listening_channel_remove.length)) + message.listening_channel_remove = []; + message.listening_channel_remove.push(reader.uint32()); + continue; + } + case 23: { + if (wireType !== 2) + break; + if (!(message.listening_volume_adjustment && message.listening_volume_adjustment.length)) + message.listening_volume_adjustment = []; + message.listening_volume_adjustment.push($root.MumbleProto.UserState.VolumeAdjustment.decode(reader, reader.uint32(), $undefined, _depth + 1)); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + UserState.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + if (!$util.isInteger(message.session)) + return "session: integer expected"; + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + if (!$util.isInteger(message.actor)) + return "actor: integer expected"; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + if (!$util.isString(message.name)) + return "name: string expected"; + if (message.user_id != null && $Object.hasOwnProperty.call(message, "user_id")) + if (!$util.isInteger(message.user_id)) + return "user_id: integer expected"; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + if (!$util.isInteger(message.channel_id)) + return "channel_id: integer expected"; + if (message.mute != null && $Object.hasOwnProperty.call(message, "mute")) + if (typeof message.mute !== "boolean") + return "mute: boolean expected"; + if (message.deaf != null && $Object.hasOwnProperty.call(message, "deaf")) + if (typeof message.deaf !== "boolean") + return "deaf: boolean expected"; + if (message.suppress != null && $Object.hasOwnProperty.call(message, "suppress")) + if (typeof message.suppress !== "boolean") + return "suppress: boolean expected"; + if (message.self_mute != null && $Object.hasOwnProperty.call(message, "self_mute")) + if (typeof message.self_mute !== "boolean") + return "self_mute: boolean expected"; + if (message.self_deaf != null && $Object.hasOwnProperty.call(message, "self_deaf")) + if (typeof message.self_deaf !== "boolean") + return "self_deaf: boolean expected"; + if (message.texture != null && $Object.hasOwnProperty.call(message, "texture")) + if (!(message.texture && typeof message.texture.length === "number" || $util.isString(message.texture))) + return "texture: buffer expected"; + if (message.plugin_context != null && $Object.hasOwnProperty.call(message, "plugin_context")) + if (!(message.plugin_context && typeof message.plugin_context.length === "number" || $util.isString(message.plugin_context))) + return "plugin_context: buffer expected"; + if (message.plugin_identity != null && $Object.hasOwnProperty.call(message, "plugin_identity")) + if (!$util.isString(message.plugin_identity)) + return "plugin_identity: string expected"; + if (message.comment != null && $Object.hasOwnProperty.call(message, "comment")) + if (!$util.isString(message.comment)) + return "comment: string expected"; + if (message.hash != null && $Object.hasOwnProperty.call(message, "hash")) + if (!$util.isString(message.hash)) + return "hash: string expected"; + if (message.comment_hash != null && $Object.hasOwnProperty.call(message, "comment_hash")) + if (!(message.comment_hash && typeof message.comment_hash.length === "number" || $util.isString(message.comment_hash))) + return "comment_hash: buffer expected"; + if (message.texture_hash != null && $Object.hasOwnProperty.call(message, "texture_hash")) + if (!(message.texture_hash && typeof message.texture_hash.length === "number" || $util.isString(message.texture_hash))) + return "texture_hash: buffer expected"; + if (message.priority_speaker != null && $Object.hasOwnProperty.call(message, "priority_speaker")) + if (typeof message.priority_speaker !== "boolean") + return "priority_speaker: boolean expected"; + if (message.recording != null && $Object.hasOwnProperty.call(message, "recording")) + if (typeof message.recording !== "boolean") + return "recording: boolean expected"; + if (message.temporary_access_tokens != null && $Object.hasOwnProperty.call(message, "temporary_access_tokens")) { + if (!$Array.isArray(message.temporary_access_tokens)) + return "temporary_access_tokens: array expected"; + for (let i = 0; i < message.temporary_access_tokens.length; ++i) + if (!$util.isString(message.temporary_access_tokens[i])) + return "temporary_access_tokens: string[] expected"; + } + if (message.listening_channel_add != null && $Object.hasOwnProperty.call(message, "listening_channel_add")) { + if (!$Array.isArray(message.listening_channel_add)) + return "listening_channel_add: array expected"; + for (let i = 0; i < message.listening_channel_add.length; ++i) + if (!$util.isInteger(message.listening_channel_add[i])) + return "listening_channel_add: integer[] expected"; + } + if (message.listening_channel_remove != null && $Object.hasOwnProperty.call(message, "listening_channel_remove")) { + if (!$Array.isArray(message.listening_channel_remove)) + return "listening_channel_remove: array expected"; + for (let i = 0; i < message.listening_channel_remove.length; ++i) + if (!$util.isInteger(message.listening_channel_remove[i])) + return "listening_channel_remove: integer[] expected"; + } + if (message.listening_volume_adjustment != null && $Object.hasOwnProperty.call(message, "listening_volume_adjustment")) { + if (!$Array.isArray(message.listening_volume_adjustment)) + return "listening_volume_adjustment: array expected"; + for (let i = 0; i < message.listening_volume_adjustment.length; ++i) { + let error = $root.MumbleProto.UserState.VolumeAdjustment.verify(message.listening_volume_adjustment[i], _depth + 1); + if (error) + return "listening_volume_adjustment." + error; + } + } + return null; + }; + + UserState.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UserState) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UserState: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UserState(); + if (object.session != null) + message.session = object.session >>> 0; + if (object.actor != null) + message.actor = object.actor >>> 0; + if (object.name != null) + message.name = $String(object.name); + if (object.user_id != null) + message.user_id = object.user_id >>> 0; + if (object.channel_id != null) + message.channel_id = object.channel_id >>> 0; + if (object.mute != null) + message.mute = $Boolean(object.mute); + if (object.deaf != null) + message.deaf = $Boolean(object.deaf); + if (object.suppress != null) + message.suppress = $Boolean(object.suppress); + if (object.self_mute != null) + message.self_mute = $Boolean(object.self_mute); + if (object.self_deaf != null) + message.self_deaf = $Boolean(object.self_deaf); + if (object.texture != null) + if (typeof object.texture === "string") + $util.base64.decode(object.texture, message.texture = $util.newBuffer($util.base64.length(object.texture)), 0); + else if (object.texture.length >= 0) + message.texture = object.texture; + if (object.plugin_context != null) + if (typeof object.plugin_context === "string") + $util.base64.decode(object.plugin_context, message.plugin_context = $util.newBuffer($util.base64.length(object.plugin_context)), 0); + else if (object.plugin_context.length >= 0) + message.plugin_context = object.plugin_context; + if (object.plugin_identity != null) + message.plugin_identity = $String(object.plugin_identity); + if (object.comment != null) + message.comment = $String(object.comment); + if (object.hash != null) + message.hash = $String(object.hash); + if (object.comment_hash != null) + if (typeof object.comment_hash === "string") + $util.base64.decode(object.comment_hash, message.comment_hash = $util.newBuffer($util.base64.length(object.comment_hash)), 0); + else if (object.comment_hash.length >= 0) + message.comment_hash = object.comment_hash; + if (object.texture_hash != null) + if (typeof object.texture_hash === "string") + $util.base64.decode(object.texture_hash, message.texture_hash = $util.newBuffer($util.base64.length(object.texture_hash)), 0); + else if (object.texture_hash.length >= 0) + message.texture_hash = object.texture_hash; + if (object.priority_speaker != null) + message.priority_speaker = $Boolean(object.priority_speaker); + if (object.recording != null) + message.recording = $Boolean(object.recording); + if (object.temporary_access_tokens) { + if (!$Array.isArray(object.temporary_access_tokens)) + throw $TypeError(".MumbleProto.UserState.temporary_access_tokens: array expected"); + message.temporary_access_tokens = $Array(object.temporary_access_tokens.length); + for (let i = 0; i < object.temporary_access_tokens.length; ++i) + message.temporary_access_tokens[i] = $String(object.temporary_access_tokens[i]); + } + if (object.listening_channel_add) { + if (!$Array.isArray(object.listening_channel_add)) + throw $TypeError(".MumbleProto.UserState.listening_channel_add: array expected"); + message.listening_channel_add = $Array(object.listening_channel_add.length); + for (let i = 0; i < object.listening_channel_add.length; ++i) + message.listening_channel_add[i] = object.listening_channel_add[i] >>> 0; + } + if (object.listening_channel_remove) { + if (!$Array.isArray(object.listening_channel_remove)) + throw $TypeError(".MumbleProto.UserState.listening_channel_remove: array expected"); + message.listening_channel_remove = $Array(object.listening_channel_remove.length); + for (let i = 0; i < object.listening_channel_remove.length; ++i) + message.listening_channel_remove[i] = object.listening_channel_remove[i] >>> 0; + } + if (object.listening_volume_adjustment) { + if (!$Array.isArray(object.listening_volume_adjustment)) + throw $TypeError(".MumbleProto.UserState.listening_volume_adjustment: array expected"); + message.listening_volume_adjustment = $Array(object.listening_volume_adjustment.length); + for (let i = 0; i < object.listening_volume_adjustment.length; ++i) { + if (!$util.isObject(object.listening_volume_adjustment[i])) + throw $TypeError(".MumbleProto.UserState.listening_volume_adjustment: object expected"); + message.listening_volume_adjustment[i] = $root.MumbleProto.UserState.VolumeAdjustment.fromObject(object.listening_volume_adjustment[i], _depth + 1); + } + } + return message; + }; + + UserState.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.temporary_access_tokens = []; + object.listening_channel_add = []; + object.listening_channel_remove = []; + object.listening_volume_adjustment = []; + } + if (options.defaults) { + object.session = 0; + object.actor = 0; + object.name = ""; + object.user_id = 0; + object.channel_id = 0; + object.mute = false; + object.deaf = false; + object.suppress = false; + object.self_mute = false; + object.self_deaf = false; + if (options.bytes === $String) + object.texture = ""; + else { + object.texture = []; + if (options.bytes !== $Array) + object.texture = $util.newBuffer(object.texture); + } + if (options.bytes === $String) + object.plugin_context = ""; + else { + object.plugin_context = []; + if (options.bytes !== $Array) + object.plugin_context = $util.newBuffer(object.plugin_context); + } + object.plugin_identity = ""; + object.comment = ""; + object.hash = ""; + if (options.bytes === $String) + object.comment_hash = ""; + else { + object.comment_hash = []; + if (options.bytes !== $Array) + object.comment_hash = $util.newBuffer(object.comment_hash); + } + if (options.bytes === $String) + object.texture_hash = ""; + else { + object.texture_hash = []; + if (options.bytes !== $Array) + object.texture_hash = $util.newBuffer(object.texture_hash); + } + object.priority_speaker = false; + object.recording = false; + } + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + object.session = message.session; + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + object.actor = message.actor; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + object.name = message.name; + if (message.user_id != null && $Object.hasOwnProperty.call(message, "user_id")) + object.user_id = message.user_id; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + object.channel_id = message.channel_id; + if (message.mute != null && $Object.hasOwnProperty.call(message, "mute")) + object.mute = message.mute; + if (message.deaf != null && $Object.hasOwnProperty.call(message, "deaf")) + object.deaf = message.deaf; + if (message.suppress != null && $Object.hasOwnProperty.call(message, "suppress")) + object.suppress = message.suppress; + if (message.self_mute != null && $Object.hasOwnProperty.call(message, "self_mute")) + object.self_mute = message.self_mute; + if (message.self_deaf != null && $Object.hasOwnProperty.call(message, "self_deaf")) + object.self_deaf = message.self_deaf; + if (message.texture != null && $Object.hasOwnProperty.call(message, "texture")) + object.texture = options.bytes === $String ? $util.base64.encode(message.texture, 0, message.texture.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.texture) : message.texture; + if (message.plugin_context != null && $Object.hasOwnProperty.call(message, "plugin_context")) + object.plugin_context = options.bytes === $String ? $util.base64.encode(message.plugin_context, 0, message.plugin_context.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.plugin_context) : message.plugin_context; + if (message.plugin_identity != null && $Object.hasOwnProperty.call(message, "plugin_identity")) + object.plugin_identity = message.plugin_identity; + if (message.comment != null && $Object.hasOwnProperty.call(message, "comment")) + object.comment = message.comment; + if (message.hash != null && $Object.hasOwnProperty.call(message, "hash")) + object.hash = message.hash; + if (message.comment_hash != null && $Object.hasOwnProperty.call(message, "comment_hash")) + object.comment_hash = options.bytes === $String ? $util.base64.encode(message.comment_hash, 0, message.comment_hash.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.comment_hash) : message.comment_hash; + if (message.texture_hash != null && $Object.hasOwnProperty.call(message, "texture_hash")) + object.texture_hash = options.bytes === $String ? $util.base64.encode(message.texture_hash, 0, message.texture_hash.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.texture_hash) : message.texture_hash; + if (message.priority_speaker != null && $Object.hasOwnProperty.call(message, "priority_speaker")) + object.priority_speaker = message.priority_speaker; + if (message.recording != null && $Object.hasOwnProperty.call(message, "recording")) + object.recording = message.recording; + if (message.temporary_access_tokens && message.temporary_access_tokens.length) { + object.temporary_access_tokens = $Array(message.temporary_access_tokens.length); + for (let j = 0; j < message.temporary_access_tokens.length; ++j) + object.temporary_access_tokens[j] = message.temporary_access_tokens[j]; + } + if (message.listening_channel_add && message.listening_channel_add.length) { + object.listening_channel_add = $Array(message.listening_channel_add.length); + for (let j = 0; j < message.listening_channel_add.length; ++j) + object.listening_channel_add[j] = message.listening_channel_add[j]; + } + if (message.listening_channel_remove && message.listening_channel_remove.length) { + object.listening_channel_remove = $Array(message.listening_channel_remove.length); + for (let j = 0; j < message.listening_channel_remove.length; ++j) + object.listening_channel_remove[j] = message.listening_channel_remove[j]; + } + if (message.listening_volume_adjustment && message.listening_volume_adjustment.length) { + object.listening_volume_adjustment = $Array(message.listening_volume_adjustment.length); + for (let j = 0; j < message.listening_volume_adjustment.length; ++j) + object.listening_volume_adjustment[j] = $root.MumbleProto.UserState.VolumeAdjustment.toObject(message.listening_volume_adjustment[j], options, _depth + 1); + } + return object; + }; + + UserState.prototype.toJSON = function() { + return UserState.toObject(this, $protobuf.util.toJSONOptions); + }; + + UserState.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UserState"; + }; + + UserState.VolumeAdjustment = (function() { + + const VolumeAdjustment = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + VolumeAdjustment.prototype.listening_channel = 0; + VolumeAdjustment.prototype.volume_adjustment = 0; + + VolumeAdjustment.create = function(properties) { + return new VolumeAdjustment(properties); + }; + + VolumeAdjustment.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.listening_channel != null && $Object.hasOwnProperty.call(message, "listening_channel")) + writer.uint32(8).uint32(message.listening_channel); + if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment")) + writer.uint32(21).float(message.volume_adjustment); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + VolumeAdjustment.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UserState.VolumeAdjustment(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.listening_channel = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 5) + break; + message.volume_adjustment = reader.float(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + VolumeAdjustment.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.listening_channel != null && $Object.hasOwnProperty.call(message, "listening_channel")) + if (!$util.isInteger(message.listening_channel)) + return "listening_channel: integer expected"; + if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment")) + if (typeof message.volume_adjustment !== "number") + return "volume_adjustment: number expected"; + return null; + }; + + VolumeAdjustment.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UserState.VolumeAdjustment) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UserState.VolumeAdjustment: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UserState.VolumeAdjustment(); + if (object.listening_channel != null) + message.listening_channel = object.listening_channel >>> 0; + if (object.volume_adjustment != null) + message.volume_adjustment = $Number(object.volume_adjustment); + return message; + }; + + VolumeAdjustment.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.listening_channel = 0; + object.volume_adjustment = 0; + } + if (message.listening_channel != null && $Object.hasOwnProperty.call(message, "listening_channel")) + object.listening_channel = message.listening_channel; + if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment")) + object.volume_adjustment = options.json && !$isFinite(message.volume_adjustment) ? $String(message.volume_adjustment) : message.volume_adjustment; + return object; + }; + + VolumeAdjustment.prototype.toJSON = function() { + return VolumeAdjustment.toObject(this, $protobuf.util.toJSONOptions); + }; + + VolumeAdjustment.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UserState.VolumeAdjustment"; + }; + + return VolumeAdjustment; + })(); + + return UserState; + })(); + + MumbleProto.BanList = (function() { + + const BanList = function (properties) { + this.bans = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + BanList.prototype.bans = $util.emptyArray; + BanList.prototype.query = false; + + BanList.create = function(properties) { + return new BanList(properties); + }; + + BanList.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.bans != null && message.bans.length) + for (let i = 0; i < message.bans.length; ++i) + $root.MumbleProto.BanList.BanEntry.encode(message.bans[i], writer.uint32(10).fork(), _depth + 1).ldelim(); + if (message.query != null && $Object.hasOwnProperty.call(message, "query")) + writer.uint32(16).bool(message.query); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + BanList.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.BanList(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 2) + break; + if (!(message.bans && message.bans.length)) + message.bans = []; + message.bans.push($root.MumbleProto.BanList.BanEntry.decode(reader, reader.uint32(), $undefined, _depth + 1)); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.query = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + BanList.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.bans != null && $Object.hasOwnProperty.call(message, "bans")) { + if (!$Array.isArray(message.bans)) + return "bans: array expected"; + for (let i = 0; i < message.bans.length; ++i) { + let error = $root.MumbleProto.BanList.BanEntry.verify(message.bans[i], _depth + 1); + if (error) + return "bans." + error; + } + } + if (message.query != null && $Object.hasOwnProperty.call(message, "query")) + if (typeof message.query !== "boolean") + return "query: boolean expected"; + return null; + }; + + BanList.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.BanList) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.BanList: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.BanList(); + if (object.bans) { + if (!$Array.isArray(object.bans)) + throw $TypeError(".MumbleProto.BanList.bans: array expected"); + message.bans = $Array(object.bans.length); + for (let i = 0; i < object.bans.length; ++i) { + if (!$util.isObject(object.bans[i])) + throw $TypeError(".MumbleProto.BanList.bans: object expected"); + message.bans[i] = $root.MumbleProto.BanList.BanEntry.fromObject(object.bans[i], _depth + 1); + } + } + if (object.query != null) + message.query = $Boolean(object.query); + return message; + }; + + BanList.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.bans = []; + if (options.defaults) + object.query = false; + if (message.bans && message.bans.length) { + object.bans = $Array(message.bans.length); + for (let j = 0; j < message.bans.length; ++j) + object.bans[j] = $root.MumbleProto.BanList.BanEntry.toObject(message.bans[j], options, _depth + 1); + } + if (message.query != null && $Object.hasOwnProperty.call(message, "query")) + object.query = message.query; + return object; + }; + + BanList.prototype.toJSON = function() { + return BanList.toObject(this, $protobuf.util.toJSONOptions); + }; + + BanList.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.BanList"; + }; + + BanList.BanEntry = (function() { + + const BanEntry = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + BanEntry.prototype.address = $util.newBuffer([]); + BanEntry.prototype.mask = 0; + BanEntry.prototype.name = ""; + BanEntry.prototype.hash = ""; + BanEntry.prototype.reason = ""; + BanEntry.prototype.start = ""; + BanEntry.prototype.duration = 0; + + BanEntry.create = function(properties) { + return new BanEntry(properties); + }; + + BanEntry.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(10).bytes(message.address); + writer.uint32(16).uint32(message.mask); + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + writer.uint32(26).string(message.name); + if (message.hash != null && $Object.hasOwnProperty.call(message, "hash")) + writer.uint32(34).string(message.hash); + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + writer.uint32(42).string(message.reason); + if (message.start != null && $Object.hasOwnProperty.call(message, "start")) + writer.uint32(50).string(message.start); + if (message.duration != null && $Object.hasOwnProperty.call(message, "duration")) + writer.uint32(56).uint32(message.duration); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + BanEntry.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.BanList.BanEntry(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 2) + break; + message.address = reader.bytes(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.mask = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.name = reader.string(); + continue; + } + case 4: { + if (wireType !== 2) + break; + message.hash = reader.string(); + continue; + } + case 5: { + if (wireType !== 2) + break; + message.reason = reader.string(); + continue; + } + case 6: { + if (wireType !== 2) + break; + message.start = reader.string(); + continue; + } + case 7: { + if (wireType !== 0) + break; + message.duration = reader.uint32(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "address")) + throw $util.ProtocolError("missing required 'address'", { instance: message }); + if (!$Object.hasOwnProperty.call(message, "mask")) + throw $util.ProtocolError("missing required 'mask'", { instance: message }); + return message; + }; + + BanEntry.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!(message.address && typeof message.address.length === "number" || $util.isString(message.address))) + return "address: buffer expected"; + if (!$util.isInteger(message.mask)) + return "mask: integer expected"; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + if (!$util.isString(message.name)) + return "name: string expected"; + if (message.hash != null && $Object.hasOwnProperty.call(message, "hash")) + if (!$util.isString(message.hash)) + return "hash: string expected"; + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + if (!$util.isString(message.reason)) + return "reason: string expected"; + if (message.start != null && $Object.hasOwnProperty.call(message, "start")) + if (!$util.isString(message.start)) + return "start: string expected"; + if (message.duration != null && $Object.hasOwnProperty.call(message, "duration")) + if (!$util.isInteger(message.duration)) + return "duration: integer expected"; + return null; + }; + + BanEntry.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.BanList.BanEntry) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.BanList.BanEntry: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.BanList.BanEntry(); + if (object.address != null) + if (typeof object.address === "string") + $util.base64.decode(object.address, message.address = $util.newBuffer($util.base64.length(object.address)), 0); + else if (object.address.length >= 0) + message.address = object.address; + if (object.mask != null) + message.mask = object.mask >>> 0; + if (object.name != null) + message.name = $String(object.name); + if (object.hash != null) + message.hash = $String(object.hash); + if (object.reason != null) + message.reason = $String(object.reason); + if (object.start != null) + message.start = $String(object.start); + if (object.duration != null) + message.duration = object.duration >>> 0; + return message; + }; + + BanEntry.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + if (options.bytes === $String) + object.address = ""; + else { + object.address = []; + if (options.bytes !== $Array) + object.address = $util.newBuffer(object.address); + } + object.mask = 0; + object.name = ""; + object.hash = ""; + object.reason = ""; + object.start = ""; + object.duration = 0; + } + if (message.address != null && $Object.hasOwnProperty.call(message, "address")) + object.address = options.bytes === $String ? $util.base64.encode(message.address, 0, message.address.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.address) : message.address; + if (message.mask != null && $Object.hasOwnProperty.call(message, "mask")) + object.mask = message.mask; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + object.name = message.name; + if (message.hash != null && $Object.hasOwnProperty.call(message, "hash")) + object.hash = message.hash; + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + object.reason = message.reason; + if (message.start != null && $Object.hasOwnProperty.call(message, "start")) + object.start = message.start; + if (message.duration != null && $Object.hasOwnProperty.call(message, "duration")) + object.duration = message.duration; + return object; + }; + + BanEntry.prototype.toJSON = function() { + return BanEntry.toObject(this, $protobuf.util.toJSONOptions); + }; + + BanEntry.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.BanList.BanEntry"; + }; + + return BanEntry; + })(); + + return BanList; + })(); + + MumbleProto.TextMessage = (function() { + + const TextMessage = function (properties) { + this.session = []; + this.channel_id = []; + this.tree_id = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + TextMessage.prototype.actor = 0; + TextMessage.prototype.session = $util.emptyArray; + TextMessage.prototype.channel_id = $util.emptyArray; + TextMessage.prototype.tree_id = $util.emptyArray; + TextMessage.prototype.message = ""; + + TextMessage.create = function(properties) { + return new TextMessage(properties); + }; + + TextMessage.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + writer.uint32(8).uint32(message.actor); + if (message.session != null && message.session.length) + for (let i = 0; i < message.session.length; ++i) + writer.uint32(16).uint32(message.session[i]); + if (message.channel_id != null && message.channel_id.length) + for (let i = 0; i < message.channel_id.length; ++i) + writer.uint32(24).uint32(message.channel_id[i]); + if (message.tree_id != null && message.tree_id.length) + for (let i = 0; i < message.tree_id.length; ++i) + writer.uint32(32).uint32(message.tree_id[i]); + writer.uint32(42).string(message.message); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + TextMessage.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.TextMessage(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.actor = reader.uint32(); + continue; + } + case 2: { + if (wireType === 2) { + if (!(message.session && message.session.length)) + message.session = []; + reader.uint32s(message.session); + continue; + } + if (wireType !== 0) + break; + if (!(message.session && message.session.length)) + message.session = []; + message.session.push(reader.uint32()); + continue; + } + case 3: { + if (wireType === 2) { + if (!(message.channel_id && message.channel_id.length)) + message.channel_id = []; + reader.uint32s(message.channel_id); + continue; + } + if (wireType !== 0) + break; + if (!(message.channel_id && message.channel_id.length)) + message.channel_id = []; + message.channel_id.push(reader.uint32()); + continue; + } + case 4: { + if (wireType === 2) { + if (!(message.tree_id && message.tree_id.length)) + message.tree_id = []; + reader.uint32s(message.tree_id); + continue; + } + if (wireType !== 0) + break; + if (!(message.tree_id && message.tree_id.length)) + message.tree_id = []; + message.tree_id.push(reader.uint32()); + continue; + } + case 5: { + if (wireType !== 2) + break; + message.message = reader.string(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "message")) + throw $util.ProtocolError("missing required 'message'", { instance: message }); + return message; + }; + + TextMessage.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + if (!$util.isInteger(message.actor)) + return "actor: integer expected"; + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) { + if (!$Array.isArray(message.session)) + return "session: array expected"; + for (let i = 0; i < message.session.length; ++i) + if (!$util.isInteger(message.session[i])) + return "session: integer[] expected"; + } + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) { + if (!$Array.isArray(message.channel_id)) + return "channel_id: array expected"; + for (let i = 0; i < message.channel_id.length; ++i) + if (!$util.isInteger(message.channel_id[i])) + return "channel_id: integer[] expected"; + } + if (message.tree_id != null && $Object.hasOwnProperty.call(message, "tree_id")) { + if (!$Array.isArray(message.tree_id)) + return "tree_id: array expected"; + for (let i = 0; i < message.tree_id.length; ++i) + if (!$util.isInteger(message.tree_id[i])) + return "tree_id: integer[] expected"; + } + if (!$util.isString(message.message)) + return "message: string expected"; + return null; + }; + + TextMessage.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.TextMessage) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.TextMessage: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.TextMessage(); + if (object.actor != null) + message.actor = object.actor >>> 0; + if (object.session) { + if (!$Array.isArray(object.session)) + throw $TypeError(".MumbleProto.TextMessage.session: array expected"); + message.session = $Array(object.session.length); + for (let i = 0; i < object.session.length; ++i) + message.session[i] = object.session[i] >>> 0; + } + if (object.channel_id) { + if (!$Array.isArray(object.channel_id)) + throw $TypeError(".MumbleProto.TextMessage.channel_id: array expected"); + message.channel_id = $Array(object.channel_id.length); + for (let i = 0; i < object.channel_id.length; ++i) + message.channel_id[i] = object.channel_id[i] >>> 0; + } + if (object.tree_id) { + if (!$Array.isArray(object.tree_id)) + throw $TypeError(".MumbleProto.TextMessage.tree_id: array expected"); + message.tree_id = $Array(object.tree_id.length); + for (let i = 0; i < object.tree_id.length; ++i) + message.tree_id[i] = object.tree_id[i] >>> 0; + } + if (object.message != null) + message.message = $String(object.message); + return message; + }; + + TextMessage.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.session = []; + object.channel_id = []; + object.tree_id = []; + } + if (options.defaults) { + object.actor = 0; + object.message = ""; + } + if (message.actor != null && $Object.hasOwnProperty.call(message, "actor")) + object.actor = message.actor; + if (message.session && message.session.length) { + object.session = $Array(message.session.length); + for (let j = 0; j < message.session.length; ++j) + object.session[j] = message.session[j]; + } + if (message.channel_id && message.channel_id.length) { + object.channel_id = $Array(message.channel_id.length); + for (let j = 0; j < message.channel_id.length; ++j) + object.channel_id[j] = message.channel_id[j]; + } + if (message.tree_id && message.tree_id.length) { + object.tree_id = $Array(message.tree_id.length); + for (let j = 0; j < message.tree_id.length; ++j) + object.tree_id[j] = message.tree_id[j]; + } + if (message.message != null && $Object.hasOwnProperty.call(message, "message")) + object.message = message.message; + return object; + }; + + TextMessage.prototype.toJSON = function() { + return TextMessage.toObject(this, $protobuf.util.toJSONOptions); + }; + + TextMessage.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.TextMessage"; + }; + + return TextMessage; + })(); + + MumbleProto.PermissionDenied = (function() { + + const PermissionDenied = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + PermissionDenied.prototype.permission = 0; + PermissionDenied.prototype.channel_id = 0; + PermissionDenied.prototype.session = 0; + PermissionDenied.prototype.reason = ""; + PermissionDenied.prototype.type = 0; + PermissionDenied.prototype.name = ""; + + PermissionDenied.create = function(properties) { + return new PermissionDenied(properties); + }; + + PermissionDenied.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.permission != null && $Object.hasOwnProperty.call(message, "permission")) + writer.uint32(8).uint32(message.permission); + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + writer.uint32(16).uint32(message.channel_id); + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + writer.uint32(24).uint32(message.session); + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + writer.uint32(34).string(message.reason); + if (message.type != null && $Object.hasOwnProperty.call(message, "type")) + writer.uint32(40).int32(message.type); + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + writer.uint32(50).string(message.name); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + PermissionDenied.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message, value; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.PermissionDenied(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.permission = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.channel_id = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.session = reader.uint32(); + continue; + } + case 4: { + if (wireType !== 2) + break; + message.reason = reader.string(); + continue; + } + case 5: { + if (wireType !== 0) + break; + value = reader.int32(); + if ($root.MumbleProto.PermissionDenied.DenyType[value] !== $undefined) + message.type = value; + else if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + continue; + } + case 6: { + if (wireType !== 2) + break; + message.name = reader.string(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + PermissionDenied.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.permission != null && $Object.hasOwnProperty.call(message, "permission")) + if (!$util.isInteger(message.permission)) + return "permission: integer expected"; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + if (!$util.isInteger(message.channel_id)) + return "channel_id: integer expected"; + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + if (!$util.isInteger(message.session)) + return "session: integer expected"; + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + if (!$util.isString(message.reason)) + return "reason: string expected"; + if (message.type != null && $Object.hasOwnProperty.call(message, "type")) + switch (message.type) { + default: + return "type: enum value expected"; + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + break; + } + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + if (!$util.isString(message.name)) + return "name: string expected"; + return null; + }; + + PermissionDenied.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.PermissionDenied) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.PermissionDenied: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.PermissionDenied(); + if (object.permission != null) + message.permission = object.permission >>> 0; + if (object.channel_id != null) + message.channel_id = object.channel_id >>> 0; + if (object.session != null) + message.session = object.session >>> 0; + if (object.reason != null) + message.reason = $String(object.reason); + switch (object.type) { + case "Text": + case 0: + message.type = 0; + break; + case "Permission": + case 1: + message.type = 1; + break; + case "SuperUser": + case 2: + message.type = 2; + break; + case "ChannelName": + case 3: + message.type = 3; + break; + case "TextTooLong": + case 4: + message.type = 4; + break; + case "H9K": + case 5: + message.type = 5; + break; + case "TemporaryChannel": + case 6: + message.type = 6; + break; + case "MissingCertificate": + case 7: + message.type = 7; + break; + case "UserName": + case 8: + message.type = 8; + break; + case "ChannelFull": + case 9: + message.type = 9; + break; + case "NestingLimit": + case 10: + message.type = 10; + break; + case "ChannelCountLimit": + case 11: + message.type = 11; + break; + case "ChannelListenerLimit": + case 12: + message.type = 12; + break; + case "UserListenerLimit": + case 13: + message.type = 13; + break; + default: + } + if (object.name != null) + message.name = $String(object.name); + return message; + }; + + PermissionDenied.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.permission = 0; + object.channel_id = 0; + object.session = 0; + object.reason = ""; + object.type = options.enums === $String ? "Text" : 0; + object.name = ""; + } + if (message.permission != null && $Object.hasOwnProperty.call(message, "permission")) + object.permission = message.permission; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + object.channel_id = message.channel_id; + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + object.session = message.session; + if (message.reason != null && $Object.hasOwnProperty.call(message, "reason")) + object.reason = message.reason; + if (message.type != null && $Object.hasOwnProperty.call(message, "type")) + object.type = options.enums === $String ? $root.MumbleProto.PermissionDenied.DenyType[message.type] === $undefined ? message.type : $root.MumbleProto.PermissionDenied.DenyType[message.type] : message.type; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + object.name = message.name; + return object; + }; + + PermissionDenied.prototype.toJSON = function() { + return PermissionDenied.toObject(this, $protobuf.util.toJSONOptions); + }; + + PermissionDenied.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.PermissionDenied"; + }; + + PermissionDenied.DenyType = (function() { + const valuesById = $Object.create(null), values = $Object.create(valuesById); + values[valuesById[0] = "Text"] = 0; + values[valuesById[1] = "Permission"] = 1; + values[valuesById[2] = "SuperUser"] = 2; + values[valuesById[3] = "ChannelName"] = 3; + values[valuesById[4] = "TextTooLong"] = 4; + values[valuesById[5] = "H9K"] = 5; + values[valuesById[6] = "TemporaryChannel"] = 6; + values[valuesById[7] = "MissingCertificate"] = 7; + values[valuesById[8] = "UserName"] = 8; + values[valuesById[9] = "ChannelFull"] = 9; + values[valuesById[10] = "NestingLimit"] = 10; + values[valuesById[11] = "ChannelCountLimit"] = 11; + values[valuesById[12] = "ChannelListenerLimit"] = 12; + values[valuesById[13] = "UserListenerLimit"] = 13; + return values; + })(); + + return PermissionDenied; + })(); + + MumbleProto.ACL = (function() { + + const ACL = function (properties) { + this.groups = []; + this.acls = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ACL.prototype.channel_id = 0; + ACL.prototype.inherit_acls = true; + ACL.prototype.groups = $util.emptyArray; + ACL.prototype.acls = $util.emptyArray; + ACL.prototype.query = false; + + ACL.create = function(properties) { + return new ACL(properties); + }; + + ACL.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(8).uint32(message.channel_id); + if (message.inherit_acls != null && $Object.hasOwnProperty.call(message, "inherit_acls")) + writer.uint32(16).bool(message.inherit_acls); + if (message.groups != null && message.groups.length) + for (let i = 0; i < message.groups.length; ++i) + $root.MumbleProto.ACL.ChanGroup.encode(message.groups[i], writer.uint32(26).fork(), _depth + 1).ldelim(); + if (message.acls != null && message.acls.length) + for (let i = 0; i < message.acls.length; ++i) + $root.MumbleProto.ACL.ChanACL.encode(message.acls[i], writer.uint32(34).fork(), _depth + 1).ldelim(); + if (message.query != null && $Object.hasOwnProperty.call(message, "query")) + writer.uint32(40).bool(message.query); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ACL.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ACL(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.channel_id = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.inherit_acls = reader.bool(); + continue; + } + case 3: { + if (wireType !== 2) + break; + if (!(message.groups && message.groups.length)) + message.groups = []; + message.groups.push($root.MumbleProto.ACL.ChanGroup.decode(reader, reader.uint32(), $undefined, _depth + 1)); + continue; + } + case 4: { + if (wireType !== 2) + break; + if (!(message.acls && message.acls.length)) + message.acls = []; + message.acls.push($root.MumbleProto.ACL.ChanACL.decode(reader, reader.uint32(), $undefined, _depth + 1)); + continue; + } + case 5: { + if (wireType !== 0) + break; + message.query = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "channel_id")) + throw $util.ProtocolError("missing required 'channel_id'", { instance: message }); + return message; + }; + + ACL.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!$util.isInteger(message.channel_id)) + return "channel_id: integer expected"; + if (message.inherit_acls != null && $Object.hasOwnProperty.call(message, "inherit_acls")) + if (typeof message.inherit_acls !== "boolean") + return "inherit_acls: boolean expected"; + if (message.groups != null && $Object.hasOwnProperty.call(message, "groups")) { + if (!$Array.isArray(message.groups)) + return "groups: array expected"; + for (let i = 0; i < message.groups.length; ++i) { + let error = $root.MumbleProto.ACL.ChanGroup.verify(message.groups[i], _depth + 1); + if (error) + return "groups." + error; + } + } + if (message.acls != null && $Object.hasOwnProperty.call(message, "acls")) { + if (!$Array.isArray(message.acls)) + return "acls: array expected"; + for (let i = 0; i < message.acls.length; ++i) { + let error = $root.MumbleProto.ACL.ChanACL.verify(message.acls[i], _depth + 1); + if (error) + return "acls." + error; + } + } + if (message.query != null && $Object.hasOwnProperty.call(message, "query")) + if (typeof message.query !== "boolean") + return "query: boolean expected"; + return null; + }; + + ACL.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ACL) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ACL: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ACL(); + if (object.channel_id != null) + message.channel_id = object.channel_id >>> 0; + if (object.inherit_acls != null) + message.inherit_acls = $Boolean(object.inherit_acls); + if (object.groups) { + if (!$Array.isArray(object.groups)) + throw $TypeError(".MumbleProto.ACL.groups: array expected"); + message.groups = $Array(object.groups.length); + for (let i = 0; i < object.groups.length; ++i) { + if (!$util.isObject(object.groups[i])) + throw $TypeError(".MumbleProto.ACL.groups: object expected"); + message.groups[i] = $root.MumbleProto.ACL.ChanGroup.fromObject(object.groups[i], _depth + 1); + } + } + if (object.acls) { + if (!$Array.isArray(object.acls)) + throw $TypeError(".MumbleProto.ACL.acls: array expected"); + message.acls = $Array(object.acls.length); + for (let i = 0; i < object.acls.length; ++i) { + if (!$util.isObject(object.acls[i])) + throw $TypeError(".MumbleProto.ACL.acls: object expected"); + message.acls[i] = $root.MumbleProto.ACL.ChanACL.fromObject(object.acls[i], _depth + 1); + } + } + if (object.query != null) + message.query = $Boolean(object.query); + return message; + }; + + ACL.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.groups = []; + object.acls = []; + } + if (options.defaults) { + object.channel_id = 0; + object.inherit_acls = true; + object.query = false; + } + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + object.channel_id = message.channel_id; + if (message.inherit_acls != null && $Object.hasOwnProperty.call(message, "inherit_acls")) + object.inherit_acls = message.inherit_acls; + if (message.groups && message.groups.length) { + object.groups = $Array(message.groups.length); + for (let j = 0; j < message.groups.length; ++j) + object.groups[j] = $root.MumbleProto.ACL.ChanGroup.toObject(message.groups[j], options, _depth + 1); + } + if (message.acls && message.acls.length) { + object.acls = $Array(message.acls.length); + for (let j = 0; j < message.acls.length; ++j) + object.acls[j] = $root.MumbleProto.ACL.ChanACL.toObject(message.acls[j], options, _depth + 1); + } + if (message.query != null && $Object.hasOwnProperty.call(message, "query")) + object.query = message.query; + return object; + }; + + ACL.prototype.toJSON = function() { + return ACL.toObject(this, $protobuf.util.toJSONOptions); + }; + + ACL.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ACL"; + }; + + ACL.ChanGroup = (function() { + + const ChanGroup = function (properties) { + this.add = []; + this.remove = []; + this.inherited_members = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ChanGroup.prototype.name = ""; + ChanGroup.prototype.inherited = true; + ChanGroup.prototype.inherit = true; + ChanGroup.prototype.inheritable = true; + ChanGroup.prototype.add = $util.emptyArray; + ChanGroup.prototype.remove = $util.emptyArray; + ChanGroup.prototype.inherited_members = $util.emptyArray; + + ChanGroup.create = function(properties) { + return new ChanGroup(properties); + }; + + ChanGroup.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(10).string(message.name); + if (message.inherited != null && $Object.hasOwnProperty.call(message, "inherited")) + writer.uint32(16).bool(message.inherited); + if (message.inherit != null && $Object.hasOwnProperty.call(message, "inherit")) + writer.uint32(24).bool(message.inherit); + if (message.inheritable != null && $Object.hasOwnProperty.call(message, "inheritable")) + writer.uint32(32).bool(message.inheritable); + if (message.add != null && message.add.length) + for (let i = 0; i < message.add.length; ++i) + writer.uint32(40).uint32(message.add[i]); + if (message.remove != null && message.remove.length) + for (let i = 0; i < message.remove.length; ++i) + writer.uint32(48).uint32(message.remove[i]); + if (message.inherited_members != null && message.inherited_members.length) + for (let i = 0; i < message.inherited_members.length; ++i) + writer.uint32(56).uint32(message.inherited_members[i]); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ChanGroup.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ACL.ChanGroup(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 2) + break; + message.name = reader.string(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.inherited = reader.bool(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.inherit = reader.bool(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.inheritable = reader.bool(); + continue; + } + case 5: { + if (wireType === 2) { + if (!(message.add && message.add.length)) + message.add = []; + reader.uint32s(message.add); + continue; + } + if (wireType !== 0) + break; + if (!(message.add && message.add.length)) + message.add = []; + message.add.push(reader.uint32()); + continue; + } + case 6: { + if (wireType === 2) { + if (!(message.remove && message.remove.length)) + message.remove = []; + reader.uint32s(message.remove); + continue; + } + if (wireType !== 0) + break; + if (!(message.remove && message.remove.length)) + message.remove = []; + message.remove.push(reader.uint32()); + continue; + } + case 7: { + if (wireType === 2) { + if (!(message.inherited_members && message.inherited_members.length)) + message.inherited_members = []; + reader.uint32s(message.inherited_members); + continue; + } + if (wireType !== 0) + break; + if (!(message.inherited_members && message.inherited_members.length)) + message.inherited_members = []; + message.inherited_members.push(reader.uint32()); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "name")) + throw $util.ProtocolError("missing required 'name'", { instance: message }); + return message; + }; + + ChanGroup.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!$util.isString(message.name)) + return "name: string expected"; + if (message.inherited != null && $Object.hasOwnProperty.call(message, "inherited")) + if (typeof message.inherited !== "boolean") + return "inherited: boolean expected"; + if (message.inherit != null && $Object.hasOwnProperty.call(message, "inherit")) + if (typeof message.inherit !== "boolean") + return "inherit: boolean expected"; + if (message.inheritable != null && $Object.hasOwnProperty.call(message, "inheritable")) + if (typeof message.inheritable !== "boolean") + return "inheritable: boolean expected"; + if (message.add != null && $Object.hasOwnProperty.call(message, "add")) { + if (!$Array.isArray(message.add)) + return "add: array expected"; + for (let i = 0; i < message.add.length; ++i) + if (!$util.isInteger(message.add[i])) + return "add: integer[] expected"; + } + if (message.remove != null && $Object.hasOwnProperty.call(message, "remove")) { + if (!$Array.isArray(message.remove)) + return "remove: array expected"; + for (let i = 0; i < message.remove.length; ++i) + if (!$util.isInteger(message.remove[i])) + return "remove: integer[] expected"; + } + if (message.inherited_members != null && $Object.hasOwnProperty.call(message, "inherited_members")) { + if (!$Array.isArray(message.inherited_members)) + return "inherited_members: array expected"; + for (let i = 0; i < message.inherited_members.length; ++i) + if (!$util.isInteger(message.inherited_members[i])) + return "inherited_members: integer[] expected"; + } + return null; + }; + + ChanGroup.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ACL.ChanGroup) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ACL.ChanGroup: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ACL.ChanGroup(); + if (object.name != null) + message.name = $String(object.name); + if (object.inherited != null) + message.inherited = $Boolean(object.inherited); + if (object.inherit != null) + message.inherit = $Boolean(object.inherit); + if (object.inheritable != null) + message.inheritable = $Boolean(object.inheritable); + if (object.add) { + if (!$Array.isArray(object.add)) + throw $TypeError(".MumbleProto.ACL.ChanGroup.add: array expected"); + message.add = $Array(object.add.length); + for (let i = 0; i < object.add.length; ++i) + message.add[i] = object.add[i] >>> 0; + } + if (object.remove) { + if (!$Array.isArray(object.remove)) + throw $TypeError(".MumbleProto.ACL.ChanGroup.remove: array expected"); + message.remove = $Array(object.remove.length); + for (let i = 0; i < object.remove.length; ++i) + message.remove[i] = object.remove[i] >>> 0; + } + if (object.inherited_members) { + if (!$Array.isArray(object.inherited_members)) + throw $TypeError(".MumbleProto.ACL.ChanGroup.inherited_members: array expected"); + message.inherited_members = $Array(object.inherited_members.length); + for (let i = 0; i < object.inherited_members.length; ++i) + message.inherited_members[i] = object.inherited_members[i] >>> 0; + } + return message; + }; + + ChanGroup.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.add = []; + object.remove = []; + object.inherited_members = []; + } + if (options.defaults) { + object.name = ""; + object.inherited = true; + object.inherit = true; + object.inheritable = true; + } + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + object.name = message.name; + if (message.inherited != null && $Object.hasOwnProperty.call(message, "inherited")) + object.inherited = message.inherited; + if (message.inherit != null && $Object.hasOwnProperty.call(message, "inherit")) + object.inherit = message.inherit; + if (message.inheritable != null && $Object.hasOwnProperty.call(message, "inheritable")) + object.inheritable = message.inheritable; + if (message.add && message.add.length) { + object.add = $Array(message.add.length); + for (let j = 0; j < message.add.length; ++j) + object.add[j] = message.add[j]; + } + if (message.remove && message.remove.length) { + object.remove = $Array(message.remove.length); + for (let j = 0; j < message.remove.length; ++j) + object.remove[j] = message.remove[j]; + } + if (message.inherited_members && message.inherited_members.length) { + object.inherited_members = $Array(message.inherited_members.length); + for (let j = 0; j < message.inherited_members.length; ++j) + object.inherited_members[j] = message.inherited_members[j]; + } + return object; + }; + + ChanGroup.prototype.toJSON = function() { + return ChanGroup.toObject(this, $protobuf.util.toJSONOptions); + }; + + ChanGroup.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ACL.ChanGroup"; + }; + + return ChanGroup; + })(); + + ACL.ChanACL = (function() { + + const ChanACL = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ChanACL.prototype.apply_here = true; + ChanACL.prototype.apply_subs = true; + ChanACL.prototype.inherited = true; + ChanACL.prototype.user_id = 0; + ChanACL.prototype.group = ""; + ChanACL.prototype.grant = 0; + ChanACL.prototype.deny = 0; + + ChanACL.create = function(properties) { + return new ChanACL(properties); + }; + + ChanACL.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.apply_here != null && $Object.hasOwnProperty.call(message, "apply_here")) + writer.uint32(8).bool(message.apply_here); + if (message.apply_subs != null && $Object.hasOwnProperty.call(message, "apply_subs")) + writer.uint32(16).bool(message.apply_subs); + if (message.inherited != null && $Object.hasOwnProperty.call(message, "inherited")) + writer.uint32(24).bool(message.inherited); + if (message.user_id != null && $Object.hasOwnProperty.call(message, "user_id")) + writer.uint32(32).uint32(message.user_id); + if (message.group != null && $Object.hasOwnProperty.call(message, "group")) + writer.uint32(42).string(message.group); + if (message.grant != null && $Object.hasOwnProperty.call(message, "grant")) + writer.uint32(48).uint32(message.grant); + if (message.deny != null && $Object.hasOwnProperty.call(message, "deny")) + writer.uint32(56).uint32(message.deny); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ChanACL.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ACL.ChanACL(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.apply_here = reader.bool(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.apply_subs = reader.bool(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.inherited = reader.bool(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.user_id = reader.uint32(); + continue; + } + case 5: { + if (wireType !== 2) + break; + message.group = reader.string(); + continue; + } + case 6: { + if (wireType !== 0) + break; + message.grant = reader.uint32(); + continue; + } + case 7: { + if (wireType !== 0) + break; + message.deny = reader.uint32(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + ChanACL.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.apply_here != null && $Object.hasOwnProperty.call(message, "apply_here")) + if (typeof message.apply_here !== "boolean") + return "apply_here: boolean expected"; + if (message.apply_subs != null && $Object.hasOwnProperty.call(message, "apply_subs")) + if (typeof message.apply_subs !== "boolean") + return "apply_subs: boolean expected"; + if (message.inherited != null && $Object.hasOwnProperty.call(message, "inherited")) + if (typeof message.inherited !== "boolean") + return "inherited: boolean expected"; + if (message.user_id != null && $Object.hasOwnProperty.call(message, "user_id")) + if (!$util.isInteger(message.user_id)) + return "user_id: integer expected"; + if (message.group != null && $Object.hasOwnProperty.call(message, "group")) + if (!$util.isString(message.group)) + return "group: string expected"; + if (message.grant != null && $Object.hasOwnProperty.call(message, "grant")) + if (!$util.isInteger(message.grant)) + return "grant: integer expected"; + if (message.deny != null && $Object.hasOwnProperty.call(message, "deny")) + if (!$util.isInteger(message.deny)) + return "deny: integer expected"; + return null; + }; + + ChanACL.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ACL.ChanACL) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ACL.ChanACL: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ACL.ChanACL(); + if (object.apply_here != null) + message.apply_here = $Boolean(object.apply_here); + if (object.apply_subs != null) + message.apply_subs = $Boolean(object.apply_subs); + if (object.inherited != null) + message.inherited = $Boolean(object.inherited); + if (object.user_id != null) + message.user_id = object.user_id >>> 0; + if (object.group != null) + message.group = $String(object.group); + if (object.grant != null) + message.grant = object.grant >>> 0; + if (object.deny != null) + message.deny = object.deny >>> 0; + return message; + }; + + ChanACL.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.apply_here = true; + object.apply_subs = true; + object.inherited = true; + object.user_id = 0; + object.group = ""; + object.grant = 0; + object.deny = 0; + } + if (message.apply_here != null && $Object.hasOwnProperty.call(message, "apply_here")) + object.apply_here = message.apply_here; + if (message.apply_subs != null && $Object.hasOwnProperty.call(message, "apply_subs")) + object.apply_subs = message.apply_subs; + if (message.inherited != null && $Object.hasOwnProperty.call(message, "inherited")) + object.inherited = message.inherited; + if (message.user_id != null && $Object.hasOwnProperty.call(message, "user_id")) + object.user_id = message.user_id; + if (message.group != null && $Object.hasOwnProperty.call(message, "group")) + object.group = message.group; + if (message.grant != null && $Object.hasOwnProperty.call(message, "grant")) + object.grant = message.grant; + if (message.deny != null && $Object.hasOwnProperty.call(message, "deny")) + object.deny = message.deny; + return object; + }; + + ChanACL.prototype.toJSON = function() { + return ChanACL.toObject(this, $protobuf.util.toJSONOptions); + }; + + ChanACL.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ACL.ChanACL"; + }; + + return ChanACL; + })(); + + return ACL; + })(); + + MumbleProto.QueryUsers = (function() { + + const QueryUsers = function (properties) { + this.ids = []; + this.names = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + QueryUsers.prototype.ids = $util.emptyArray; + QueryUsers.prototype.names = $util.emptyArray; + + QueryUsers.create = function(properties) { + return new QueryUsers(properties); + }; + + QueryUsers.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.ids != null && message.ids.length) + for (let i = 0; i < message.ids.length; ++i) + writer.uint32(8).uint32(message.ids[i]); + if (message.names != null && message.names.length) + for (let i = 0; i < message.names.length; ++i) + writer.uint32(18).string(message.names[i]); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + QueryUsers.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.QueryUsers(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType === 2) { + if (!(message.ids && message.ids.length)) + message.ids = []; + reader.uint32s(message.ids); + continue; + } + if (wireType !== 0) + break; + if (!(message.ids && message.ids.length)) + message.ids = []; + message.ids.push(reader.uint32()); + continue; + } + case 2: { + if (wireType !== 2) + break; + if (!(message.names && message.names.length)) + message.names = []; + message.names.push(reader.string()); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + QueryUsers.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.ids != null && $Object.hasOwnProperty.call(message, "ids")) { + if (!$Array.isArray(message.ids)) + return "ids: array expected"; + for (let i = 0; i < message.ids.length; ++i) + if (!$util.isInteger(message.ids[i])) + return "ids: integer[] expected"; + } + if (message.names != null && $Object.hasOwnProperty.call(message, "names")) { + if (!$Array.isArray(message.names)) + return "names: array expected"; + for (let i = 0; i < message.names.length; ++i) + if (!$util.isString(message.names[i])) + return "names: string[] expected"; + } + return null; + }; + + QueryUsers.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.QueryUsers) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.QueryUsers: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.QueryUsers(); + if (object.ids) { + if (!$Array.isArray(object.ids)) + throw $TypeError(".MumbleProto.QueryUsers.ids: array expected"); + message.ids = $Array(object.ids.length); + for (let i = 0; i < object.ids.length; ++i) + message.ids[i] = object.ids[i] >>> 0; + } + if (object.names) { + if (!$Array.isArray(object.names)) + throw $TypeError(".MumbleProto.QueryUsers.names: array expected"); + message.names = $Array(object.names.length); + for (let i = 0; i < object.names.length; ++i) + message.names[i] = $String(object.names[i]); + } + return message; + }; + + QueryUsers.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.ids = []; + object.names = []; + } + if (message.ids && message.ids.length) { + object.ids = $Array(message.ids.length); + for (let j = 0; j < message.ids.length; ++j) + object.ids[j] = message.ids[j]; + } + if (message.names && message.names.length) { + object.names = $Array(message.names.length); + for (let j = 0; j < message.names.length; ++j) + object.names[j] = message.names[j]; + } + return object; + }; + + QueryUsers.prototype.toJSON = function() { + return QueryUsers.toObject(this, $protobuf.util.toJSONOptions); + }; + + QueryUsers.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.QueryUsers"; + }; + + return QueryUsers; + })(); + + MumbleProto.CryptSetup = (function() { + + const CryptSetup = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + CryptSetup.prototype.key = $util.newBuffer([]); + CryptSetup.prototype.client_nonce = $util.newBuffer([]); + CryptSetup.prototype.server_nonce = $util.newBuffer([]); + + CryptSetup.create = function(properties) { + return new CryptSetup(properties); + }; + + CryptSetup.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.key != null && $Object.hasOwnProperty.call(message, "key")) + writer.uint32(10).bytes(message.key); + if (message.client_nonce != null && $Object.hasOwnProperty.call(message, "client_nonce")) + writer.uint32(18).bytes(message.client_nonce); + if (message.server_nonce != null && $Object.hasOwnProperty.call(message, "server_nonce")) + writer.uint32(26).bytes(message.server_nonce); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + CryptSetup.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.CryptSetup(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 2) + break; + message.key = reader.bytes(); + continue; + } + case 2: { + if (wireType !== 2) + break; + message.client_nonce = reader.bytes(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.server_nonce = reader.bytes(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + CryptSetup.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.key != null && $Object.hasOwnProperty.call(message, "key")) + if (!(message.key && typeof message.key.length === "number" || $util.isString(message.key))) + return "key: buffer expected"; + if (message.client_nonce != null && $Object.hasOwnProperty.call(message, "client_nonce")) + if (!(message.client_nonce && typeof message.client_nonce.length === "number" || $util.isString(message.client_nonce))) + return "client_nonce: buffer expected"; + if (message.server_nonce != null && $Object.hasOwnProperty.call(message, "server_nonce")) + if (!(message.server_nonce && typeof message.server_nonce.length === "number" || $util.isString(message.server_nonce))) + return "server_nonce: buffer expected"; + return null; + }; + + CryptSetup.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.CryptSetup) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.CryptSetup: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.CryptSetup(); + if (object.key != null) + if (typeof object.key === "string") + $util.base64.decode(object.key, message.key = $util.newBuffer($util.base64.length(object.key)), 0); + else if (object.key.length >= 0) + message.key = object.key; + if (object.client_nonce != null) + if (typeof object.client_nonce === "string") + $util.base64.decode(object.client_nonce, message.client_nonce = $util.newBuffer($util.base64.length(object.client_nonce)), 0); + else if (object.client_nonce.length >= 0) + message.client_nonce = object.client_nonce; + if (object.server_nonce != null) + if (typeof object.server_nonce === "string") + $util.base64.decode(object.server_nonce, message.server_nonce = $util.newBuffer($util.base64.length(object.server_nonce)), 0); + else if (object.server_nonce.length >= 0) + message.server_nonce = object.server_nonce; + return message; + }; + + CryptSetup.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + if (options.bytes === $String) + object.key = ""; + else { + object.key = []; + if (options.bytes !== $Array) + object.key = $util.newBuffer(object.key); + } + if (options.bytes === $String) + object.client_nonce = ""; + else { + object.client_nonce = []; + if (options.bytes !== $Array) + object.client_nonce = $util.newBuffer(object.client_nonce); + } + if (options.bytes === $String) + object.server_nonce = ""; + else { + object.server_nonce = []; + if (options.bytes !== $Array) + object.server_nonce = $util.newBuffer(object.server_nonce); + } + } + if (message.key != null && $Object.hasOwnProperty.call(message, "key")) + object.key = options.bytes === $String ? $util.base64.encode(message.key, 0, message.key.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.key) : message.key; + if (message.client_nonce != null && $Object.hasOwnProperty.call(message, "client_nonce")) + object.client_nonce = options.bytes === $String ? $util.base64.encode(message.client_nonce, 0, message.client_nonce.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.client_nonce) : message.client_nonce; + if (message.server_nonce != null && $Object.hasOwnProperty.call(message, "server_nonce")) + object.server_nonce = options.bytes === $String ? $util.base64.encode(message.server_nonce, 0, message.server_nonce.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.server_nonce) : message.server_nonce; + return object; + }; + + CryptSetup.prototype.toJSON = function() { + return CryptSetup.toObject(this, $protobuf.util.toJSONOptions); + }; + + CryptSetup.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.CryptSetup"; + }; + + return CryptSetup; + })(); + + MumbleProto.ContextActionModify = (function() { + + const ContextActionModify = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ContextActionModify.prototype.action = ""; + ContextActionModify.prototype.text = ""; + ContextActionModify.prototype.context = 0; + ContextActionModify.prototype.operation = 0; + + ContextActionModify.create = function(properties) { + return new ContextActionModify(properties); + }; + + ContextActionModify.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(10).string(message.action); + if (message.text != null && $Object.hasOwnProperty.call(message, "text")) + writer.uint32(18).string(message.text); + if (message.context != null && $Object.hasOwnProperty.call(message, "context")) + writer.uint32(24).uint32(message.context); + if (message.operation != null && $Object.hasOwnProperty.call(message, "operation")) + writer.uint32(32).int32(message.operation); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ContextActionModify.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message, value; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ContextActionModify(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 2) + break; + message.action = reader.string(); + continue; + } + case 2: { + if (wireType !== 2) + break; + message.text = reader.string(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.context = reader.uint32(); + continue; + } + case 4: { + if (wireType !== 0) + break; + value = reader.int32(); + if ($root.MumbleProto.ContextActionModify.Operation[value] !== $undefined) + message.operation = value; + else if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "action")) + throw $util.ProtocolError("missing required 'action'", { instance: message }); + return message; + }; + + ContextActionModify.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!$util.isString(message.action)) + return "action: string expected"; + if (message.text != null && $Object.hasOwnProperty.call(message, "text")) + if (!$util.isString(message.text)) + return "text: string expected"; + if (message.context != null && $Object.hasOwnProperty.call(message, "context")) + if (!$util.isInteger(message.context)) + return "context: integer expected"; + if (message.operation != null && $Object.hasOwnProperty.call(message, "operation")) + switch (message.operation) { + default: + return "operation: enum value expected"; + case 0: + case 1: + break; + } + return null; + }; + + ContextActionModify.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ContextActionModify) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ContextActionModify: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ContextActionModify(); + if (object.action != null) + message.action = $String(object.action); + if (object.text != null) + message.text = $String(object.text); + if (object.context != null) + message.context = object.context >>> 0; + switch (object.operation) { + case "Add": + case 0: + message.operation = 0; + break; + case "Remove": + case 1: + message.operation = 1; + break; + default: + } + return message; + }; + + ContextActionModify.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.action = ""; + object.text = ""; + object.context = 0; + object.operation = options.enums === $String ? "Add" : 0; + } + if (message.action != null && $Object.hasOwnProperty.call(message, "action")) + object.action = message.action; + if (message.text != null && $Object.hasOwnProperty.call(message, "text")) + object.text = message.text; + if (message.context != null && $Object.hasOwnProperty.call(message, "context")) + object.context = message.context; + if (message.operation != null && $Object.hasOwnProperty.call(message, "operation")) + object.operation = options.enums === $String ? $root.MumbleProto.ContextActionModify.Operation[message.operation] === $undefined ? message.operation : $root.MumbleProto.ContextActionModify.Operation[message.operation] : message.operation; + return object; + }; + + ContextActionModify.prototype.toJSON = function() { + return ContextActionModify.toObject(this, $protobuf.util.toJSONOptions); + }; + + ContextActionModify.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ContextActionModify"; + }; + + ContextActionModify.Context = (function() { + const valuesById = $Object.create(null), values = $Object.create(valuesById); + values[valuesById[1] = "Server"] = 1; + values[valuesById[2] = "Channel"] = 2; + values[valuesById[4] = "User"] = 4; + return values; + })(); + + ContextActionModify.Operation = (function() { + const valuesById = $Object.create(null), values = $Object.create(valuesById); + values[valuesById[0] = "Add"] = 0; + values[valuesById[1] = "Remove"] = 1; + return values; + })(); + + return ContextActionModify; + })(); + + MumbleProto.ContextAction = (function() { + + const ContextAction = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ContextAction.prototype.session = 0; + ContextAction.prototype.channel_id = 0; + ContextAction.prototype.action = ""; + + ContextAction.create = function(properties) { + return new ContextAction(properties); + }; + + ContextAction.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + writer.uint32(8).uint32(message.session); + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + writer.uint32(16).uint32(message.channel_id); + writer.uint32(26).string(message.action); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ContextAction.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ContextAction(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.session = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.channel_id = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.action = reader.string(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "action")) + throw $util.ProtocolError("missing required 'action'", { instance: message }); + return message; + }; + + ContextAction.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + if (!$util.isInteger(message.session)) + return "session: integer expected"; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + if (!$util.isInteger(message.channel_id)) + return "channel_id: integer expected"; + if (!$util.isString(message.action)) + return "action: string expected"; + return null; + }; + + ContextAction.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ContextAction) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ContextAction: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ContextAction(); + if (object.session != null) + message.session = object.session >>> 0; + if (object.channel_id != null) + message.channel_id = object.channel_id >>> 0; + if (object.action != null) + message.action = $String(object.action); + return message; + }; + + ContextAction.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.session = 0; + object.channel_id = 0; + object.action = ""; + } + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + object.session = message.session; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + object.channel_id = message.channel_id; + if (message.action != null && $Object.hasOwnProperty.call(message, "action")) + object.action = message.action; + return object; + }; + + ContextAction.prototype.toJSON = function() { + return ContextAction.toObject(this, $protobuf.util.toJSONOptions); + }; + + ContextAction.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ContextAction"; + }; + + return ContextAction; + })(); + + MumbleProto.UserList = (function() { + + const UserList = function (properties) { + this.users = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + UserList.prototype.users = $util.emptyArray; + + UserList.create = function(properties) { + return new UserList(properties); + }; + + UserList.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.users != null && message.users.length) + for (let i = 0; i < message.users.length; ++i) + $root.MumbleProto.UserList.User.encode(message.users[i], writer.uint32(10).fork(), _depth + 1).ldelim(); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + UserList.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UserList(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 2) + break; + if (!(message.users && message.users.length)) + message.users = []; + message.users.push($root.MumbleProto.UserList.User.decode(reader, reader.uint32(), $undefined, _depth + 1)); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + UserList.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.users != null && $Object.hasOwnProperty.call(message, "users")) { + if (!$Array.isArray(message.users)) + return "users: array expected"; + for (let i = 0; i < message.users.length; ++i) { + let error = $root.MumbleProto.UserList.User.verify(message.users[i], _depth + 1); + if (error) + return "users." + error; + } + } + return null; + }; + + UserList.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UserList) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UserList: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UserList(); + if (object.users) { + if (!$Array.isArray(object.users)) + throw $TypeError(".MumbleProto.UserList.users: array expected"); + message.users = $Array(object.users.length); + for (let i = 0; i < object.users.length; ++i) { + if (!$util.isObject(object.users[i])) + throw $TypeError(".MumbleProto.UserList.users: object expected"); + message.users[i] = $root.MumbleProto.UserList.User.fromObject(object.users[i], _depth + 1); + } + } + return message; + }; + + UserList.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.users = []; + if (message.users && message.users.length) { + object.users = $Array(message.users.length); + for (let j = 0; j < message.users.length; ++j) + object.users[j] = $root.MumbleProto.UserList.User.toObject(message.users[j], options, _depth + 1); + } + return object; + }; + + UserList.prototype.toJSON = function() { + return UserList.toObject(this, $protobuf.util.toJSONOptions); + }; + + UserList.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UserList"; + }; + + UserList.User = (function() { + + const User = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + User.prototype.user_id = 0; + User.prototype.name = ""; + User.prototype.last_seen = ""; + User.prototype.last_channel = 0; + + User.create = function(properties) { + return new User(properties); + }; + + User.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(8).uint32(message.user_id); + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + writer.uint32(18).string(message.name); + if (message.last_seen != null && $Object.hasOwnProperty.call(message, "last_seen")) + writer.uint32(26).string(message.last_seen); + if (message.last_channel != null && $Object.hasOwnProperty.call(message, "last_channel")) + writer.uint32(32).uint32(message.last_channel); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + User.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UserList.User(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.user_id = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 2) + break; + message.name = reader.string(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.last_seen = reader.string(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.last_channel = reader.uint32(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "user_id")) + throw $util.ProtocolError("missing required 'user_id'", { instance: message }); + return message; + }; + + User.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!$util.isInteger(message.user_id)) + return "user_id: integer expected"; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + if (!$util.isString(message.name)) + return "name: string expected"; + if (message.last_seen != null && $Object.hasOwnProperty.call(message, "last_seen")) + if (!$util.isString(message.last_seen)) + return "last_seen: string expected"; + if (message.last_channel != null && $Object.hasOwnProperty.call(message, "last_channel")) + if (!$util.isInteger(message.last_channel)) + return "last_channel: integer expected"; + return null; + }; + + User.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UserList.User) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UserList.User: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UserList.User(); + if (object.user_id != null) + message.user_id = object.user_id >>> 0; + if (object.name != null) + message.name = $String(object.name); + if (object.last_seen != null) + message.last_seen = $String(object.last_seen); + if (object.last_channel != null) + message.last_channel = object.last_channel >>> 0; + return message; + }; + + User.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.user_id = 0; + object.name = ""; + object.last_seen = ""; + object.last_channel = 0; + } + if (message.user_id != null && $Object.hasOwnProperty.call(message, "user_id")) + object.user_id = message.user_id; + if (message.name != null && $Object.hasOwnProperty.call(message, "name")) + object.name = message.name; + if (message.last_seen != null && $Object.hasOwnProperty.call(message, "last_seen")) + object.last_seen = message.last_seen; + if (message.last_channel != null && $Object.hasOwnProperty.call(message, "last_channel")) + object.last_channel = message.last_channel; + return object; + }; + + User.prototype.toJSON = function() { + return User.toObject(this, $protobuf.util.toJSONOptions); + }; + + User.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UserList.User"; + }; + + return User; + })(); + + return UserList; + })(); + + MumbleProto.VoiceTarget = (function() { + + const VoiceTarget = function (properties) { + this.targets = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + VoiceTarget.prototype.id = 0; + VoiceTarget.prototype.targets = $util.emptyArray; + + VoiceTarget.create = function(properties) { + return new VoiceTarget(properties); + }; + + VoiceTarget.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.id != null && $Object.hasOwnProperty.call(message, "id")) + writer.uint32(8).uint32(message.id); + if (message.targets != null && message.targets.length) + for (let i = 0; i < message.targets.length; ++i) + $root.MumbleProto.VoiceTarget.Target.encode(message.targets[i], writer.uint32(18).fork(), _depth + 1).ldelim(); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + VoiceTarget.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.VoiceTarget(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.id = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 2) + break; + if (!(message.targets && message.targets.length)) + message.targets = []; + message.targets.push($root.MumbleProto.VoiceTarget.Target.decode(reader, reader.uint32(), $undefined, _depth + 1)); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + VoiceTarget.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.id != null && $Object.hasOwnProperty.call(message, "id")) + if (!$util.isInteger(message.id)) + return "id: integer expected"; + if (message.targets != null && $Object.hasOwnProperty.call(message, "targets")) { + if (!$Array.isArray(message.targets)) + return "targets: array expected"; + for (let i = 0; i < message.targets.length; ++i) { + let error = $root.MumbleProto.VoiceTarget.Target.verify(message.targets[i], _depth + 1); + if (error) + return "targets." + error; + } + } + return null; + }; + + VoiceTarget.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.VoiceTarget) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.VoiceTarget: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.VoiceTarget(); + if (object.id != null) + message.id = object.id >>> 0; + if (object.targets) { + if (!$Array.isArray(object.targets)) + throw $TypeError(".MumbleProto.VoiceTarget.targets: array expected"); + message.targets = $Array(object.targets.length); + for (let i = 0; i < object.targets.length; ++i) { + if (!$util.isObject(object.targets[i])) + throw $TypeError(".MumbleProto.VoiceTarget.targets: object expected"); + message.targets[i] = $root.MumbleProto.VoiceTarget.Target.fromObject(object.targets[i], _depth + 1); + } + } + return message; + }; + + VoiceTarget.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.targets = []; + if (options.defaults) + object.id = 0; + if (message.id != null && $Object.hasOwnProperty.call(message, "id")) + object.id = message.id; + if (message.targets && message.targets.length) { + object.targets = $Array(message.targets.length); + for (let j = 0; j < message.targets.length; ++j) + object.targets[j] = $root.MumbleProto.VoiceTarget.Target.toObject(message.targets[j], options, _depth + 1); + } + return object; + }; + + VoiceTarget.prototype.toJSON = function() { + return VoiceTarget.toObject(this, $protobuf.util.toJSONOptions); + }; + + VoiceTarget.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.VoiceTarget"; + }; + + VoiceTarget.Target = (function() { + + const Target = function (properties) { + this.session = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + Target.prototype.session = $util.emptyArray; + Target.prototype.channel_id = 0; + Target.prototype.group = ""; + Target.prototype.links = false; + Target.prototype.children = false; + + Target.create = function(properties) { + return new Target(properties); + }; + + Target.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.session != null && message.session.length) + for (let i = 0; i < message.session.length; ++i) + writer.uint32(8).uint32(message.session[i]); + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + writer.uint32(16).uint32(message.channel_id); + if (message.group != null && $Object.hasOwnProperty.call(message, "group")) + writer.uint32(26).string(message.group); + if (message.links != null && $Object.hasOwnProperty.call(message, "links")) + writer.uint32(32).bool(message.links); + if (message.children != null && $Object.hasOwnProperty.call(message, "children")) + writer.uint32(40).bool(message.children); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + Target.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.VoiceTarget.Target(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType === 2) { + if (!(message.session && message.session.length)) + message.session = []; + reader.uint32s(message.session); + continue; + } + if (wireType !== 0) + break; + if (!(message.session && message.session.length)) + message.session = []; + message.session.push(reader.uint32()); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.channel_id = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.group = reader.string(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.links = reader.bool(); + continue; + } + case 5: { + if (wireType !== 0) + break; + message.children = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + Target.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) { + if (!$Array.isArray(message.session)) + return "session: array expected"; + for (let i = 0; i < message.session.length; ++i) + if (!$util.isInteger(message.session[i])) + return "session: integer[] expected"; + } + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + if (!$util.isInteger(message.channel_id)) + return "channel_id: integer expected"; + if (message.group != null && $Object.hasOwnProperty.call(message, "group")) + if (!$util.isString(message.group)) + return "group: string expected"; + if (message.links != null && $Object.hasOwnProperty.call(message, "links")) + if (typeof message.links !== "boolean") + return "links: boolean expected"; + if (message.children != null && $Object.hasOwnProperty.call(message, "children")) + if (typeof message.children !== "boolean") + return "children: boolean expected"; + return null; + }; + + Target.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.VoiceTarget.Target) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.VoiceTarget.Target: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.VoiceTarget.Target(); + if (object.session) { + if (!$Array.isArray(object.session)) + throw $TypeError(".MumbleProto.VoiceTarget.Target.session: array expected"); + message.session = $Array(object.session.length); + for (let i = 0; i < object.session.length; ++i) + message.session[i] = object.session[i] >>> 0; + } + if (object.channel_id != null) + message.channel_id = object.channel_id >>> 0; + if (object.group != null) + message.group = $String(object.group); + if (object.links != null) + message.links = $Boolean(object.links); + if (object.children != null) + message.children = $Boolean(object.children); + return message; + }; + + Target.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.session = []; + if (options.defaults) { + object.channel_id = 0; + object.group = ""; + object.links = false; + object.children = false; + } + if (message.session && message.session.length) { + object.session = $Array(message.session.length); + for (let j = 0; j < message.session.length; ++j) + object.session[j] = message.session[j]; + } + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + object.channel_id = message.channel_id; + if (message.group != null && $Object.hasOwnProperty.call(message, "group")) + object.group = message.group; + if (message.links != null && $Object.hasOwnProperty.call(message, "links")) + object.links = message.links; + if (message.children != null && $Object.hasOwnProperty.call(message, "children")) + object.children = message.children; + return object; + }; + + Target.prototype.toJSON = function() { + return Target.toObject(this, $protobuf.util.toJSONOptions); + }; + + Target.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.VoiceTarget.Target"; + }; + + return Target; + })(); + + return VoiceTarget; + })(); + + MumbleProto.PermissionQuery = (function() { + + const PermissionQuery = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + PermissionQuery.prototype.channel_id = 0; + PermissionQuery.prototype.permissions = 0; + PermissionQuery.prototype.flush = false; + + PermissionQuery.create = function(properties) { + return new PermissionQuery(properties); + }; + + PermissionQuery.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + writer.uint32(8).uint32(message.channel_id); + if (message.permissions != null && $Object.hasOwnProperty.call(message, "permissions")) + writer.uint32(16).uint32(message.permissions); + if (message.flush != null && $Object.hasOwnProperty.call(message, "flush")) + writer.uint32(24).bool(message.flush); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + PermissionQuery.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.PermissionQuery(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.channel_id = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.permissions = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.flush = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + PermissionQuery.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + if (!$util.isInteger(message.channel_id)) + return "channel_id: integer expected"; + if (message.permissions != null && $Object.hasOwnProperty.call(message, "permissions")) + if (!$util.isInteger(message.permissions)) + return "permissions: integer expected"; + if (message.flush != null && $Object.hasOwnProperty.call(message, "flush")) + if (typeof message.flush !== "boolean") + return "flush: boolean expected"; + return null; + }; + + PermissionQuery.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.PermissionQuery) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.PermissionQuery: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.PermissionQuery(); + if (object.channel_id != null) + message.channel_id = object.channel_id >>> 0; + if (object.permissions != null) + message.permissions = object.permissions >>> 0; + if (object.flush != null) + message.flush = $Boolean(object.flush); + return message; + }; + + PermissionQuery.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.channel_id = 0; + object.permissions = 0; + object.flush = false; + } + if (message.channel_id != null && $Object.hasOwnProperty.call(message, "channel_id")) + object.channel_id = message.channel_id; + if (message.permissions != null && $Object.hasOwnProperty.call(message, "permissions")) + object.permissions = message.permissions; + if (message.flush != null && $Object.hasOwnProperty.call(message, "flush")) + object.flush = message.flush; + return object; + }; + + PermissionQuery.prototype.toJSON = function() { + return PermissionQuery.toObject(this, $protobuf.util.toJSONOptions); + }; + + PermissionQuery.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.PermissionQuery"; + }; + + return PermissionQuery; + })(); + + MumbleProto.CodecVersion = (function() { + + const CodecVersion = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + CodecVersion.prototype.alpha = 0; + CodecVersion.prototype.beta = 0; + CodecVersion.prototype.prefer_alpha = true; + CodecVersion.prototype.opus = false; + + CodecVersion.create = function(properties) { + return new CodecVersion(properties); + }; + + CodecVersion.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + writer.uint32(8).int32(message.alpha); + writer.uint32(16).int32(message.beta); + writer.uint32(24).bool(message.prefer_alpha); + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + writer.uint32(32).bool(message.opus); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + CodecVersion.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.CodecVersion(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.alpha = reader.int32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.beta = reader.int32(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.prefer_alpha = reader.bool(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.opus = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + if (!$Object.hasOwnProperty.call(message, "alpha")) + throw $util.ProtocolError("missing required 'alpha'", { instance: message }); + if (!$Object.hasOwnProperty.call(message, "beta")) + throw $util.ProtocolError("missing required 'beta'", { instance: message }); + if (!$Object.hasOwnProperty.call(message, "prefer_alpha")) + throw $util.ProtocolError("missing required 'prefer_alpha'", { instance: message }); + return message; + }; + + CodecVersion.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (!$util.isInteger(message.alpha)) + return "alpha: integer expected"; + if (!$util.isInteger(message.beta)) + return "beta: integer expected"; + if (typeof message.prefer_alpha !== "boolean") + return "prefer_alpha: boolean expected"; + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + if (typeof message.opus !== "boolean") + return "opus: boolean expected"; + return null; + }; + + CodecVersion.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.CodecVersion) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.CodecVersion: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.CodecVersion(); + if (object.alpha != null) + message.alpha = object.alpha | 0; + if (object.beta != null) + message.beta = object.beta | 0; + if (object.prefer_alpha != null) + message.prefer_alpha = $Boolean(object.prefer_alpha); + if (object.opus != null) + message.opus = $Boolean(object.opus); + return message; + }; + + CodecVersion.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.alpha = 0; + object.beta = 0; + object.prefer_alpha = true; + object.opus = false; + } + if (message.alpha != null && $Object.hasOwnProperty.call(message, "alpha")) + object.alpha = message.alpha; + if (message.beta != null && $Object.hasOwnProperty.call(message, "beta")) + object.beta = message.beta; + if (message.prefer_alpha != null && $Object.hasOwnProperty.call(message, "prefer_alpha")) + object.prefer_alpha = message.prefer_alpha; + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + object.opus = message.opus; + return object; + }; + + CodecVersion.prototype.toJSON = function() { + return CodecVersion.toObject(this, $protobuf.util.toJSONOptions); + }; + + CodecVersion.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.CodecVersion"; + }; + + return CodecVersion; + })(); + + MumbleProto.UserStats = (function() { + + const UserStats = function (properties) { + this.certificates = []; + this.celt_versions = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + UserStats.prototype.session = 0; + UserStats.prototype.stats_only = false; + UserStats.prototype.certificates = $util.emptyArray; + UserStats.prototype.from_client = null; + UserStats.prototype.from_server = null; + UserStats.prototype.udp_packets = 0; + UserStats.prototype.tcp_packets = 0; + UserStats.prototype.udp_ping_avg = 0; + UserStats.prototype.udp_ping_var = 0; + UserStats.prototype.tcp_ping_avg = 0; + UserStats.prototype.tcp_ping_var = 0; + UserStats.prototype.version = null; + UserStats.prototype.celt_versions = $util.emptyArray; + UserStats.prototype.address = $util.newBuffer([]); + UserStats.prototype.bandwidth = 0; + UserStats.prototype.onlinesecs = 0; + UserStats.prototype.idlesecs = 0; + UserStats.prototype.strong_certificate = false; + UserStats.prototype.opus = false; + UserStats.prototype.rolling_stats = null; + + UserStats.create = function(properties) { + return new UserStats(properties); + }; + + UserStats.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + writer.uint32(8).uint32(message.session); + if (message.stats_only != null && $Object.hasOwnProperty.call(message, "stats_only")) + writer.uint32(16).bool(message.stats_only); + if (message.certificates != null && message.certificates.length) + for (let i = 0; i < message.certificates.length; ++i) + writer.uint32(26).bytes(message.certificates[i]); + if (message.from_client != null && $Object.hasOwnProperty.call(message, "from_client")) + $root.MumbleProto.UserStats.Stats.encode(message.from_client, writer.uint32(34).fork(), _depth + 1).ldelim(); + if (message.from_server != null && $Object.hasOwnProperty.call(message, "from_server")) + $root.MumbleProto.UserStats.Stats.encode(message.from_server, writer.uint32(42).fork(), _depth + 1).ldelim(); + if (message.udp_packets != null && $Object.hasOwnProperty.call(message, "udp_packets")) + writer.uint32(48).uint32(message.udp_packets); + if (message.tcp_packets != null && $Object.hasOwnProperty.call(message, "tcp_packets")) + writer.uint32(56).uint32(message.tcp_packets); + if (message.udp_ping_avg != null && $Object.hasOwnProperty.call(message, "udp_ping_avg")) + writer.uint32(69).float(message.udp_ping_avg); + if (message.udp_ping_var != null && $Object.hasOwnProperty.call(message, "udp_ping_var")) + writer.uint32(77).float(message.udp_ping_var); + if (message.tcp_ping_avg != null && $Object.hasOwnProperty.call(message, "tcp_ping_avg")) + writer.uint32(85).float(message.tcp_ping_avg); + if (message.tcp_ping_var != null && $Object.hasOwnProperty.call(message, "tcp_ping_var")) + writer.uint32(93).float(message.tcp_ping_var); + if (message.version != null && $Object.hasOwnProperty.call(message, "version")) + $root.MumbleProto.Version.encode(message.version, writer.uint32(98).fork(), _depth + 1).ldelim(); + if (message.celt_versions != null && message.celt_versions.length) + for (let i = 0; i < message.celt_versions.length; ++i) + writer.uint32(104).int32(message.celt_versions[i]); + if (message.address != null && $Object.hasOwnProperty.call(message, "address")) + writer.uint32(114).bytes(message.address); + if (message.bandwidth != null && $Object.hasOwnProperty.call(message, "bandwidth")) + writer.uint32(120).uint32(message.bandwidth); + if (message.onlinesecs != null && $Object.hasOwnProperty.call(message, "onlinesecs")) + writer.uint32(128).uint32(message.onlinesecs); + if (message.idlesecs != null && $Object.hasOwnProperty.call(message, "idlesecs")) + writer.uint32(136).uint32(message.idlesecs); + if (message.strong_certificate != null && $Object.hasOwnProperty.call(message, "strong_certificate")) + writer.uint32(144).bool(message.strong_certificate); + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + writer.uint32(152).bool(message.opus); + if (message.rolling_stats != null && $Object.hasOwnProperty.call(message, "rolling_stats")) + $root.MumbleProto.UserStats.RollingStats.encode(message.rolling_stats, writer.uint32(162).fork(), _depth + 1).ldelim(); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + UserStats.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UserStats(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.session = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.stats_only = reader.bool(); + continue; + } + case 3: { + if (wireType !== 2) + break; + if (!(message.certificates && message.certificates.length)) + message.certificates = []; + message.certificates.push(reader.bytes()); + continue; + } + case 4: { + if (wireType !== 2) + break; + message.from_client = $root.MumbleProto.UserStats.Stats.decode(reader, reader.uint32(), $undefined, _depth + 1, message.from_client); + continue; + } + case 5: { + if (wireType !== 2) + break; + message.from_server = $root.MumbleProto.UserStats.Stats.decode(reader, reader.uint32(), $undefined, _depth + 1, message.from_server); + continue; + } + case 6: { + if (wireType !== 0) + break; + message.udp_packets = reader.uint32(); + continue; + } + case 7: { + if (wireType !== 0) + break; + message.tcp_packets = reader.uint32(); + continue; + } + case 8: { + if (wireType !== 5) + break; + message.udp_ping_avg = reader.float(); + continue; + } + case 9: { + if (wireType !== 5) + break; + message.udp_ping_var = reader.float(); + continue; + } + case 10: { + if (wireType !== 5) + break; + message.tcp_ping_avg = reader.float(); + continue; + } + case 11: { + if (wireType !== 5) + break; + message.tcp_ping_var = reader.float(); + continue; + } + case 12: { + if (wireType !== 2) + break; + message.version = $root.MumbleProto.Version.decode(reader, reader.uint32(), $undefined, _depth + 1, message.version); + continue; + } + case 13: { + if (wireType === 2) { + if (!(message.celt_versions && message.celt_versions.length)) + message.celt_versions = []; + reader.int32s(message.celt_versions); + continue; + } + if (wireType !== 0) + break; + if (!(message.celt_versions && message.celt_versions.length)) + message.celt_versions = []; + message.celt_versions.push(reader.int32()); + continue; + } + case 14: { + if (wireType !== 2) + break; + message.address = reader.bytes(); + continue; + } + case 15: { + if (wireType !== 0) + break; + message.bandwidth = reader.uint32(); + continue; + } + case 16: { + if (wireType !== 0) + break; + message.onlinesecs = reader.uint32(); + continue; + } + case 17: { + if (wireType !== 0) + break; + message.idlesecs = reader.uint32(); + continue; + } + case 18: { + if (wireType !== 0) + break; + message.strong_certificate = reader.bool(); + continue; + } + case 19: { + if (wireType !== 0) + break; + message.opus = reader.bool(); + continue; + } + case 20: { + if (wireType !== 2) + break; + message.rolling_stats = $root.MumbleProto.UserStats.RollingStats.decode(reader, reader.uint32(), $undefined, _depth + 1, message.rolling_stats); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + UserStats.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + if (!$util.isInteger(message.session)) + return "session: integer expected"; + if (message.stats_only != null && $Object.hasOwnProperty.call(message, "stats_only")) + if (typeof message.stats_only !== "boolean") + return "stats_only: boolean expected"; + if (message.certificates != null && $Object.hasOwnProperty.call(message, "certificates")) { + if (!$Array.isArray(message.certificates)) + return "certificates: array expected"; + for (let i = 0; i < message.certificates.length; ++i) + if (!(message.certificates[i] && typeof message.certificates[i].length === "number" || $util.isString(message.certificates[i]))) + return "certificates: buffer[] expected"; + } + if (message.from_client != null && $Object.hasOwnProperty.call(message, "from_client")) { + let error = $root.MumbleProto.UserStats.Stats.verify(message.from_client, _depth + 1); + if (error) + return "from_client." + error; + } + if (message.from_server != null && $Object.hasOwnProperty.call(message, "from_server")) { + let error = $root.MumbleProto.UserStats.Stats.verify(message.from_server, _depth + 1); + if (error) + return "from_server." + error; + } + if (message.udp_packets != null && $Object.hasOwnProperty.call(message, "udp_packets")) + if (!$util.isInteger(message.udp_packets)) + return "udp_packets: integer expected"; + if (message.tcp_packets != null && $Object.hasOwnProperty.call(message, "tcp_packets")) + if (!$util.isInteger(message.tcp_packets)) + return "tcp_packets: integer expected"; + if (message.udp_ping_avg != null && $Object.hasOwnProperty.call(message, "udp_ping_avg")) + if (typeof message.udp_ping_avg !== "number") + return "udp_ping_avg: number expected"; + if (message.udp_ping_var != null && $Object.hasOwnProperty.call(message, "udp_ping_var")) + if (typeof message.udp_ping_var !== "number") + return "udp_ping_var: number expected"; + if (message.tcp_ping_avg != null && $Object.hasOwnProperty.call(message, "tcp_ping_avg")) + if (typeof message.tcp_ping_avg !== "number") + return "tcp_ping_avg: number expected"; + if (message.tcp_ping_var != null && $Object.hasOwnProperty.call(message, "tcp_ping_var")) + if (typeof message.tcp_ping_var !== "number") + return "tcp_ping_var: number expected"; + if (message.version != null && $Object.hasOwnProperty.call(message, "version")) { + let error = $root.MumbleProto.Version.verify(message.version, _depth + 1); + if (error) + return "version." + error; + } + if (message.celt_versions != null && $Object.hasOwnProperty.call(message, "celt_versions")) { + if (!$Array.isArray(message.celt_versions)) + return "celt_versions: array expected"; + for (let i = 0; i < message.celt_versions.length; ++i) + if (!$util.isInteger(message.celt_versions[i])) + return "celt_versions: integer[] expected"; + } + if (message.address != null && $Object.hasOwnProperty.call(message, "address")) + if (!(message.address && typeof message.address.length === "number" || $util.isString(message.address))) + return "address: buffer expected"; + if (message.bandwidth != null && $Object.hasOwnProperty.call(message, "bandwidth")) + if (!$util.isInteger(message.bandwidth)) + return "bandwidth: integer expected"; + if (message.onlinesecs != null && $Object.hasOwnProperty.call(message, "onlinesecs")) + if (!$util.isInteger(message.onlinesecs)) + return "onlinesecs: integer expected"; + if (message.idlesecs != null && $Object.hasOwnProperty.call(message, "idlesecs")) + if (!$util.isInteger(message.idlesecs)) + return "idlesecs: integer expected"; + if (message.strong_certificate != null && $Object.hasOwnProperty.call(message, "strong_certificate")) + if (typeof message.strong_certificate !== "boolean") + return "strong_certificate: boolean expected"; + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + if (typeof message.opus !== "boolean") + return "opus: boolean expected"; + if (message.rolling_stats != null && $Object.hasOwnProperty.call(message, "rolling_stats")) { + let error = $root.MumbleProto.UserStats.RollingStats.verify(message.rolling_stats, _depth + 1); + if (error) + return "rolling_stats." + error; + } + return null; + }; + + UserStats.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UserStats) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UserStats: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UserStats(); + if (object.session != null) + message.session = object.session >>> 0; + if (object.stats_only != null) + message.stats_only = $Boolean(object.stats_only); + if (object.certificates) { + if (!$Array.isArray(object.certificates)) + throw $TypeError(".MumbleProto.UserStats.certificates: array expected"); + message.certificates = $Array(object.certificates.length); + for (let i = 0; i < object.certificates.length; ++i) + if (typeof object.certificates[i] === "string") + $util.base64.decode(object.certificates[i], message.certificates[i] = $util.newBuffer($util.base64.length(object.certificates[i])), 0); + else if (object.certificates[i].length >= 0) + message.certificates[i] = object.certificates[i]; + } + if (object.from_client != null) { + if (!$util.isObject(object.from_client)) + throw $TypeError(".MumbleProto.UserStats.from_client: object expected"); + message.from_client = $root.MumbleProto.UserStats.Stats.fromObject(object.from_client, _depth + 1); + } + if (object.from_server != null) { + if (!$util.isObject(object.from_server)) + throw $TypeError(".MumbleProto.UserStats.from_server: object expected"); + message.from_server = $root.MumbleProto.UserStats.Stats.fromObject(object.from_server, _depth + 1); + } + if (object.udp_packets != null) + message.udp_packets = object.udp_packets >>> 0; + if (object.tcp_packets != null) + message.tcp_packets = object.tcp_packets >>> 0; + if (object.udp_ping_avg != null) + message.udp_ping_avg = $Number(object.udp_ping_avg); + if (object.udp_ping_var != null) + message.udp_ping_var = $Number(object.udp_ping_var); + if (object.tcp_ping_avg != null) + message.tcp_ping_avg = $Number(object.tcp_ping_avg); + if (object.tcp_ping_var != null) + message.tcp_ping_var = $Number(object.tcp_ping_var); + if (object.version != null) { + if (!$util.isObject(object.version)) + throw $TypeError(".MumbleProto.UserStats.version: object expected"); + message.version = $root.MumbleProto.Version.fromObject(object.version, _depth + 1); + } + if (object.celt_versions) { + if (!$Array.isArray(object.celt_versions)) + throw $TypeError(".MumbleProto.UserStats.celt_versions: array expected"); + message.celt_versions = $Array(object.celt_versions.length); + for (let i = 0; i < object.celt_versions.length; ++i) + message.celt_versions[i] = object.celt_versions[i] | 0; + } + if (object.address != null) + if (typeof object.address === "string") + $util.base64.decode(object.address, message.address = $util.newBuffer($util.base64.length(object.address)), 0); + else if (object.address.length >= 0) + message.address = object.address; + if (object.bandwidth != null) + message.bandwidth = object.bandwidth >>> 0; + if (object.onlinesecs != null) + message.onlinesecs = object.onlinesecs >>> 0; + if (object.idlesecs != null) + message.idlesecs = object.idlesecs >>> 0; + if (object.strong_certificate != null) + message.strong_certificate = $Boolean(object.strong_certificate); + if (object.opus != null) + message.opus = $Boolean(object.opus); + if (object.rolling_stats != null) { + if (!$util.isObject(object.rolling_stats)) + throw $TypeError(".MumbleProto.UserStats.rolling_stats: object expected"); + message.rolling_stats = $root.MumbleProto.UserStats.RollingStats.fromObject(object.rolling_stats, _depth + 1); + } + return message; + }; + + UserStats.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.certificates = []; + object.celt_versions = []; + } + if (options.defaults) { + object.session = 0; + object.stats_only = false; + object.from_client = null; + object.from_server = null; + object.udp_packets = 0; + object.tcp_packets = 0; + object.udp_ping_avg = 0; + object.udp_ping_var = 0; + object.tcp_ping_avg = 0; + object.tcp_ping_var = 0; + object.version = null; + if (options.bytes === $String) + object.address = ""; + else { + object.address = []; + if (options.bytes !== $Array) + object.address = $util.newBuffer(object.address); + } + object.bandwidth = 0; + object.onlinesecs = 0; + object.idlesecs = 0; + object.strong_certificate = false; + object.opus = false; + object.rolling_stats = null; + } + if (message.session != null && $Object.hasOwnProperty.call(message, "session")) + object.session = message.session; + if (message.stats_only != null && $Object.hasOwnProperty.call(message, "stats_only")) + object.stats_only = message.stats_only; + if (message.certificates && message.certificates.length) { + object.certificates = $Array(message.certificates.length); + for (let j = 0; j < message.certificates.length; ++j) + object.certificates[j] = options.bytes === $String ? $util.base64.encode(message.certificates[j], 0, message.certificates[j].length) : options.bytes === $Array ? $Array.prototype.slice.call(message.certificates[j]) : message.certificates[j]; + } + if (message.from_client != null && $Object.hasOwnProperty.call(message, "from_client")) + object.from_client = $root.MumbleProto.UserStats.Stats.toObject(message.from_client, options, _depth + 1); + if (message.from_server != null && $Object.hasOwnProperty.call(message, "from_server")) + object.from_server = $root.MumbleProto.UserStats.Stats.toObject(message.from_server, options, _depth + 1); + if (message.udp_packets != null && $Object.hasOwnProperty.call(message, "udp_packets")) + object.udp_packets = message.udp_packets; + if (message.tcp_packets != null && $Object.hasOwnProperty.call(message, "tcp_packets")) + object.tcp_packets = message.tcp_packets; + if (message.udp_ping_avg != null && $Object.hasOwnProperty.call(message, "udp_ping_avg")) + object.udp_ping_avg = options.json && !$isFinite(message.udp_ping_avg) ? $String(message.udp_ping_avg) : message.udp_ping_avg; + if (message.udp_ping_var != null && $Object.hasOwnProperty.call(message, "udp_ping_var")) + object.udp_ping_var = options.json && !$isFinite(message.udp_ping_var) ? $String(message.udp_ping_var) : message.udp_ping_var; + if (message.tcp_ping_avg != null && $Object.hasOwnProperty.call(message, "tcp_ping_avg")) + object.tcp_ping_avg = options.json && !$isFinite(message.tcp_ping_avg) ? $String(message.tcp_ping_avg) : message.tcp_ping_avg; + if (message.tcp_ping_var != null && $Object.hasOwnProperty.call(message, "tcp_ping_var")) + object.tcp_ping_var = options.json && !$isFinite(message.tcp_ping_var) ? $String(message.tcp_ping_var) : message.tcp_ping_var; + if (message.version != null && $Object.hasOwnProperty.call(message, "version")) + object.version = $root.MumbleProto.Version.toObject(message.version, options, _depth + 1); + if (message.celt_versions && message.celt_versions.length) { + object.celt_versions = $Array(message.celt_versions.length); + for (let j = 0; j < message.celt_versions.length; ++j) + object.celt_versions[j] = message.celt_versions[j]; + } + if (message.address != null && $Object.hasOwnProperty.call(message, "address")) + object.address = options.bytes === $String ? $util.base64.encode(message.address, 0, message.address.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.address) : message.address; + if (message.bandwidth != null && $Object.hasOwnProperty.call(message, "bandwidth")) + object.bandwidth = message.bandwidth; + if (message.onlinesecs != null && $Object.hasOwnProperty.call(message, "onlinesecs")) + object.onlinesecs = message.onlinesecs; + if (message.idlesecs != null && $Object.hasOwnProperty.call(message, "idlesecs")) + object.idlesecs = message.idlesecs; + if (message.strong_certificate != null && $Object.hasOwnProperty.call(message, "strong_certificate")) + object.strong_certificate = message.strong_certificate; + if (message.opus != null && $Object.hasOwnProperty.call(message, "opus")) + object.opus = message.opus; + if (message.rolling_stats != null && $Object.hasOwnProperty.call(message, "rolling_stats")) + object.rolling_stats = $root.MumbleProto.UserStats.RollingStats.toObject(message.rolling_stats, options, _depth + 1); + return object; + }; + + UserStats.prototype.toJSON = function() { + return UserStats.toObject(this, $protobuf.util.toJSONOptions); + }; + + UserStats.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UserStats"; + }; + + UserStats.Stats = (function() { + + const Stats = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + Stats.prototype.good = 0; + Stats.prototype.late = 0; + Stats.prototype.lost = 0; + Stats.prototype.resync = 0; + + Stats.create = function(properties) { + return new Stats(properties); + }; + + Stats.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.good != null && $Object.hasOwnProperty.call(message, "good")) + writer.uint32(8).uint32(message.good); + if (message.late != null && $Object.hasOwnProperty.call(message, "late")) + writer.uint32(16).uint32(message.late); + if (message.lost != null && $Object.hasOwnProperty.call(message, "lost")) + writer.uint32(24).uint32(message.lost); + if (message.resync != null && $Object.hasOwnProperty.call(message, "resync")) + writer.uint32(32).uint32(message.resync); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + Stats.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UserStats.Stats(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.good = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.late = reader.uint32(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.lost = reader.uint32(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.resync = reader.uint32(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + Stats.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.good != null && $Object.hasOwnProperty.call(message, "good")) + if (!$util.isInteger(message.good)) + return "good: integer expected"; + if (message.late != null && $Object.hasOwnProperty.call(message, "late")) + if (!$util.isInteger(message.late)) + return "late: integer expected"; + if (message.lost != null && $Object.hasOwnProperty.call(message, "lost")) + if (!$util.isInteger(message.lost)) + return "lost: integer expected"; + if (message.resync != null && $Object.hasOwnProperty.call(message, "resync")) + if (!$util.isInteger(message.resync)) + return "resync: integer expected"; + return null; + }; + + Stats.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UserStats.Stats) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UserStats.Stats: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UserStats.Stats(); + if (object.good != null) + message.good = object.good >>> 0; + if (object.late != null) + message.late = object.late >>> 0; + if (object.lost != null) + message.lost = object.lost >>> 0; + if (object.resync != null) + message.resync = object.resync >>> 0; + return message; + }; + + Stats.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.good = 0; + object.late = 0; + object.lost = 0; + object.resync = 0; + } + if (message.good != null && $Object.hasOwnProperty.call(message, "good")) + object.good = message.good; + if (message.late != null && $Object.hasOwnProperty.call(message, "late")) + object.late = message.late; + if (message.lost != null && $Object.hasOwnProperty.call(message, "lost")) + object.lost = message.lost; + if (message.resync != null && $Object.hasOwnProperty.call(message, "resync")) + object.resync = message.resync; + return object; + }; + + Stats.prototype.toJSON = function() { + return Stats.toObject(this, $protobuf.util.toJSONOptions); + }; + + Stats.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UserStats.Stats"; + }; + + return Stats; + })(); + + UserStats.RollingStats = (function() { + + const RollingStats = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + RollingStats.prototype.time_window = 0; + RollingStats.prototype.from_client = null; + RollingStats.prototype.from_server = null; + + RollingStats.create = function(properties) { + return new RollingStats(properties); + }; + + RollingStats.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.time_window != null && $Object.hasOwnProperty.call(message, "time_window")) + writer.uint32(8).uint32(message.time_window); + if (message.from_client != null && $Object.hasOwnProperty.call(message, "from_client")) + $root.MumbleProto.UserStats.Stats.encode(message.from_client, writer.uint32(18).fork(), _depth + 1).ldelim(); + if (message.from_server != null && $Object.hasOwnProperty.call(message, "from_server")) + $root.MumbleProto.UserStats.Stats.encode(message.from_server, writer.uint32(26).fork(), _depth + 1).ldelim(); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + RollingStats.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.UserStats.RollingStats(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.time_window = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 2) + break; + message.from_client = $root.MumbleProto.UserStats.Stats.decode(reader, reader.uint32(), $undefined, _depth + 1, message.from_client); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.from_server = $root.MumbleProto.UserStats.Stats.decode(reader, reader.uint32(), $undefined, _depth + 1, message.from_server); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + RollingStats.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.time_window != null && $Object.hasOwnProperty.call(message, "time_window")) + if (!$util.isInteger(message.time_window)) + return "time_window: integer expected"; + if (message.from_client != null && $Object.hasOwnProperty.call(message, "from_client")) { + let error = $root.MumbleProto.UserStats.Stats.verify(message.from_client, _depth + 1); + if (error) + return "from_client." + error; + } + if (message.from_server != null && $Object.hasOwnProperty.call(message, "from_server")) { + let error = $root.MumbleProto.UserStats.Stats.verify(message.from_server, _depth + 1); + if (error) + return "from_server." + error; + } + return null; + }; + + RollingStats.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.UserStats.RollingStats) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.UserStats.RollingStats: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.UserStats.RollingStats(); + if (object.time_window != null) + message.time_window = object.time_window >>> 0; + if (object.from_client != null) { + if (!$util.isObject(object.from_client)) + throw $TypeError(".MumbleProto.UserStats.RollingStats.from_client: object expected"); + message.from_client = $root.MumbleProto.UserStats.Stats.fromObject(object.from_client, _depth + 1); + } + if (object.from_server != null) { + if (!$util.isObject(object.from_server)) + throw $TypeError(".MumbleProto.UserStats.RollingStats.from_server: object expected"); + message.from_server = $root.MumbleProto.UserStats.Stats.fromObject(object.from_server, _depth + 1); + } + return message; + }; + + RollingStats.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.time_window = 0; + object.from_client = null; + object.from_server = null; + } + if (message.time_window != null && $Object.hasOwnProperty.call(message, "time_window")) + object.time_window = message.time_window; + if (message.from_client != null && $Object.hasOwnProperty.call(message, "from_client")) + object.from_client = $root.MumbleProto.UserStats.Stats.toObject(message.from_client, options, _depth + 1); + if (message.from_server != null && $Object.hasOwnProperty.call(message, "from_server")) + object.from_server = $root.MumbleProto.UserStats.Stats.toObject(message.from_server, options, _depth + 1); + return object; + }; + + RollingStats.prototype.toJSON = function() { + return RollingStats.toObject(this, $protobuf.util.toJSONOptions); + }; + + RollingStats.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.UserStats.RollingStats"; + }; + + return RollingStats; + })(); + + return UserStats; + })(); + + MumbleProto.RequestBlob = (function() { + + const RequestBlob = function (properties) { + this.session_texture = []; + this.session_comment = []; + this.channel_description = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + RequestBlob.prototype.session_texture = $util.emptyArray; + RequestBlob.prototype.session_comment = $util.emptyArray; + RequestBlob.prototype.channel_description = $util.emptyArray; + + RequestBlob.create = function(properties) { + return new RequestBlob(properties); + }; + + RequestBlob.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.session_texture != null && message.session_texture.length) + for (let i = 0; i < message.session_texture.length; ++i) + writer.uint32(8).uint32(message.session_texture[i]); + if (message.session_comment != null && message.session_comment.length) + for (let i = 0; i < message.session_comment.length; ++i) + writer.uint32(16).uint32(message.session_comment[i]); + if (message.channel_description != null && message.channel_description.length) + for (let i = 0; i < message.channel_description.length; ++i) + writer.uint32(24).uint32(message.channel_description[i]); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + RequestBlob.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.RequestBlob(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType === 2) { + if (!(message.session_texture && message.session_texture.length)) + message.session_texture = []; + reader.uint32s(message.session_texture); + continue; + } + if (wireType !== 0) + break; + if (!(message.session_texture && message.session_texture.length)) + message.session_texture = []; + message.session_texture.push(reader.uint32()); + continue; + } + case 2: { + if (wireType === 2) { + if (!(message.session_comment && message.session_comment.length)) + message.session_comment = []; + reader.uint32s(message.session_comment); + continue; + } + if (wireType !== 0) + break; + if (!(message.session_comment && message.session_comment.length)) + message.session_comment = []; + message.session_comment.push(reader.uint32()); + continue; + } + case 3: { + if (wireType === 2) { + if (!(message.channel_description && message.channel_description.length)) + message.channel_description = []; + reader.uint32s(message.channel_description); + continue; + } + if (wireType !== 0) + break; + if (!(message.channel_description && message.channel_description.length)) + message.channel_description = []; + message.channel_description.push(reader.uint32()); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + RequestBlob.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.session_texture != null && $Object.hasOwnProperty.call(message, "session_texture")) { + if (!$Array.isArray(message.session_texture)) + return "session_texture: array expected"; + for (let i = 0; i < message.session_texture.length; ++i) + if (!$util.isInteger(message.session_texture[i])) + return "session_texture: integer[] expected"; + } + if (message.session_comment != null && $Object.hasOwnProperty.call(message, "session_comment")) { + if (!$Array.isArray(message.session_comment)) + return "session_comment: array expected"; + for (let i = 0; i < message.session_comment.length; ++i) + if (!$util.isInteger(message.session_comment[i])) + return "session_comment: integer[] expected"; + } + if (message.channel_description != null && $Object.hasOwnProperty.call(message, "channel_description")) { + if (!$Array.isArray(message.channel_description)) + return "channel_description: array expected"; + for (let i = 0; i < message.channel_description.length; ++i) + if (!$util.isInteger(message.channel_description[i])) + return "channel_description: integer[] expected"; + } + return null; + }; + + RequestBlob.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.RequestBlob) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.RequestBlob: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.RequestBlob(); + if (object.session_texture) { + if (!$Array.isArray(object.session_texture)) + throw $TypeError(".MumbleProto.RequestBlob.session_texture: array expected"); + message.session_texture = $Array(object.session_texture.length); + for (let i = 0; i < object.session_texture.length; ++i) + message.session_texture[i] = object.session_texture[i] >>> 0; + } + if (object.session_comment) { + if (!$Array.isArray(object.session_comment)) + throw $TypeError(".MumbleProto.RequestBlob.session_comment: array expected"); + message.session_comment = $Array(object.session_comment.length); + for (let i = 0; i < object.session_comment.length; ++i) + message.session_comment[i] = object.session_comment[i] >>> 0; + } + if (object.channel_description) { + if (!$Array.isArray(object.channel_description)) + throw $TypeError(".MumbleProto.RequestBlob.channel_description: array expected"); + message.channel_description = $Array(object.channel_description.length); + for (let i = 0; i < object.channel_description.length; ++i) + message.channel_description[i] = object.channel_description[i] >>> 0; + } + return message; + }; + + RequestBlob.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) { + object.session_texture = []; + object.session_comment = []; + object.channel_description = []; + } + if (message.session_texture && message.session_texture.length) { + object.session_texture = $Array(message.session_texture.length); + for (let j = 0; j < message.session_texture.length; ++j) + object.session_texture[j] = message.session_texture[j]; + } + if (message.session_comment && message.session_comment.length) { + object.session_comment = $Array(message.session_comment.length); + for (let j = 0; j < message.session_comment.length; ++j) + object.session_comment[j] = message.session_comment[j]; + } + if (message.channel_description && message.channel_description.length) { + object.channel_description = $Array(message.channel_description.length); + for (let j = 0; j < message.channel_description.length; ++j) + object.channel_description[j] = message.channel_description[j]; + } + return object; + }; + + RequestBlob.prototype.toJSON = function() { + return RequestBlob.toObject(this, $protobuf.util.toJSONOptions); + }; + + RequestBlob.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.RequestBlob"; + }; + + return RequestBlob; + })(); + + MumbleProto.ServerConfig = (function() { + + const ServerConfig = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + ServerConfig.prototype.max_bandwidth = 0; + ServerConfig.prototype.welcome_text = ""; + ServerConfig.prototype.allow_html = false; + ServerConfig.prototype.message_length = 0; + ServerConfig.prototype.image_message_length = 0; + ServerConfig.prototype.max_users = 0; + ServerConfig.prototype.recording_allowed = false; + + ServerConfig.create = function(properties) { + return new ServerConfig(properties); + }; + + ServerConfig.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.max_bandwidth != null && $Object.hasOwnProperty.call(message, "max_bandwidth")) + writer.uint32(8).uint32(message.max_bandwidth); + if (message.welcome_text != null && $Object.hasOwnProperty.call(message, "welcome_text")) + writer.uint32(18).string(message.welcome_text); + if (message.allow_html != null && $Object.hasOwnProperty.call(message, "allow_html")) + writer.uint32(24).bool(message.allow_html); + if (message.message_length != null && $Object.hasOwnProperty.call(message, "message_length")) + writer.uint32(32).uint32(message.message_length); + if (message.image_message_length != null && $Object.hasOwnProperty.call(message, "image_message_length")) + writer.uint32(40).uint32(message.image_message_length); + if (message.max_users != null && $Object.hasOwnProperty.call(message, "max_users")) + writer.uint32(48).uint32(message.max_users); + if (message.recording_allowed != null && $Object.hasOwnProperty.call(message, "recording_allowed")) + writer.uint32(56).bool(message.recording_allowed); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + ServerConfig.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.ServerConfig(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.max_bandwidth = reader.uint32(); + continue; + } + case 2: { + if (wireType !== 2) + break; + message.welcome_text = reader.string(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.allow_html = reader.bool(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.message_length = reader.uint32(); + continue; + } + case 5: { + if (wireType !== 0) + break; + message.image_message_length = reader.uint32(); + continue; + } + case 6: { + if (wireType !== 0) + break; + message.max_users = reader.uint32(); + continue; + } + case 7: { + if (wireType !== 0) + break; + message.recording_allowed = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + ServerConfig.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.max_bandwidth != null && $Object.hasOwnProperty.call(message, "max_bandwidth")) + if (!$util.isInteger(message.max_bandwidth)) + return "max_bandwidth: integer expected"; + if (message.welcome_text != null && $Object.hasOwnProperty.call(message, "welcome_text")) + if (!$util.isString(message.welcome_text)) + return "welcome_text: string expected"; + if (message.allow_html != null && $Object.hasOwnProperty.call(message, "allow_html")) + if (typeof message.allow_html !== "boolean") + return "allow_html: boolean expected"; + if (message.message_length != null && $Object.hasOwnProperty.call(message, "message_length")) + if (!$util.isInteger(message.message_length)) + return "message_length: integer expected"; + if (message.image_message_length != null && $Object.hasOwnProperty.call(message, "image_message_length")) + if (!$util.isInteger(message.image_message_length)) + return "image_message_length: integer expected"; + if (message.max_users != null && $Object.hasOwnProperty.call(message, "max_users")) + if (!$util.isInteger(message.max_users)) + return "max_users: integer expected"; + if (message.recording_allowed != null && $Object.hasOwnProperty.call(message, "recording_allowed")) + if (typeof message.recording_allowed !== "boolean") + return "recording_allowed: boolean expected"; + return null; + }; + + ServerConfig.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.ServerConfig) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.ServerConfig: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.ServerConfig(); + if (object.max_bandwidth != null) + message.max_bandwidth = object.max_bandwidth >>> 0; + if (object.welcome_text != null) + message.welcome_text = $String(object.welcome_text); + if (object.allow_html != null) + message.allow_html = $Boolean(object.allow_html); + if (object.message_length != null) + message.message_length = object.message_length >>> 0; + if (object.image_message_length != null) + message.image_message_length = object.image_message_length >>> 0; + if (object.max_users != null) + message.max_users = object.max_users >>> 0; + if (object.recording_allowed != null) + message.recording_allowed = $Boolean(object.recording_allowed); + return message; + }; + + ServerConfig.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.max_bandwidth = 0; + object.welcome_text = ""; + object.allow_html = false; + object.message_length = 0; + object.image_message_length = 0; + object.max_users = 0; + object.recording_allowed = false; + } + if (message.max_bandwidth != null && $Object.hasOwnProperty.call(message, "max_bandwidth")) + object.max_bandwidth = message.max_bandwidth; + if (message.welcome_text != null && $Object.hasOwnProperty.call(message, "welcome_text")) + object.welcome_text = message.welcome_text; + if (message.allow_html != null && $Object.hasOwnProperty.call(message, "allow_html")) + object.allow_html = message.allow_html; + if (message.message_length != null && $Object.hasOwnProperty.call(message, "message_length")) + object.message_length = message.message_length; + if (message.image_message_length != null && $Object.hasOwnProperty.call(message, "image_message_length")) + object.image_message_length = message.image_message_length; + if (message.max_users != null && $Object.hasOwnProperty.call(message, "max_users")) + object.max_users = message.max_users; + if (message.recording_allowed != null && $Object.hasOwnProperty.call(message, "recording_allowed")) + object.recording_allowed = message.recording_allowed; + return object; + }; + + ServerConfig.prototype.toJSON = function() { + return ServerConfig.toObject(this, $protobuf.util.toJSONOptions); + }; + + ServerConfig.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.ServerConfig"; + }; + + return ServerConfig; + })(); + + MumbleProto.SuggestConfig = (function() { + + const SuggestConfig = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + SuggestConfig.prototype.version_v1 = 0; + SuggestConfig.prototype.version_v2 = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + SuggestConfig.prototype.positional = false; + SuggestConfig.prototype.push_to_talk = false; + + SuggestConfig.create = function(properties) { + return new SuggestConfig(properties); + }; + + SuggestConfig.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.version_v1 != null && $Object.hasOwnProperty.call(message, "version_v1")) + writer.uint32(8).uint32(message.version_v1); + if (message.positional != null && $Object.hasOwnProperty.call(message, "positional")) + writer.uint32(16).bool(message.positional); + if (message.push_to_talk != null && $Object.hasOwnProperty.call(message, "push_to_talk")) + writer.uint32(24).bool(message.push_to_talk); + if (message.version_v2 != null && $Object.hasOwnProperty.call(message, "version_v2")) + writer.uint32(32).uint64(message.version_v2); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + SuggestConfig.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.SuggestConfig(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.version_v1 = reader.uint32(); + continue; + } + case 4: { + if (wireType !== 0) + break; + message.version_v2 = reader.uint64(); + continue; + } + case 2: { + if (wireType !== 0) + break; + message.positional = reader.bool(); + continue; + } + case 3: { + if (wireType !== 0) + break; + message.push_to_talk = reader.bool(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + SuggestConfig.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.version_v1 != null && $Object.hasOwnProperty.call(message, "version_v1")) + if (!$util.isInteger(message.version_v1)) + return "version_v1: integer expected"; + if (message.version_v2 != null && $Object.hasOwnProperty.call(message, "version_v2")) + if (!$util.isInteger(message.version_v2) && !(message.version_v2 && $util.isInteger(message.version_v2.low) && $util.isInteger(message.version_v2.high))) + return "version_v2: integer|Long expected"; + if (message.positional != null && $Object.hasOwnProperty.call(message, "positional")) + if (typeof message.positional !== "boolean") + return "positional: boolean expected"; + if (message.push_to_talk != null && $Object.hasOwnProperty.call(message, "push_to_talk")) + if (typeof message.push_to_talk !== "boolean") + return "push_to_talk: boolean expected"; + return null; + }; + + SuggestConfig.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.SuggestConfig) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.SuggestConfig: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.SuggestConfig(); + if (object.version_v1 != null) + message.version_v1 = object.version_v1 >>> 0; + if (object.version_v2 != null) + if ($util.Long) + message.version_v2 = $util.Long.fromValue(object.version_v2, true); + else if (typeof object.version_v2 === "string") + message.version_v2 = $parseInt(object.version_v2, 10); + else if (typeof object.version_v2 === "number") + message.version_v2 = object.version_v2; + else if (typeof object.version_v2 === "object") + message.version_v2 = new $util.LongBits(object.version_v2.low >>> 0, object.version_v2.high >>> 0).toNumber(true); + if (object.positional != null) + message.positional = $Boolean(object.positional); + if (object.push_to_talk != null) + message.push_to_talk = $Boolean(object.push_to_talk); + return message; + }; + + SuggestConfig.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + object.version_v1 = 0; + object.positional = false; + object.push_to_talk = false; + if ($util.Long) { + let long = new $util.Long(0, 0, true); + object.version_v2 = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long; + } else + object.version_v2 = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0; + } + if (message.version_v1 != null && $Object.hasOwnProperty.call(message, "version_v1")) + object.version_v1 = message.version_v1; + if (message.positional != null && $Object.hasOwnProperty.call(message, "positional")) + object.positional = message.positional; + if (message.push_to_talk != null && $Object.hasOwnProperty.call(message, "push_to_talk")) + object.push_to_talk = message.push_to_talk; + if (message.version_v2 != null && $Object.hasOwnProperty.call(message, "version_v2")) + if (typeof $BigInt !== "undefined" && options.longs === $BigInt) + object.version_v2 = typeof message.version_v2 === "number" ? $BigInt(message.version_v2) : $util.Long.fromBits(message.version_v2.low >>> 0, message.version_v2.high >>> 0, true).toBigInt(); + else if (typeof message.version_v2 === "number") + object.version_v2 = options.longs === $String ? $String(message.version_v2) : message.version_v2; + else + object.version_v2 = options.longs === $String ? $util.Long.prototype.toString.call(message.version_v2) : options.longs === $Number ? new $util.LongBits(message.version_v2.low >>> 0, message.version_v2.high >>> 0).toNumber(true) : message.version_v2; + return object; + }; + + SuggestConfig.prototype.toJSON = function() { + return SuggestConfig.toObject(this, $protobuf.util.toJSONOptions); + }; + + SuggestConfig.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.SuggestConfig"; + }; + + return SuggestConfig; + })(); + + MumbleProto.PluginDataTransmission = (function() { + + const PluginDataTransmission = function (properties) { + this.receiverSessions = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + PluginDataTransmission.prototype.senderSession = 0; + PluginDataTransmission.prototype.receiverSessions = $util.emptyArray; + PluginDataTransmission.prototype.data = $util.newBuffer([]); + PluginDataTransmission.prototype.dataID = ""; + + PluginDataTransmission.create = function(properties) { + return new PluginDataTransmission(properties); + }; + + PluginDataTransmission.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.senderSession != null && $Object.hasOwnProperty.call(message, "senderSession")) + writer.uint32(8).uint32(message.senderSession); + if (message.receiverSessions != null && message.receiverSessions.length) + writer.uint32(18).uint32s(message.receiverSessions); + if (message.data != null && $Object.hasOwnProperty.call(message, "data")) + writer.uint32(26).bytes(message.data); + if (message.dataID != null && $Object.hasOwnProperty.call(message, "dataID")) + writer.uint32(34).string(message.dataID); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + PluginDataTransmission.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleProto.PluginDataTransmission(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.senderSession = reader.uint32(); + continue; + } + case 2: { + if (wireType === 2) { + if (!(message.receiverSessions && message.receiverSessions.length)) + message.receiverSessions = []; + reader.uint32s(message.receiverSessions); + continue; + } + if (wireType !== 0) + break; + if (!(message.receiverSessions && message.receiverSessions.length)) + message.receiverSessions = []; + message.receiverSessions.push(reader.uint32()); + continue; + } + case 3: { + if (wireType !== 2) + break; + message.data = reader.bytes(); + continue; + } + case 4: { + if (wireType !== 2) + break; + message.dataID = reader.string(); + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + PluginDataTransmission.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.senderSession != null && $Object.hasOwnProperty.call(message, "senderSession")) + if (!$util.isInteger(message.senderSession)) + return "senderSession: integer expected"; + if (message.receiverSessions != null && $Object.hasOwnProperty.call(message, "receiverSessions")) { + if (!$Array.isArray(message.receiverSessions)) + return "receiverSessions: array expected"; + for (let i = 0; i < message.receiverSessions.length; ++i) + if (!$util.isInteger(message.receiverSessions[i])) + return "receiverSessions: integer[] expected"; + } + if (message.data != null && $Object.hasOwnProperty.call(message, "data")) + if (!(message.data && typeof message.data.length === "number" || $util.isString(message.data))) + return "data: buffer expected"; + if (message.dataID != null && $Object.hasOwnProperty.call(message, "dataID")) + if (!$util.isString(message.dataID)) + return "dataID: string expected"; + return null; + }; + + PluginDataTransmission.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleProto.PluginDataTransmission) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleProto.PluginDataTransmission: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleProto.PluginDataTransmission(); + if (object.senderSession != null) + message.senderSession = object.senderSession >>> 0; + if (object.receiverSessions) { + if (!$Array.isArray(object.receiverSessions)) + throw $TypeError(".MumbleProto.PluginDataTransmission.receiverSessions: array expected"); + message.receiverSessions = $Array(object.receiverSessions.length); + for (let i = 0; i < object.receiverSessions.length; ++i) + message.receiverSessions[i] = object.receiverSessions[i] >>> 0; + } + if (object.data != null) + if (typeof object.data === "string") + $util.base64.decode(object.data, message.data = $util.newBuffer($util.base64.length(object.data)), 0); + else if (object.data.length >= 0) + message.data = object.data; + if (object.dataID != null) + message.dataID = $String(object.dataID); + return message; + }; + + PluginDataTransmission.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.receiverSessions = []; + if (options.defaults) { + object.senderSession = 0; + if (options.bytes === $String) + object.data = ""; + else { + object.data = []; + if (options.bytes !== $Array) + object.data = $util.newBuffer(object.data); + } + object.dataID = ""; + } + if (message.senderSession != null && $Object.hasOwnProperty.call(message, "senderSession")) + object.senderSession = message.senderSession; + if (message.receiverSessions && message.receiverSessions.length) { + object.receiverSessions = $Array(message.receiverSessions.length); + for (let j = 0; j < message.receiverSessions.length; ++j) + object.receiverSessions[j] = message.receiverSessions[j]; + } + if (message.data != null && $Object.hasOwnProperty.call(message, "data")) + object.data = options.bytes === $String ? $util.base64.encode(message.data, 0, message.data.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.data) : message.data; + if (message.dataID != null && $Object.hasOwnProperty.call(message, "dataID")) + object.dataID = message.dataID; + return object; + }; + + PluginDataTransmission.prototype.toJSON = function() { + return PluginDataTransmission.toObject(this, $protobuf.util.toJSONOptions); + }; + + PluginDataTransmission.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleProto.PluginDataTransmission"; + }; + + return PluginDataTransmission; + })(); + + return MumbleProto; +})(); + +export { + $root as default +}; diff --git a/src/core/mumble-udp-pb.d.ts b/src/core/mumble-udp-pb.d.ts new file mode 100644 index 0000000..2ec8176 --- /dev/null +++ b/src/core/mumble-udp-pb.d.ts @@ -0,0 +1 @@ +export const MumbleUDP: Record; diff --git a/src/core/mumble-udp-pb.js b/src/core/mumble-udp-pb.js new file mode 100644 index 0000000..9f34779 --- /dev/null +++ b/src/core/mumble-udp-pb.js @@ -0,0 +1,639 @@ +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-mixed-operators, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars, default-case, jsdoc/require-param*/ +import $protobuf from "protobufjs/minimal.js"; + +const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; +const $Object = $util.global.Object, $undefined = $util.global.undefined, $Error = $util.global.Error, $RangeError = $util.global.RangeError, $Array = $util.global.Array, $TypeError = $util.global.TypeError, $Number = $util.global.Number, $parseInt = $util.global.parseInt, $Boolean = $util.global.Boolean, $String = $util.global.String, $BigInt = $util.global.BigInt, $isFinite = $util.global.isFinite; + +const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); + +export const MumbleUDP = $root.MumbleUDP = (() => { + + const MumbleUDP = {}; + + MumbleUDP.Audio = (function() { + + const Audio = function (properties) { + this.positional_data = []; + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + Audio.prototype.target = null; + Audio.prototype.context = null; + Audio.prototype.sender_session = 0; + Audio.prototype.frame_number = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + Audio.prototype.opus_data = $util.newBuffer([]); + Audio.prototype.positional_data = $util.emptyArray; + Audio.prototype.volume_adjustment = 0; + Audio.prototype.is_terminator = false; + + let $oneOfFields; + + $Object.defineProperty(Audio.prototype, "Header", { + get: $util.oneOfGetter($oneOfFields = ["target", "context"]), + set: $util.oneOfSetter($oneOfFields) + }); + + Audio.create = function(properties) { + return new Audio(properties); + }; + + Audio.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.target != null && $Object.hasOwnProperty.call(message, "target")) + writer.uint32(8).uint32(message.target); + if (message.context != null && $Object.hasOwnProperty.call(message, "context")) + writer.uint32(16).uint32(message.context); + if (message.sender_session != null && $Object.hasOwnProperty.call(message, "sender_session") && message.sender_session !== 0) + writer.uint32(24).uint32(message.sender_session); + if (message.frame_number != null && $Object.hasOwnProperty.call(message, "frame_number") && (typeof message.frame_number === "object" ? message.frame_number.low || message.frame_number.high : message.frame_number !== 0)) + writer.uint32(32).uint64(message.frame_number); + if (message.opus_data != null && $Object.hasOwnProperty.call(message, "opus_data") && message.opus_data.length) + writer.uint32(42).bytes(message.opus_data); + if (message.positional_data != null && message.positional_data.length) + writer.uint32(50).floats(message.positional_data); + if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment") && !$Object.is(message.volume_adjustment, 0)) + writer.uint32(61).float(message.volume_adjustment); + if (message.is_terminator != null && $Object.hasOwnProperty.call(message, "is_terminator") && message.is_terminator !== false) + writer.uint32(128).bool(message.is_terminator); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + Audio.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message, value; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleUDP.Audio(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + message.target = reader.uint32(); + message.Header = "target"; + continue; + } + case 2: { + if (wireType !== 0) + break; + message.context = reader.uint32(); + message.Header = "context"; + continue; + } + case 3: { + if (wireType !== 0) + break; + if (value = reader.uint32()) + message.sender_session = value; + else + delete message.sender_session; + continue; + } + case 4: { + if (wireType !== 0) + break; + if (typeof (value = reader.uint64()) === "object" ? value.low || value.high : value !== 0) + message.frame_number = value; + else + delete message.frame_number; + continue; + } + case 5: { + if (wireType !== 2) + break; + if ((value = reader.bytes()).length) + message.opus_data = value; + else + delete message.opus_data; + continue; + } + case 6: { + if (wireType === 2) { + if (!(message.positional_data && message.positional_data.length)) + message.positional_data = []; + reader.floats(message.positional_data); + continue; + } + if (wireType !== 5) + break; + if (!(message.positional_data && message.positional_data.length)) + message.positional_data = []; + message.positional_data.push(reader.float()); + continue; + } + case 7: { + if (wireType !== 5) + break; + if (!$Object.is(value = reader.float(), 0)) + message.volume_adjustment = value; + else + delete message.volume_adjustment; + continue; + } + case 16: { + if (wireType !== 0) + break; + if (value = reader.bool()) + message.is_terminator = value; + else + delete message.is_terminator; + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + Audio.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + let properties = {}; + if (message.target != null && $Object.hasOwnProperty.call(message, "target")) { + properties.Header = 1; + if (!$util.isInteger(message.target)) + return "target: integer expected"; + } + if (message.context != null && $Object.hasOwnProperty.call(message, "context")) { + if (properties.Header === 1) + return "Header: multiple values"; + properties.Header = 1; + if (!$util.isInteger(message.context)) + return "context: integer expected"; + } + if (message.sender_session != null && $Object.hasOwnProperty.call(message, "sender_session")) + if (!$util.isInteger(message.sender_session)) + return "sender_session: integer expected"; + if (message.frame_number != null && $Object.hasOwnProperty.call(message, "frame_number")) + if (!$util.isInteger(message.frame_number) && !(message.frame_number && $util.isInteger(message.frame_number.low) && $util.isInteger(message.frame_number.high))) + return "frame_number: integer|Long expected"; + if (message.opus_data != null && $Object.hasOwnProperty.call(message, "opus_data")) + if (!(message.opus_data && typeof message.opus_data.length === "number" || $util.isString(message.opus_data))) + return "opus_data: buffer expected"; + if (message.positional_data != null && $Object.hasOwnProperty.call(message, "positional_data")) { + if (!$Array.isArray(message.positional_data)) + return "positional_data: array expected"; + for (let i = 0; i < message.positional_data.length; ++i) + if (typeof message.positional_data[i] !== "number") + return "positional_data: number[] expected"; + } + if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment")) + if (typeof message.volume_adjustment !== "number") + return "volume_adjustment: number expected"; + if (message.is_terminator != null && $Object.hasOwnProperty.call(message, "is_terminator")) + if (typeof message.is_terminator !== "boolean") + return "is_terminator: boolean expected"; + return null; + }; + + Audio.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleUDP.Audio) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleUDP.Audio: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleUDP.Audio(); + if (object.target != null) + message.target = object.target >>> 0; + if (object.context != null) + message.context = object.context >>> 0; + if (object.sender_session != null) + if ($Number(object.sender_session) !== 0) + message.sender_session = object.sender_session >>> 0; + if (object.frame_number != null) + if (typeof object.frame_number === "object" ? object.frame_number.low || object.frame_number.high : $Number(object.frame_number) !== 0) + if ($util.Long) + message.frame_number = $util.Long.fromValue(object.frame_number, true); + else if (typeof object.frame_number === "string") + message.frame_number = $parseInt(object.frame_number, 10); + else if (typeof object.frame_number === "number") + message.frame_number = object.frame_number; + else if (typeof object.frame_number === "object") + message.frame_number = new $util.LongBits(object.frame_number.low >>> 0, object.frame_number.high >>> 0).toNumber(true); + if (object.opus_data != null) + if (object.opus_data.length) + if (typeof object.opus_data === "string") + $util.base64.decode(object.opus_data, message.opus_data = $util.newBuffer($util.base64.length(object.opus_data)), 0); + else if (object.opus_data.length >= 0) + message.opus_data = object.opus_data; + if (object.positional_data) { + if (!$Array.isArray(object.positional_data)) + throw $TypeError(".MumbleUDP.Audio.positional_data: array expected"); + message.positional_data = $Array(object.positional_data.length); + for (let i = 0; i < object.positional_data.length; ++i) + message.positional_data[i] = $Number(object.positional_data[i]); + } + if (object.volume_adjustment != null) + if (!$Object.is($Number(object.volume_adjustment), 0)) + message.volume_adjustment = $Number(object.volume_adjustment); + if (object.is_terminator != null) + if (object.is_terminator) + message.is_terminator = $Boolean(object.is_terminator); + return message; + }; + + Audio.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.arrays || options.defaults) + object.positional_data = []; + if (options.defaults) { + object.sender_session = 0; + if ($util.Long) { + let long = new $util.Long(0, 0, true); + object.frame_number = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long; + } else + object.frame_number = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0; + if (options.bytes === $String) + object.opus_data = ""; + else { + object.opus_data = []; + if (options.bytes !== $Array) + object.opus_data = $util.newBuffer(object.opus_data); + } + object.volume_adjustment = 0; + object.is_terminator = false; + } + if (message.target != null && $Object.hasOwnProperty.call(message, "target")) { + object.target = message.target; + if (options.oneofs) + object.Header = "target"; + } + if (message.context != null && $Object.hasOwnProperty.call(message, "context")) { + object.context = message.context; + if (options.oneofs) + object.Header = "context"; + } + if (message.sender_session != null && $Object.hasOwnProperty.call(message, "sender_session")) + object.sender_session = message.sender_session; + if (message.frame_number != null && $Object.hasOwnProperty.call(message, "frame_number")) + if (typeof $BigInt !== "undefined" && options.longs === $BigInt) + object.frame_number = typeof message.frame_number === "number" ? $BigInt(message.frame_number) : $util.Long.fromBits(message.frame_number.low >>> 0, message.frame_number.high >>> 0, true).toBigInt(); + else if (typeof message.frame_number === "number") + object.frame_number = options.longs === $String ? $String(message.frame_number) : message.frame_number; + else + object.frame_number = options.longs === $String ? $util.Long.prototype.toString.call(message.frame_number) : options.longs === $Number ? new $util.LongBits(message.frame_number.low >>> 0, message.frame_number.high >>> 0).toNumber(true) : message.frame_number; + if (message.opus_data != null && $Object.hasOwnProperty.call(message, "opus_data")) + object.opus_data = options.bytes === $String ? $util.base64.encode(message.opus_data, 0, message.opus_data.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.opus_data) : message.opus_data; + if (message.positional_data && message.positional_data.length) { + object.positional_data = $Array(message.positional_data.length); + for (let j = 0; j < message.positional_data.length; ++j) + object.positional_data[j] = options.json && !$isFinite(message.positional_data[j]) ? $String(message.positional_data[j]) : message.positional_data[j]; + } + if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment")) + object.volume_adjustment = options.json && !$isFinite(message.volume_adjustment) ? $String(message.volume_adjustment) : message.volume_adjustment; + if (message.is_terminator != null && $Object.hasOwnProperty.call(message, "is_terminator")) + object.is_terminator = message.is_terminator; + return object; + }; + + Audio.prototype.toJSON = function() { + return Audio.toObject(this, $protobuf.util.toJSONOptions); + }; + + Audio.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleUDP.Audio"; + }; + + return Audio; + })(); + + MumbleUDP.Ping = (function() { + + const Ping = function (properties) { + if (properties) + for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null && keys[i] !== "__proto__") + this[keys[i]] = properties[keys[i]]; + }; + + Ping.prototype.timestamp = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + Ping.prototype.request_extended_information = false; + Ping.prototype.server_version_v2 = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + Ping.prototype.user_count = 0; + Ping.prototype.max_user_count = 0; + Ping.prototype.max_bandwidth_per_user = 0; + + Ping.create = function(properties) { + return new Ping(properties); + }; + + Ping.encode = function (message, writer, _depth) { + if (!writer) + writer = $Writer.create(); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp") && (typeof message.timestamp === "object" ? message.timestamp.low || message.timestamp.high : message.timestamp !== 0)) + writer.uint32(8).uint64(message.timestamp); + if (message.request_extended_information != null && $Object.hasOwnProperty.call(message, "request_extended_information") && message.request_extended_information !== false) + writer.uint32(16).bool(message.request_extended_information); + if (message.server_version_v2 != null && $Object.hasOwnProperty.call(message, "server_version_v2") && (typeof message.server_version_v2 === "object" ? message.server_version_v2.low || message.server_version_v2.high : message.server_version_v2 !== 0)) + writer.uint32(24).uint64(message.server_version_v2); + if (message.user_count != null && $Object.hasOwnProperty.call(message, "user_count") && message.user_count !== 0) + writer.uint32(32).uint32(message.user_count); + if (message.max_user_count != null && $Object.hasOwnProperty.call(message, "max_user_count") && message.max_user_count !== 0) + writer.uint32(40).uint32(message.max_user_count); + if (message.max_bandwidth_per_user != null && $Object.hasOwnProperty.call(message, "max_bandwidth_per_user") && message.max_bandwidth_per_user !== 0) + writer.uint32(48).uint32(message.max_bandwidth_per_user); + if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns")) + for (let i = 0; i < message.$unknowns.length; ++i) + writer.raw(message.$unknowns[i]); + return writer; + }; + + Ping.decode = function (reader, length, _end, _depth, _target) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + if (_depth === $undefined) + _depth = 0; + if (_depth > $Reader.recursionLimit) + throw $Error("max depth exceeded"); + let end, message, value; + if (length === $undefined) + end = reader.len; + else { + end = reader.pos + length; + if (end > reader.len) + throw $RangeError("index out of range"); + length = reader.len; + reader.len = end; + } + message = _target || new $root.MumbleUDP.Ping(); + while (reader.pos < end) { + let start = reader.pos; + let tag = reader.tag(); + if (tag === _end) { + _end = $undefined; + break; + } + let wireType = tag & 7; + switch (tag >>>= 3) { + case 1: { + if (wireType !== 0) + break; + if (typeof (value = reader.uint64()) === "object" ? value.low || value.high : value !== 0) + message.timestamp = value; + else + delete message.timestamp; + continue; + } + case 2: { + if (wireType !== 0) + break; + if (value = reader.bool()) + message.request_extended_information = value; + else + delete message.request_extended_information; + continue; + } + case 3: { + if (wireType !== 0) + break; + if (typeof (value = reader.uint64()) === "object" ? value.low || value.high : value !== 0) + message.server_version_v2 = value; + else + delete message.server_version_v2; + continue; + } + case 4: { + if (wireType !== 0) + break; + if (value = reader.uint32()) + message.user_count = value; + else + delete message.user_count; + continue; + } + case 5: { + if (wireType !== 0) + break; + if (value = reader.uint32()) + message.max_user_count = value; + else + delete message.max_user_count; + continue; + } + case 6: { + if (wireType !== 0) + break; + if (value = reader.uint32()) + message.max_bandwidth_per_user = value; + else + delete message.max_bandwidth_per_user; + continue; + } + } + reader.skipType(wireType, _depth, tag); + if (!reader.discardUnknown) { + $util.makeProp(message, "$unknowns", false); + (message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos)); + } + } + if (length !== $undefined) { + if (reader.pos !== end) + throw $RangeError("index out of range"); + reader.len = length; + } + if (_end !== $undefined) + throw $Error("missing end group"); + return message; + }; + + Ping.verify = function (message, _depth) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + return "max depth exceeded"; + if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp")) + if (!$util.isInteger(message.timestamp) && !(message.timestamp && $util.isInteger(message.timestamp.low) && $util.isInteger(message.timestamp.high))) + return "timestamp: integer|Long expected"; + if (message.request_extended_information != null && $Object.hasOwnProperty.call(message, "request_extended_information")) + if (typeof message.request_extended_information !== "boolean") + return "request_extended_information: boolean expected"; + if (message.server_version_v2 != null && $Object.hasOwnProperty.call(message, "server_version_v2")) + if (!$util.isInteger(message.server_version_v2) && !(message.server_version_v2 && $util.isInteger(message.server_version_v2.low) && $util.isInteger(message.server_version_v2.high))) + return "server_version_v2: integer|Long expected"; + if (message.user_count != null && $Object.hasOwnProperty.call(message, "user_count")) + if (!$util.isInteger(message.user_count)) + return "user_count: integer expected"; + if (message.max_user_count != null && $Object.hasOwnProperty.call(message, "max_user_count")) + if (!$util.isInteger(message.max_user_count)) + return "max_user_count: integer expected"; + if (message.max_bandwidth_per_user != null && $Object.hasOwnProperty.call(message, "max_bandwidth_per_user")) + if (!$util.isInteger(message.max_bandwidth_per_user)) + return "max_bandwidth_per_user: integer expected"; + return null; + }; + + Ping.fromObject = function (object, _depth) { + if (object instanceof $root.MumbleUDP.Ping) + return object; + if (!$util.isObject(object)) + throw $TypeError(".MumbleUDP.Ping: object expected"); + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let message = new $root.MumbleUDP.Ping(); + if (object.timestamp != null) + if (typeof object.timestamp === "object" ? object.timestamp.low || object.timestamp.high : $Number(object.timestamp) !== 0) + if ($util.Long) + message.timestamp = $util.Long.fromValue(object.timestamp, true); + else if (typeof object.timestamp === "string") + message.timestamp = $parseInt(object.timestamp, 10); + else if (typeof object.timestamp === "number") + message.timestamp = object.timestamp; + else if (typeof object.timestamp === "object") + message.timestamp = new $util.LongBits(object.timestamp.low >>> 0, object.timestamp.high >>> 0).toNumber(true); + if (object.request_extended_information != null) + if (object.request_extended_information) + message.request_extended_information = $Boolean(object.request_extended_information); + if (object.server_version_v2 != null) + if (typeof object.server_version_v2 === "object" ? object.server_version_v2.low || object.server_version_v2.high : $Number(object.server_version_v2) !== 0) + if ($util.Long) + message.server_version_v2 = $util.Long.fromValue(object.server_version_v2, true); + else if (typeof object.server_version_v2 === "string") + message.server_version_v2 = $parseInt(object.server_version_v2, 10); + else if (typeof object.server_version_v2 === "number") + message.server_version_v2 = object.server_version_v2; + else if (typeof object.server_version_v2 === "object") + message.server_version_v2 = new $util.LongBits(object.server_version_v2.low >>> 0, object.server_version_v2.high >>> 0).toNumber(true); + if (object.user_count != null) + if ($Number(object.user_count) !== 0) + message.user_count = object.user_count >>> 0; + if (object.max_user_count != null) + if ($Number(object.max_user_count) !== 0) + message.max_user_count = object.max_user_count >>> 0; + if (object.max_bandwidth_per_user != null) + if ($Number(object.max_bandwidth_per_user) !== 0) + message.max_bandwidth_per_user = object.max_bandwidth_per_user >>> 0; + return message; + }; + + Ping.toObject = function (message, options, _depth) { + if (!options) + options = {}; + if (_depth === $undefined) + _depth = 0; + if (_depth > $util.recursionLimit) + throw $Error("max depth exceeded"); + let object = {}; + if (options.defaults) { + if ($util.Long) { + let long = new $util.Long(0, 0, true); + object.timestamp = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long; + } else + object.timestamp = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0; + object.request_extended_information = false; + if ($util.Long) { + let long = new $util.Long(0, 0, true); + object.server_version_v2 = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long; + } else + object.server_version_v2 = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0; + object.user_count = 0; + object.max_user_count = 0; + object.max_bandwidth_per_user = 0; + } + if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp")) + if (typeof $BigInt !== "undefined" && options.longs === $BigInt) + object.timestamp = typeof message.timestamp === "number" ? $BigInt(message.timestamp) : $util.Long.fromBits(message.timestamp.low >>> 0, message.timestamp.high >>> 0, true).toBigInt(); + else if (typeof message.timestamp === "number") + object.timestamp = options.longs === $String ? $String(message.timestamp) : message.timestamp; + else + object.timestamp = options.longs === $String ? $util.Long.prototype.toString.call(message.timestamp) : options.longs === $Number ? new $util.LongBits(message.timestamp.low >>> 0, message.timestamp.high >>> 0).toNumber(true) : message.timestamp; + if (message.request_extended_information != null && $Object.hasOwnProperty.call(message, "request_extended_information")) + object.request_extended_information = message.request_extended_information; + if (message.server_version_v2 != null && $Object.hasOwnProperty.call(message, "server_version_v2")) + if (typeof $BigInt !== "undefined" && options.longs === $BigInt) + object.server_version_v2 = typeof message.server_version_v2 === "number" ? $BigInt(message.server_version_v2) : $util.Long.fromBits(message.server_version_v2.low >>> 0, message.server_version_v2.high >>> 0, true).toBigInt(); + else if (typeof message.server_version_v2 === "number") + object.server_version_v2 = options.longs === $String ? $String(message.server_version_v2) : message.server_version_v2; + else + object.server_version_v2 = options.longs === $String ? $util.Long.prototype.toString.call(message.server_version_v2) : options.longs === $Number ? new $util.LongBits(message.server_version_v2.low >>> 0, message.server_version_v2.high >>> 0).toNumber(true) : message.server_version_v2; + if (message.user_count != null && $Object.hasOwnProperty.call(message, "user_count")) + object.user_count = message.user_count; + if (message.max_user_count != null && $Object.hasOwnProperty.call(message, "max_user_count")) + object.max_user_count = message.max_user_count; + if (message.max_bandwidth_per_user != null && $Object.hasOwnProperty.call(message, "max_bandwidth_per_user")) + object.max_bandwidth_per_user = message.max_bandwidth_per_user; + return object; + }; + + Ping.prototype.toJSON = function() { + return Ping.toObject(this, $protobuf.util.toJSONOptions); + }; + + Ping.getTypeUrl = function(prefix) { + if (prefix === $undefined) + prefix = "type.googleapis.com"; + return prefix + "/MumbleUDP.Ping"; + }; + + return Ping; + })(); + + return MumbleUDP; +})(); + +export { + $root as default +}; diff --git a/src/core/proto.ts b/src/core/proto.ts new file mode 100644 index 0000000..6264944 --- /dev/null +++ b/src/core/proto.ts @@ -0,0 +1,81 @@ +// Generated from proto/Mumble.proto by `npm run proto` (static code, no eval, so it runs under a strict CSP) +import { MumbleProto } from './mumble-pb.js'; + +// TCP control message type ids, in wire order (index = type id). +export const MESSAGE_TYPES = [ + 'Version', 'UDPTunnel', 'Authenticate', 'Ping', 'Reject', 'ServerSync', + 'ChannelRemove', 'ChannelState', 'UserRemove', 'UserState', 'BanList', + 'TextMessage', 'PermissionDenied', 'ACL', 'QueryUsers', 'CryptSetup', + 'ContextActionModify', 'ContextAction', 'UserList', 'VoiceTarget', + 'PermissionQuery', 'CodecVersion', 'UserStats', 'RequestBlob', + 'ServerConfig', 'SuggestConfig', 'PluginDataTransmission' +] as const; + +export type MessageName = (typeof MESSAGE_TYPES)[number]; + +export interface Codec { + encode(name: MessageName, payload: Record): Uint8Array; + decode(typeId: number, body: Uint8Array): { name: MessageName; msg: any } | null; +} + +interface GeneratedType { + verify(obj: Record): string | null; + fromObject(obj: Record): unknown; + encode(msg: unknown): { finish(): Uint8Array }; + decode(body: Uint8Array): unknown; + toObject(msg: unknown, opts: { longs: NumberConstructor }): any; +} + +// Field names keep their snake_case spelling from the .proto file; 64-bit integers become numbers. +export function createCodec(): Codec { + const types = MESSAGE_TYPES.map(n => (MumbleProto as unknown as Record)[n]); + + return { + encode(name, payload) { + const id = MESSAGE_TYPES.indexOf(name); + const type = types[id]; + const err = type.verify(payload); + if (err) throw new Error(`${name}: ${err}`); + const body = type.encode(type.fromObject(payload)).finish(); + return frame(id, body); + }, + decode(typeId, body) { + const type = types[typeId]; + if (!type) return null; + const msg = type.toObject(type.decode(body), { longs: Number }); + return { name: MESSAGE_TYPES[typeId], msg }; + } + }; +} + +// 6-byte header: uint16 type, uint32 length (big endian), then the body. +export function frame(typeId: number, body: Uint8Array): Uint8Array { + const out = new Uint8Array(6 + body.length); + const view = new DataView(out.buffer); + view.setUint16(0, typeId); + view.setUint32(2, body.length); + out.set(body, 6); + return out; +} + +// Reassembles framed messages from an arbitrary chunked byte stream. +export class FrameReader { + private buf = new Uint8Array(0); + + push(chunk: Uint8Array, onFrame: (typeId: number, body: Uint8Array) => void): void { + const merged = new Uint8Array(this.buf.length + chunk.length); + merged.set(this.buf); + merged.set(chunk, this.buf.length); + let off = 0; + while (merged.length - off >= 6) { + const view = new DataView(merged.buffer, merged.byteOffset + off); + const typeId = view.getUint16(0); + const len = view.getUint32(2); + if (len > 8 * 1024 * 1024) throw new Error(`Frame too large: ${len}`); + if (merged.length - off - 6 < len) break; + onFrame(typeId, merged.subarray(off + 6, off + 6 + len)); + off += 6 + len; + } + this.buf = merged.slice(off); + } +} diff --git a/src/core/transport.ts b/src/core/transport.ts new file mode 100644 index 0000000..d8bca9e --- /dev/null +++ b/src/core/transport.ts @@ -0,0 +1,42 @@ +// A reliable, ordered byte stream to a Mumble server (TLS in Electron, WebSocket proxy on the web). +export interface Transport { + send(bytes: Uint8Array): void; + close(): void; + onData: ((chunk: Uint8Array) => void) | null; + onClose: ((reason: string) => void) | null; +} + +export interface ConnectTarget { + host: string; + port: number; +} + +// Identity certificate the client presents; Mumble uses it to recognize registered users. +export interface Identity { + certPem: string; + keyPem: string; + fingerprint: string; +} + +export interface CertDetails { + subject: string; + issuer: string; + validFrom: string; + validTo: string; + serialNumber: string; + fingerprint: string; // SHA-1, the hash Mumble uses for users + fingerprint256: string; + subjectAltName: string; + keyType: string; + keyBits: number | null; + ca: boolean; + selfSigned: boolean; + pem: string; +} + +export interface ServerCertInfo { + fingerprint: string; // sha256 of the server certificate, colon-separated hex + authorized: boolean; // true if it chains to a trusted CA + authError: string | null; // why it is not trusted, e.g. DEPTH_ZERO_SELF_SIGNED_CERT + chain: CertDetails[]; // leaf first +} diff --git a/src/core/voice-packet.ts b/src/core/voice-packet.ts new file mode 100644 index 0000000..c9f9616 --- /dev/null +++ b/src/core/voice-packet.ts @@ -0,0 +1,121 @@ +// Mumble voice packets, as sent over UDP or tunneled through TCP (UDPTunnel). +// Two wire formats exist: the legacy one (header byte + Mumble varints) and, since 1.5, +// a protobuf one (type byte 0 + MumbleUDP.Audio). Servers pick per client version. +import { MumbleUDP } from './mumble-udp-pb.js'; + +export interface VoicePacket { + session: number; // sender (only in packets from the server) + target: number; // 0 normal talking, 1-30 whisper targets, 31 server loopback; context when received + frame: number; // sequence number, in 10 ms audio frames + opus: Uint8Array; + last: boolean; // end of this transmission +} + +const LEGACY_OPUS = 4; +const PROTOBUF_AUDIO = 0; + +// ─── Mumble varint (PacketDataStream) ───────────────────────────────────────── + +export function writeVarint(out: number[], value: number): void { + if (!Number.isInteger(value) || value < 0) throw new Error(`Unsupported varint ${value}`); + if (value < 0x80) out.push(value); + else if (value < 0x4000) out.push(0x80 | (value >> 8), value & 0xff); + else if (value < 0x200000) out.push(0xc0 | (value >> 16), (value >> 8) & 0xff, value & 0xff); + else if (value < 0x10000000) out.push(0xe0 | (value >> 24), (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff); + else if (value < 0x100000000) out.push(0xf0, (value >>> 24) & 0xff, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff); + else { + const hi = Math.floor(value / 0x100000000), lo = value >>> 0; + out.push(0xf4, (hi >>> 24) & 0xff, (hi >> 16) & 0xff, (hi >> 8) & 0xff, hi & 0xff, + (lo >>> 24) & 0xff, (lo >> 16) & 0xff, (lo >> 8) & 0xff, lo & 0xff); + } +} + +export function readVarint(buf: Uint8Array, pos: number): [value: number, next: number] { + const need = (n: number) => { if (pos + n > buf.length) throw new Error('Truncated varint'); }; + need(1); + const b = buf[pos]; + if ((b & 0x80) === 0) return [b, pos + 1]; + if ((b & 0xc0) === 0x80) { need(2); return [((b & 0x3f) << 8) | buf[pos + 1], pos + 2]; } + if ((b & 0xe0) === 0xc0) { need(3); return [((b & 0x1f) << 16) | (buf[pos + 1] << 8) | buf[pos + 2], pos + 3]; } + if ((b & 0xf0) === 0xe0) { need(4); return [((b & 0x0f) << 24 | (buf[pos + 1] << 16) | (buf[pos + 2] << 8) | buf[pos + 3]) >>> 0, pos + 4]; } + const be32 = (p: number) => ((buf[p] << 24) | (buf[p + 1] << 16) | (buf[p + 2] << 8) | buf[p + 3]) >>> 0; + if ((b & 0xfc) === 0xf0) { need(5); return [be32(pos + 1), pos + 5]; } + if ((b & 0xfc) === 0xf4) { need(9); return [be32(pos + 1) * 0x100000000 + be32(pos + 5), pos + 9]; } + // Negative numbers (0xf8, 0xfc prefixes) never occur in audio headers + throw new Error('Unsupported varint prefix'); +} + +// ─── Encoding (client to server) ────────────────────────────────────────────── + +export function encodeVoice(p: Omit, protobuf: boolean): Uint8Array { + if (protobuf) { + const msg = MumbleUDP.Audio.fromObject({ + target: p.target, + frame_number: p.frame, + opus_data: p.opus, + is_terminator: p.last + }); + const body: Uint8Array = MumbleUDP.Audio.encode(msg).finish(); + const out = new Uint8Array(body.length + 1); + out[0] = PROTOBUF_AUDIO; + out.set(body, 1); + return out; + } + if (p.opus.length >= 0x2000) throw new Error('Opus frame too large'); + const head: number[] = [(LEGACY_OPUS << 5) | (p.target & 0x1f)]; + writeVarint(head, p.frame); + writeVarint(head, p.opus.length | (p.last ? 0x2000 : 0)); + const out = new Uint8Array(head.length + p.opus.length); + out.set(head); + out.set(p.opus, head.length); + return out; +} + +// ─── Decoding (server to client) ────────────────────────────────────────────── + +// Returns null for pings, other codecs and malformed packets. +export function decodeVoice(buf: Uint8Array): VoicePacket | null { + if (!buf.length) return null; + try { + if (buf[0] === PROTOBUF_AUDIO) { + const m = MumbleUDP.Audio.toObject(MumbleUDP.Audio.decode(buf.subarray(1)), { longs: Number, defaults: true }); + return { + session: m.sender_session, + target: m.context ?? m.target ?? 0, + frame: m.frame_number, + opus: m.opus_data instanceof Uint8Array ? m.opus_data : new Uint8Array(m.opus_data ?? []), + last: !!m.is_terminator + }; + } + if (buf[0] >> 5 !== LEGACY_OPUS) return null; + const target = buf[0] & 0x1f; + let pos = 1, session, frame, size; + [session, pos] = readVarint(buf, pos); + [frame, pos] = readVarint(buf, pos); + [size, pos] = readVarint(buf, pos); + const len = size & 0x1fff; + if (pos + len > buf.length) return null; + return { session, target, frame, opus: buf.slice(pos, pos + len), last: (size & 0x2000) !== 0 }; + } catch { + return null; + } +} + +// For tests and the server-side view: a client packet as the server would relay it +export function relayAsServer(clientPacket: Uint8Array, session: number, protobuf: boolean): Uint8Array { + if (protobuf) { + const m = MumbleUDP.Audio.decode(clientPacket.subarray(1)); + m.sender_session = session; + m.context = 0; + const body: Uint8Array = MumbleUDP.Audio.encode(m).finish(); + const out = new Uint8Array(body.length + 1); + out.set(body, 1); + return out; + } + const head: number[] = [clientPacket[0] & 0xe0]; + writeVarint(head, session); + const out = new Uint8Array(head.length + clientPacket.length - 1); + out.set(head); + out.set(clientPacket.subarray(1), head.length); + return out; +} diff --git a/src/lib/actions.ts b/src/lib/actions.ts new file mode 100644 index 0000000..75c2934 --- /dev/null +++ b/src/lib/actions.ts @@ -0,0 +1,86 @@ +import { session } from './session.svelte.ts'; +import { ui } from './ui.svelte.ts'; +import { store } from './settings.svelte.ts'; +import { voice, userKey } from './audio/voice.svelte.ts'; +import type { MenuItem } from './menu.svelte.ts'; +import type { Channel, User } from '../core/client.ts'; + +// Right-click menu for a channel in the tree +export function channelMenu(c: Channel): MenuItem[] { + const here = session.self?.channelId === c.id; + return [ + { label: here ? 'You are here' : 'Join channel', action: () => session.joinChannel(c.id), disabled: here || !c.canEnter }, + { label: 'Open chat', action: () => { session.showChannel(c.id, 'chat'); ui.panelOpen = true; } }, + { label: 'Show description', action: () => { session.showChannel(c.id, 'info'); ui.panelOpen = true; } }, + { separator: true, label: '' }, + { label: session.isExpanded(c.id) ? 'Collapse' : 'Expand', action: () => session.toggleExpanded(c.id), disabled: c.id === 0 }, + { label: 'Expand all', action: () => session.setAllExpanded(true) }, + { label: 'Collapse all', action: () => session.setAllExpanded(false) }, + { separator: true, label: '' }, + { label: 'Copy channel name', action: () => navigator.clipboard.writeText(c.name || 'Root') } + ]; +} + +// Right-click menu for a user. Moderation entries are always offered; the server decides +// whether we may use them and answers with a permission denial otherwise. +export function userMenu(u: User): MenuItem[] { + const self = session.self; + const isSelf = u.session === self?.session; + const items: MenuItem[] = [ + { label: 'View profile', action: () => { session.showUser(u.session); ui.panelOpen = true; } }, + { label: 'Information', action: () => (ui.userInfo = u.session) } + ]; + if (isSelf) { + items.push( + { label: 'Mute', checked: u.selfMute, action: () => session.setSelfMute(!u.selfMute) }, + { label: 'Deafen', checked: u.selfDeaf, action: () => session.setSelfDeaf(!u.selfDeaf) }, + { label: 'Change name...', action: () => changeNamePrompt(u) } + ); + if (u.userId == null) items.push({ label: 'Register on this server', action: () => session.register(u.session) }); + } else { + items.push( + { label: 'Send message', action: () => session.selectView(`dm:${u.session}`) }, + { label: 'Mute for me', checked: !!voice.settings.localMutes[userKey(u)], action: () => voice.setLocalMute(u, !voice.settings.localMutes[userKey(u)]) }, + { label: 'Set nickname...', action: () => nicknamePrompt(u) } + ); + if (self && u.channelId !== self.channelId) { + items.push({ label: 'Move to my channel', action: () => session.moveUser(u.session, self.channelId) }); + } + } + items.push( + { separator: true, label: '' }, + { label: 'Server mute', checked: u.mute, action: () => session.setServerMute(u.session, !u.mute) }, + { label: 'Server deafen', checked: u.deaf, action: () => session.setServerDeaf(u.session, !u.deaf) }, + { label: 'Priority speaker', checked: u.prioritySpeaker, action: () => session.setPrioritySpeaker(u.session, !u.prioritySpeaker) } + ); + if (!isSelf && u.userId == null) items.push({ label: 'Register user', action: () => session.register(u.session) }); + if (!isSelf) { + items.push( + { separator: true, label: '' }, + { label: 'Kick...', danger: true, action: () => (ui.prompt = { title: `Kick ${u.name}`, label: 'Reason (optional)', confirm: 'Kick', danger: true, onsubmit: r => session.kick(u.session, r) }) }, + { label: 'Ban...', danger: true, action: () => (ui.prompt = { title: `Ban ${u.name}`, label: 'Reason (optional)', confirm: 'Ban', danger: true, onsubmit: r => session.kick(u.session, r, true) }) } + ); + } + items.push({ separator: true, label: '' }, { label: 'Copy name', action: () => navigator.clipboard.writeText(u.name) }); + if (u.hash) items.push({ label: 'Copy certificate hash', action: () => navigator.clipboard.writeText(u.hash) }); + return items; +} + +export function changeNamePrompt(u: User): void { + ui.prompt = { + title: 'Change your name', label: 'New name', confirm: 'Reconnect with this name', initial: u.name, + text: u.userId != null + ? 'You are registered here, so the server keeps your registered name. The new name applies on servers where you are a guest.' + : 'Mumble cannot rename you while connected, so mumh5 reconnects with the new name and remembers it for this server.', + onsubmit: name => session.changeName(name) + }; +} + +export function nicknamePrompt(u: User): void { + ui.prompt = { + title: `Nickname for ${u.name}`, label: 'Nickname (only you see it; empty to remove)', confirm: 'Save', + initial: store.nicknames[userKey(u)] ?? '', + onsubmit: nick => store.setNickname(userKey(u), nick) + }; +} + diff --git a/src/lib/audio/capture.worklet.ts b/src/lib/audio/capture.worklet.ts new file mode 100644 index 0000000..7a305b7 --- /dev/null +++ b/src/lib/audio/capture.worklet.ts @@ -0,0 +1,33 @@ +/// +// Cuts microphone input into 10 ms mono frames (480 samples at 48 kHz) for the encoder, +// with the RMS level of each frame for the meter and voice activity detection. + +const FRAME = 480; + +class CaptureProcessor extends AudioWorkletProcessor { + private buf = new Float32Array(FRAME); + private fill = 0; + + process(inputs: Float32Array[][]): boolean { + const input = inputs[0]?.[0]; + if (!input) return true; + let i = 0; + while (i < input.length) { + const n = Math.min(FRAME - this.fill, input.length - i); + this.buf.set(input.subarray(i, i + n), this.fill); + this.fill += n; + i += n; + if (this.fill === FRAME) { + let sum = 0; + for (let k = 0; k < FRAME; k++) sum += this.buf[k] * this.buf[k]; + const frame = this.buf; + this.port.postMessage({ frame, rms: Math.sqrt(sum / FRAME) }, [frame.buffer]); + this.buf = new Float32Array(FRAME); + this.fill = 0; + } + } + return true; + } +} + +registerProcessor('mumh5-capture', CaptureProcessor); diff --git a/src/lib/audio/playback.worklet.ts b/src/lib/audio/playback.worklet.ts new file mode 100644 index 0000000..4cbfc5a --- /dev/null +++ b/src/lib/audio/playback.worklet.ts @@ -0,0 +1,95 @@ +/// +// Mixes decoded voice of all speakers. Each speaker has a queue that starts playing once +// `jitter` ms are buffered (absorbs network jitter), and re-buffers after running dry. + +interface Speaker { + chunks: Float32Array[]; + offset: number; // read position in chunks[0] + queued: number; // samples waiting + playing: boolean; + gain: number; + peak: number; +} + +class PlaybackProcessor extends AudioWorkletProcessor { + private speakers = new Map(); + private jitter = Math.round(sampleRate * 0.06); + private maxQueue = Math.round(sampleRate * 0.4); + private gains = new Map(); + private sinceReport = 0; + + constructor() { + super(); + this.port.onmessage = e => { + const m = e.data; + if (m.pcm) this.push(m.session, m.pcm); + else if (m.gain !== undefined) { + this.gains.set(m.session, m.gain); + const s = this.speakers.get(m.session); + if (s) s.gain = m.gain; + } else if (m.jitterMs !== undefined) this.jitter = Math.round(sampleRate * m.jitterMs / 1000); + else if (m.drop !== undefined) this.speakers.delete(m.drop); + else if (m.clear) this.speakers.clear(); + }; + } + + private push(session: number, pcm: Float32Array) { + let s = this.speakers.get(session); + if (!s) { + s = { chunks: [], offset: 0, queued: 0, playing: false, gain: this.gains.get(session) ?? 1, peak: 0 }; + this.speakers.set(session, s); + } + s.chunks.push(pcm); + s.queued += pcm.length; + // Too far behind (e.g. after a stall): drop the oldest audio to catch up + while (s.queued > this.maxQueue && s.chunks.length > 1) { + const c = s.chunks.shift()!; + s.queued -= c.length - s.offset; + s.offset = 0; + } + if (!s.playing && s.queued >= this.jitter) s.playing = true; + } + + process(_inputs: Float32Array[][], outputs: Float32Array[][]): boolean { + const out = outputs[0]; + const n = out[0].length; + const mix = out[0]; + mix.fill(0); + for (const s of this.speakers.values()) { + if (!s.playing) continue; + let i = 0; + while (i < n && s.chunks.length) { + const c = s.chunks[0]; + const take = Math.min(n - i, c.length - s.offset); + for (let k = 0; k < take; k++) { + const v = c[s.offset + k] * s.gain; + mix[i + k] += v; + const a = v < 0 ? -v : v; + if (a > s.peak) s.peak = a; + } + i += take; + s.offset += take; + s.queued -= take; + if (s.offset === c.length) { s.chunks.shift(); s.offset = 0; } + } + if (!s.chunks.length) s.playing = false; // ran dry: buffer again before resuming + } + // Soft clip so several loud speakers do not distort harshly + for (let i = 0; i < n; i++) { + const v = mix[i]; + if (v > 1 || v < -1) mix[i] = Math.tanh(v); + } + for (let ch = 1; ch < out.length; ch++) out[ch].set(mix); + + this.sinceReport += n; + if (this.sinceReport >= sampleRate / 10) { + const levels: Record = {}; + for (const [id, s] of this.speakers) { levels[id] = s.peak; s.peak = 0; } + this.port.postMessage({ levels }); + this.sinceReport = 0; + } + return true; + } +} + +registerProcessor('mumh5-playback', PlaybackProcessor); diff --git a/src/lib/audio/voice.svelte.ts b/src/lib/audio/voice.svelte.ts new file mode 100644 index 0000000..9281697 --- /dev/null +++ b/src/lib/audio/voice.svelte.ts @@ -0,0 +1,447 @@ +import captureUrl from './capture.worklet.ts?worker&url'; +import playbackUrl from './playback.worklet.ts?worker&url'; +import { encodeVoice, decodeVoice } from '../../core/voice-packet.ts'; +import type { MumbleClient } from '../../core/client.ts'; +import { load, save } from '../storage.ts'; + +export type TransmitMode = 'vad' | 'ptt' | 'continuous'; + +export interface VoiceSettings { + inputDevice: string; + outputDevice: string; + mode: TransmitMode; + pttKey: string; // KeyboardEvent.code + vadThreshold: number; // dBFS; louder frames open the voice activity gate + vadHoldMs: number; // keep sending this long after the level drops + inputGain: number; // 0..3 + outputVolume: number; // 0..2 + echoCancellation: boolean; + noiseSuppression: boolean; + autoGainControl: boolean; + bitrate: number; // Opus bits per second + frameMs: 10 | 20 | 40 | 60; + jitterMs: number; + // Per-user local volume and mute, keyed by certificate hash (or name without one) + userVolumes: Record; + localMutes: Record; +} + +const defaults: VoiceSettings = { + inputDevice: 'default', + outputDevice: 'default', + mode: 'vad', + pttKey: 'Backquote', + vadThreshold: -45, + vadHoldMs: 350, + inputGain: 1, + outputVolume: 1, + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + bitrate: 40000, + frameMs: 20, + jitterMs: 60, + userVolumes: {}, + localMutes: {} +}; + +const SAMPLE_RATE = 48000; +const FRAME = 480; // 10 ms +// Rough per-packet cost of IP, TCP/TLS or UDP, and Mumble headers, for the bandwidth limit +const PACKET_OVERHEAD_BYTES = 60; + +export const userKey = (u: { hash: string; name: string }) => u.hash || `name:${u.name}`; + +export interface DeviceInfo { id: string; label: string } + +class VoiceEngine { + settings = $state(load('mumh5.voice', defaults)); + inputDevices = $state([]); + outputDevices = $state([]); + running = $state(false); + error = $state(''); + level = $state(-100); // current input level, dBFS + transmitting = $state(false); + pttDown = $state(false); + testing = $state(false); // hear yourself + talking = $state>({}); + effectiveBitrate = $state(0); + stats = $state({ sent: 0, received: 0, decoded: 0, lost: 0 }); + + private ctx: AudioContext | null = null; + private stream: MediaStream | null = null; + private source: MediaStreamAudioSourceNode | null = null; + private inputGainNode: GainNode | null = null; + private outputGainNode: GainNode | null = null; + private capture: AudioWorkletNode | null = null; + private playback: AudioWorkletNode | null = null; + private encoder: AudioEncoder | null = null; + private decoders = new Map | null }>(); + private client: MumbleClient | null = null; + private sessionGains = new Map(); + private unsubscribe: (() => void)[] = []; + + private frameCounter = 0; // Mumble sequence, in 10 ms frames + private sampleTime = 0; // encoder timestamps, microseconds + private lastVoiceAt = 0; // for VAD hold + private open = false; // gate state + private stopping = false; // flushing the last packet of a transmission + private held: Uint8Array[] = []; + private levelRaw = -100; + private levelTimer: ReturnType | null = null; + + constructor() { + if (typeof window === 'undefined') return; + window.addEventListener('keydown', e => this.onKey(e, true)); + window.addEventListener('keyup', e => this.onKey(e, false)); + // Released keys are not reported while the window is in the background + window.addEventListener('blur', () => { this.pttDown = false; }); + navigator.mediaDevices?.addEventListener('devicechange', () => this.refreshDevices()); + } + + save(): void { + save('mumh5.voice', $state.snapshot(this.settings)); + } + + update(patch: Partial): void { + Object.assign(this.settings, patch); + this.save(); + if ('inputGain' in patch && this.inputGainNode) this.inputGainNode.gain.value = this.settings.inputGain; + if ('outputVolume' in patch && this.outputGainNode) this.outputGainNode.gain.value = this.settings.outputVolume; + if ('jitterMs' in patch) this.playback?.port.postMessage({ jitterMs: this.settings.jitterMs }); + if ('outputDevice' in patch) this.applyOutputDevice(); + if ('bitrate' in patch || 'frameMs' in patch) this.configureEncoder(); + if (['inputDevice', 'echoCancellation', 'noiseSuppression', 'autoGainControl'].some(k => k in patch)) this.restartInput(); + } + + // ─── Devices ──────────────────────────────────────────────────────────────── + + async refreshDevices(): Promise { + if (!navigator.mediaDevices?.enumerateDevices) return; + const all = await navigator.mediaDevices.enumerateDevices(); + const map = (kind: MediaDeviceKind) => all.filter(d => d.kind === kind && d.deviceId !== 'communications') + .map(d => ({ id: d.deviceId, label: d.label || (d.deviceId === 'default' ? 'System default' : 'Unnamed device') })); + this.inputDevices = map('audioinput'); + this.outputDevices = map('audiooutput'); + } + + // ─── Lifecycle ────────────────────────────────────────────────────────────── + + // Starts the audio graph; safe to call repeatedly + async start(): Promise { + if (this.ctx) return; + this.error = ''; + try { + if (typeof AudioEncoder === 'undefined' || !(await AudioEncoder.isConfigSupported(this.encoderConfig())).supported) { + throw new Error('This system cannot encode Opus audio'); + } + const ctx = new AudioContext({ sampleRate: SAMPLE_RATE, latencyHint: 'interactive' }); + this.ctx = ctx; + await ctx.audioWorklet.addModule(captureUrl); + await ctx.audioWorklet.addModule(playbackUrl); + + this.outputGainNode = new GainNode(ctx, { gain: this.settings.outputVolume }); + this.playback = new AudioWorkletNode(ctx, 'mumh5-playback', { numberOfInputs: 0, outputChannelCount: [2] }); + this.playback.port.onmessage = e => this.onLevels(e.data.levels); + this.playback.port.postMessage({ jitterMs: this.settings.jitterMs }); + for (const [s, g] of this.sessionGains) this.playback.port.postMessage({ session: s, gain: g }); + this.playback.connect(this.outputGainNode).connect(ctx.destination); + + this.inputGainNode = new GainNode(ctx, { gain: this.settings.inputGain }); + this.capture = new AudioWorkletNode(ctx, 'mumh5-capture', { numberOfOutputs: 0 }); + this.capture.port.onmessage = e => this.onFrame(e.data.frame, e.data.rms); + this.inputGainNode.connect(this.capture); + + await this.applyOutputDevice(); + await this.restartInput(); + this.configureEncoder(); + await ctx.resume(); + this.levelTimer = setInterval(() => { this.level = this.levelRaw; }, 50); + this.running = true; + await this.refreshDevices(); + } catch (e) { + this.error = (e as Error).message; + await this.stop(); + } + } + + async stop(): Promise { + this.endTransmission(); + if (this.levelTimer) clearInterval(this.levelTimer); + this.levelTimer = null; + this.stream?.getTracks().forEach(t => t.stop()); + this.stream = null; + for (const d of this.decoders.values()) { try { d.decoder.close(); } catch { /* closed */ } } + this.decoders.clear(); + try { this.encoder?.close(); } catch { /* closed */ } + this.encoder = null; + await this.ctx?.close().catch(() => {}); + this.ctx = null; + this.capture = this.playback = null; + this.running = false; + this.transmitting = false; + this.talking = {}; + this.level = -100; + } + + private async restartInput(): Promise { + if (!this.ctx || !this.inputGainNode) return; + this.source?.disconnect(); + this.stream?.getTracks().forEach(t => t.stop()); + const s = this.settings; + try { + this.stream = await navigator.mediaDevices.getUserMedia({ + audio: { + deviceId: s.inputDevice && s.inputDevice !== 'default' ? { exact: s.inputDevice } : undefined, + echoCancellation: s.echoCancellation, + noiseSuppression: s.noiseSuppression, + autoGainControl: s.autoGainControl, + channelCount: 1, + sampleRate: SAMPLE_RATE + } + }); + } catch (e) { + this.error = `Microphone unavailable: ${(e as Error).message}`; + return; + } + this.error = ''; + this.source = this.ctx.createMediaStreamSource(this.stream); + this.source.connect(this.inputGainNode); + this.refreshDevices(); + } + + private async applyOutputDevice(): Promise { + const ctx = this.ctx as (AudioContext & { setSinkId?: (id: string) => Promise }) | null; + if (!ctx?.setSinkId) return; + try { + await ctx.setSinkId(this.settings.outputDevice === 'default' ? '' : this.settings.outputDevice); + } catch (e) { + this.error = `Output device unavailable: ${(e as Error).message}`; + } + } + + // ─── Connection ───────────────────────────────────────────────────────────── + + attach(client: MumbleClient): void { + this.unsubscribe.forEach(off => off()); + this.client = client; + this.stats = { sent: 0, received: 0, decoded: 0, lost: 0 }; + this.unsubscribe = [ + client.on('voice', raw => { if (this.client === client) this.receive(raw); }), + client.on('userRemove', u => this.dropSession(u.session)) + ]; + this.limitBitrate(); + this.start(); + } + + detach(): void { + this.endTransmission(); + this.unsubscribe.forEach(off => off()); + this.unsubscribe = []; + this.client = null; + for (const s of [...this.decoders.keys()]) this.dropSession(s); + this.playback?.port.postMessage({ clear: true }); + this.talking = {}; + this.transmitting = false; + // Release the microphone while not connected (unless testing it) + if (!this.testing) this.stop(); + } + + // Opus bitrate that keeps bitrate plus packet overhead inside the server's per-user limit + private allowedBitrate(): number { + const max = this.client?.maxBandwidth ?? 0; + const overhead = PACKET_OVERHEAD_BYTES * 8 * (1000 / this.settings.frameMs); + return Math.min(this.settings.bitrate, max ? Math.max(8000, max - overhead) : Infinity); + } + + private limitBitrate(): void { + if (this.allowedBitrate() !== this.effectiveBitrate) this.configureEncoder(); + } + + // ─── Sending ──────────────────────────────────────────────────────────────── + + private encoderConfig(): AudioEncoderConfig { + return { + codec: 'opus', + sampleRate: SAMPLE_RATE, + numberOfChannels: 1, + bitrate: this.effectiveBitrate || this.allowedBitrate(), + opus: { frameDuration: this.settings.frameMs * 1000, complexity: 10, useinbandfec: true, usedtx: false } + } as AudioEncoderConfig; + } + + private configureEncoder(): void { + this.effectiveBitrate = this.allowedBitrate(); + if (!this.ctx) return; + try { this.encoder?.close(); } catch { /* closed */ } + this.encoder = new AudioEncoder({ + output: chunk => this.onEncoded(chunk), + error: e => { this.error = `Encoder: ${e.message}`; } + }); + this.encoder.configure(this.encoderConfig()); + } + + private canTalk(): boolean { + const self = this.client?.self; + return !!self && this.client!.synced && !self.selfMute && !self.mute && !self.suppress && !self.selfDeaf && !self.deaf; + } + + private onFrame(frame: Float32Array, rms: number): void { + const db = rms > 0 ? 20 * Math.log10(rms) : -100; + this.levelRaw = Math.max(-100, db); + const now = performance.now(); + const s = this.settings; + + if (this.testing) this.playback?.port.postMessage({ session: -1, pcm: frame.slice() }); + + let wanted: boolean; + if (s.mode === 'continuous') wanted = true; + else if (s.mode === 'ptt') wanted = this.pttDown; + else { + if (db > s.vadThreshold) this.lastVoiceAt = now; + wanted = now - this.lastVoiceAt < s.vadHoldMs; + } + wanted &&= this.canTalk() && !!this.encoder && this.encoder.state === 'configured'; + + if (wanted && !this.open) { this.open = true; this.transmitting = true; } + if (!wanted && this.open) { this.endTransmission(); return; } + if (!this.open) return; + + const data = new AudioData({ + format: 'f32-planar', sampleRate: SAMPLE_RATE, numberOfFrames: FRAME, numberOfChannels: 1, + timestamp: this.sampleTime, data: frame + }); + this.sampleTime += 10000; + this.encoder!.encode(data); + data.close(); + } + + private onEncoded(chunk: EncodedAudioChunk): void { + const opus = new Uint8Array(chunk.byteLength); + chunk.copyTo(opus); + if (this.stopping) { this.held.push(opus); return; } + this.sendPacket(opus, false); + } + + private sendPacket(opus: Uint8Array, last: boolean): void { + const client = this.client; + if (!client) return; + client.sendVoiceTunnel(encodeVoice({ target: 0, frame: this.frameCounter, opus, last }, client.protobufVoice)); + this.frameCounter += this.settings.frameMs / 10; + this.stats.sent++; + } + + // Flush what the encoder still holds and mark the final packet, so receivers end the stream cleanly + private endTransmission(): void { + if (!this.open) return; + this.open = false; + this.transmitting = false; + const enc = this.encoder; + if (!enc || enc.state !== 'configured') return; + this.stopping = true; + enc.flush().then(() => { + const held = this.held; + this.held = []; + this.stopping = false; + held.forEach((opus, i) => this.sendPacket(opus, i === held.length - 1)); + if (!held.length) this.sendPacket(new Uint8Array(0), true); + }).catch(() => { this.stopping = false; this.held = []; }); + } + + private onKey(e: KeyboardEvent, down: boolean): void { + if (e.code !== this.settings.pttKey || e.repeat) return; + // Typing in a text field does not trigger push to talk + const t = e.target as HTMLElement | null; + if (down && t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; + this.pttDown = down; + } + + // ─── Receiving ────────────────────────────────────────────────────────────── + + private receive(raw: Uint8Array): void { + const p = decodeVoice(raw); + if (!p) return; + this.stats.received++; + const self = this.client?.self; + if (!this.ctx || !this.playback || self?.selfDeaf || self?.deaf) return; + const user = this.client?.users.get(p.session); + if (user && this.settings.localMutes[userKey(user)]) return; + + let d = this.decoders.get(p.session); + if (!d) { + const session = p.session; + const decoder = new AudioDecoder({ + output: data => this.onDecoded(session, data), + error: () => this.dropSession(session) + }); + decoder.configure({ codec: 'opus', sampleRate: SAMPLE_RATE, numberOfChannels: 1 }); + d = { decoder, lastFrame: -1, timer: null }; + this.decoders.set(session, d); + if (user) this.setSessionGain(session, this.settings.userVolumes[userKey(user)] ?? 1); + } + if (d.lastFrame >= 0 && p.frame > d.lastFrame + 6 && p.frame - d.lastFrame < 500) this.stats.lost++; + d.lastFrame = p.frame; + + if (p.opus.length) { + d.decoder.decode(new EncodedAudioChunk({ type: 'key', timestamp: p.frame * 10000, data: p.opus })); + } + if (!this.talking[p.session]) this.talking[p.session] = true; + if (d.timer) clearTimeout(d.timer); + // Speaking ends with the terminator, or when packets stop arriving + d.timer = setTimeout(() => { delete this.talking[p.session]; }, p.last ? this.settings.jitterMs + 60 : 400); + } + + private onDecoded(session: number, data: AudioData): void { + const pcm = new Float32Array(data.numberOfFrames); + data.copyTo(pcm, { planeIndex: 0, format: 'f32-planar' }); + data.close(); + this.stats.decoded++; + this.playback?.port.postMessage({ session, pcm }, [pcm.buffer]); + } + + private dropSession(session: number): void { + const d = this.decoders.get(session); + if (d) { + if (d.timer) clearTimeout(d.timer); + try { d.decoder.close(); } catch { /* closed */ } + this.decoders.delete(session); + } + this.playback?.port.postMessage({ drop: session }); + delete this.talking[session]; + } + + private onLevels(_levels: Record): void { + // Per-speaker peaks from the playback worklet, for future meters + } + + // ─── Per-user controls ────────────────────────────────────────────────────── + + private setSessionGain(session: number, gain: number): void { + this.sessionGains.set(session, gain); + this.playback?.port.postMessage({ session, gain }); + } + + setUserVolume(user: { session: number; hash: string; name: string }, volume: number): void { + this.settings.userVolumes[userKey(user)] = volume; + this.save(); + this.setSessionGain(user.session, volume); + } + + setLocalMute(user: { session: number; hash: string; name: string }, muted: boolean): void { + if (muted) this.settings.localMutes[userKey(user)] = true; + else delete this.settings.localMutes[userKey(user)]; + this.save(); + if (muted) this.dropSession(user.session); + } + + setTesting(on: boolean): void { + this.testing = on; + if (on) this.start(); + else { + this.playback?.port.postMessage({ drop: -1 }); + if (!this.client) this.stop(); + } + } +} + +export const voice = new VoiceEngine(); diff --git a/src/lib/audio/worklet-env.d.ts b/src/lib/audio/worklet-env.d.ts new file mode 100644 index 0000000..75fb88b --- /dev/null +++ b/src/lib/audio/worklet-env.d.ts @@ -0,0 +1,7 @@ +// Minimal AudioWorklet global scope typings (not part of the DOM lib) +declare abstract class AudioWorkletProcessor { + readonly port: MessagePort; + abstract process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record): boolean; +} +declare function registerProcessor(name: string, ctor: new () => AudioWorkletProcessor): void; +declare const sampleRate: number; diff --git a/src/lib/html.ts b/src/lib/html.ts new file mode 100644 index 0000000..1945a35 --- /dev/null +++ b/src/lib/html.ts @@ -0,0 +1,167 @@ +import DOMPurify, { type Config } from 'dompurify'; + +export type EmbedKind = 'image' | 'video' | 'audio' | 'youtube'; +export interface Embed { kind: EmbedKind; url: string; name: string; videoId?: string; start?: number } + +// YouTube links: watch, youtu.be, shorts, embed, live, music, mobile +export function youtubeVideo(href: string): { id: string; start: number } | null { + let u: URL; + try { u = new URL(href); } catch { return null; } + const host = u.hostname.replace(/^(www|m|music)\./, ''); + let id = ''; + if (host === 'youtu.be') id = u.pathname.slice(1).split('/')[0]; + else if (host === 'youtube.com' || host === 'youtube-nocookie.com') { + if (u.pathname === '/watch') id = u.searchParams.get('v') ?? ''; + else id = /^\/(?:shorts|embed|live|v)\/([^/?#]+)/.exec(u.pathname)?.[1] ?? ''; + } + if (!/^[A-Za-z0-9_-]{11}$/.test(id)) return null; + // t=90, t=1m30s, start=90 + const t = u.searchParams.get('t') ?? u.searchParams.get('start') ?? ''; + const m = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/.exec(t); + const start = m ? (Number(m[1] ?? 0) * 3600 + Number(m[2] ?? 0) * 60 + Number(m[3] ?? 0)) : 0; + return { id, start }; +} + +const EXT: Record = { + png: 'image', jpg: 'image', jpeg: 'image', gif: 'image', webp: 'image', avif: 'image', + mp4: 'video', webm: 'video', mov: 'video', m4v: 'video', + mp3: 'audio', ogg: 'audio', opus: 'audio', wav: 'audio', flac: 'audio', m4a: 'audio' +}; + +export function mediaKind(name: string): EmbedKind | null { + const m = /\.([a-z0-9]+)$/i.exec(name); + return m ? EXT[m[1].toLowerCase()] ?? null : null; +} + +const purifyConfig: Config & { RETURN_DOM_FRAGMENT: true } = { + ALLOWED_TAGS: ['a', 'b', 'i', 'u', 's', 'strong', 'em', 'br', 'p', 'span', 'div', 'ul', 'ol', 'li', + 'blockquote', 'code', 'pre', 'img', 'h1', 'h2', 'h3', 'h4', 'table', 'thead', 'tbody', 'tr', 'td', 'th', + 'hr', 'sub', 'sup', 'font'], + // style is reduced to a safe subset below; color is the attribute older clients send + ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'style', 'color'], + ALLOW_DATA_ATTR: false, + RETURN_DOM_FRAGMENT: true +}; + +// Text styling that Mumble clients (Qt rich text) use; anything else is dropped +const STYLE_PROPS: Record = { + 'color': /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%]+\)|[a-z]{3,20})$/i, + 'background-color': /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%]+\)|[a-z]{3,20})$/i, + 'font-weight': /^(normal|bold|bolder|lighter|[1-9]00)$/i, + 'font-style': /^(normal|italic|oblique)$/i, + 'text-decoration': /^(none|underline|line-through|overline)( (underline|line-through|overline))*$/i, + 'font-size': /^(\d{1,2}(\.\d+)?(pt|px|em|%)|small|medium|large|x-large|xx-large|smaller|larger)$/i, + 'text-align': /^(left|right|center|justify)$/i +}; + +// rgb(1, 2, 3) -> #010203, which every Mumble client understands +function normalizeColor(v: string): string { + const m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(v); + return m ? '#' + m.slice(1, 4).map(n => Math.min(255, Number(n)).toString(16).padStart(2, '0')).join('') : v; +} + +export function cleanStyle(style: string): string { + const out: string[] = []; + for (const decl of style.split(';')) { + const i = decl.indexOf(':'); + if (i < 0) continue; + const prop = decl.slice(0, i).trim().toLowerCase(); + let value = decl.slice(i + 1).trim().replace(/\s*!important$/i, ''); + if (!STYLE_PROPS[prop]?.test(value)) continue; + if (prop.endsWith('color')) value = normalizeColor(value); + out.push(`${prop}:${value}`); + } + return out.join(';'); +} + +DOMPurify.addHook('afterSanitizeAttributes', node => { + const el = node as Element; + if (el.hasAttribute('style')) { + const clean = cleanStyle(el.getAttribute('style') ?? ''); + if (clean) el.setAttribute('style', clean); + else el.removeAttribute('style'); + } + if (el.hasAttribute('color') && !STYLE_PROPS.color.test(el.getAttribute('color') ?? '')) el.removeAttribute('color'); + if (el.tagName === 'A') { + const href = el.getAttribute('href') ?? ''; + if (!/^https?:\/\//i.test(href)) el.removeAttribute('href'); + el.setAttribute('target', '_blank'); + el.setAttribute('rel', 'noopener noreferrer'); + } + // Only inline data: images; remote images would leak our IP to arbitrary hosts + if (el.tagName === 'IMG' && !/^data:image\/(png|jpe?g|gif|webp);/i.test(el.getAttribute('src') ?? '')) { + el.replaceWith(document.createTextNode('[image]')); + } +}); + +// Sanitize an incoming Mumble HTML message and pull out media links from trusted hosts. +export function renderIncoming(html: string, trustedHosts: string[]): { html: string; embeds: Embed[] } { + const frag = DOMPurify.sanitize(html, purifyConfig); + const embeds: Embed[] = []; + for (const a of frag.querySelectorAll('a[href]')) { + const yt = youtubeVideo(a.getAttribute('href')!); + if (yt) { + if (!embeds.some(e => e.videoId === yt.id) && embeds.filter(e => e.kind === 'youtube').length < 3) { + embeds.push({ kind: 'youtube', url: a.getAttribute('href')!, name: a.textContent ?? '', videoId: yt.id, start: yt.start }); + } + continue; + } + let url: URL; + try { url = new URL(a.getAttribute('href')!); } catch { continue; } + if (!trustedHosts.includes(url.host.toLowerCase())) continue; + const name = decodeURIComponent(url.pathname.split('/').pop() ?? ''); + const kind = mediaKind(name); + if (kind) embeds.push({ kind, url: url.href, name }); + } + // The inline thumbnail is only there for vanilla clients; we show the real media instead + if (embeds.some(e => e.kind !== 'youtube')) frag.querySelectorAll('img').forEach(img => img.remove()); + const div = document.createElement('div'); + div.append(frag); + return { html: div.innerHTML.replace(/^()+|()+$/g, ''), embeds }; +} + +export function escapeHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +// Plain composer text to the HTML Mumble clients expect: escaped, links clickable, newlines as
. +// Output must stay well-formed XML: Murmur parses long messages as XML and denies them otherwise. +export function textToHtml(text: string): string { + return escapeHtml(text) + .replace(/https?:\/\/[^\s<]+[^\s<.,:;"')\]]/g, url => `${url}`) + .replace(/\r?\n/g, '
'); +} + +export function formatBytes(n: number): string { + if (n < 1024) return `${n} B`; + if (n < 1048576) return `${(n / 1024).toFixed(1)} KB`; + if (n < 1073741824) return `${(n / 1048576).toFixed(1)} MB`; + return `${(n / 1073741824).toFixed(2)} GB`; +} + +// ─── Outgoing rich text (comments) ──────────────────────────────────────────── + +const VOID_TAGS = new Set(['br', 'hr', 'img']); +const OUT_ATTRS = new Set(['href', 'src', 'alt', 'title', 'style', 'color']); +const escText = (t: string) => t.replace(/&/g, '&').replace(//g, '>'); +const escAttr = (t: string) => escText(t).replace(/"/g, '"'); + +function serialize(node: Node): string { + if (node.nodeType === Node.TEXT_NODE) return escText(node.textContent ?? ''); + if (node.nodeType !== Node.ELEMENT_NODE) return ''; + const el = node as Element; + const tag = el.tagName.toLowerCase(); + const attrs = [...el.attributes].filter(a => OUT_ATTRS.has(a.name)).map(a => ` ${a.name}="${escAttr(a.value)}"`).join(''); + if (VOID_TAGS.has(tag)) return `<${tag}${attrs}/>`; + return `<${tag}${attrs}>${[...el.childNodes].map(serialize).join('')}`; +} + +// Sanitized, well-formed XHTML that Murmur's XML check accepts and Mumble clients render +export function toMumbleHtml(html: string): string { + const frag = DOMPurify.sanitize(html, purifyConfig); + return [...frag.childNodes].map(serialize).join('') + .replace(/(

<\/p>)+$/, '') + .replace(/^()+|()+$/g, '') + .trim(); +} + diff --git a/src/lib/icons.ts b/src/lib/icons.ts new file mode 100644 index 0000000..b924847 --- /dev/null +++ b/src/lib/icons.ts @@ -0,0 +1,30 @@ +// SVG path data (24x24 viewBox, square caps) shared by and the tray icon +export const ICON_PATHS: Record = { + mic: 'M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3zM19 10v2a7 7 0 0 1-14 0v-2M12 19v3', + 'mic-off': 'M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3zM19 10v2a7 7 0 0 1-14 0v-2M12 19v3M3 3l18 18', + headphones: 'M3 14v-2a9 9 0 0 1 18 0v2M21 14v7h-4v-7zM3 14v7h4v-7z', + 'headphones-off': 'M3 14v-2a9 9 0 0 1 18 0v2M21 14v7h-4v-7zM3 14v7h4v-7zM3 3l18 18', + settings: 'M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6', + plus: 'M12 5v14M5 12h14', + paperclip: 'M21.4 11.1l-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5', + hash: 'M4 9h16M4 15h16M10 3L8 21M16 3l-2 18', + channel: 'M11 5L6 9H2v6h4l5 4zM15.5 8.5a5 5 0 0 1 0 7', + users: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM23 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8', + menu: 'M3 6h18M3 12h18M3 18h18', + x: 'M18 6L6 18M6 6l12 12', + power: 'M18.4 6.6a9 9 0 1 1-12.8 0M12 2v10', + lock: 'M5 11h14v10H5zM8 11V7a4 4 0 0 1 8 0v4', + file: 'M14 2H6v20h12V8zM14 2v6h6', + send: 'M22 2L11 13M22 2l-7 20-4-9-9-4z', + message: 'M21 15H7l-4 4V3h18z', + trash: 'M3 6h18M8 6V3h8v3M19 6l-1 15H6L5 6', + server: 'M2 3h20v8H2zM2 13h20v8H2zM6 7h.01M6 17h.01', + shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', + download: 'M12 3v12M7 10l5 5 5-5M4 21h16', + list: 'M9 6h12M9 12h12M9 18h12M4 6h.01M4 12h.01M4 18h.01', + 'list-ordered': 'M10 6h11M10 12h11M10 18h11M4 4v4M3 18h3l-3 3h3M3 10h2l-2 3h2', + link: 'M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1 1M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6l1-1', + image: 'M3 3h18v18H3zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM21 15l-5-5L5 21', + code: 'M16 18l6-6-6-6M8 6l-6 6 6 6', + eraser: 'M20 20H7L3 16l10-10 7 7-6.5 7M6 11l7 7' +}; diff --git a/src/lib/identities.svelte.ts b/src/lib/identities.svelte.ts new file mode 100644 index 0000000..5a8f88d --- /dev/null +++ b/src/lib/identities.svelte.ts @@ -0,0 +1,27 @@ +import { native, type IdentitySummary } from './native.ts'; + +// Client certificates kept by the desktop app's main process +class Identities { + list = $state([]); + setupDone = $state(true); + loaded = $state(false); + + async refresh(): Promise { + if (!native) return; + const r = await native.identities.list(); + this.list = r.identities; + this.setupDone = r.setupDone; + this.loaded = true; + } + + get default(): IdentitySummary | undefined { + return this.list.find(i => i.isDefault) ?? this.list[0]; + } + + byId(id: string | undefined): IdentitySummary | undefined { + return this.list.find(i => i.id === id) ?? this.default; + } +} + +export const identities = new Identities(); +identities.refresh(); diff --git a/src/lib/menu.svelte.ts b/src/lib/menu.svelte.ts new file mode 100644 index 0000000..6aefdd9 --- /dev/null +++ b/src/lib/menu.svelte.ts @@ -0,0 +1,25 @@ +export interface MenuItem { + label: string; + action?: () => void; + danger?: boolean; + checked?: boolean; + disabled?: boolean; + separator?: boolean; +} + +// One context menu for the whole app, opened at the pointer position +class MenuStore { + open = $state<{ x: number; y: number; items: MenuItem[] } | null>(null); + + show(e: MouseEvent, items: MenuItem[]): void { + e.preventDefault(); + e.stopPropagation(); + this.open = { x: e.clientX, y: e.clientY, items }; + } + + close(): void { + this.open = null; + } +} + +export const menu = new MenuStore(); diff --git a/src/lib/native.ts b/src/lib/native.ts new file mode 100644 index 0000000..4673f4f --- /dev/null +++ b/src/lib/native.ts @@ -0,0 +1,72 @@ +import type { Transport, ServerCertInfo, CertDetails } from '../core/transport.ts'; + +interface NativeApi { + platformInfo(): Promise<{ os: string; osVersion: string }>; + describeCerts(ders: Uint8Array[]): Promise; + updateTray(state: { icon: string; tooltip: string; connected: boolean; server: string; muted: boolean; deafened: boolean; mode: string }): void; + onTrayAction(fn: (action: string) => void): void; + identities: { + list(): Promise<{ identities: IdentitySummary[]; setupDone: boolean }>; + create(name: string, email: string): Promise; + importP12(bytes: Uint8Array, password: string, name: string): Promise; + findMumble(): Promise<{ name: string } | null>; + importMumble(name: string): Promise; + exportP12(id: string, password: string): Promise; + setDefault(id: string): Promise; + rename(id: string, name: string): Promise; + remove(id: string): Promise; + finishSetup(): Promise; + }; + open(connId: string, host: string, port: number, identityId?: string): Promise; + send(connId: string, bytes: Uint8Array): void; + close(connId: string): void; + on(event: 'secure' | 'data' | 'close', fn: (connId: string, ...args: any[]) => void): () => void; +} + +export interface IdentitySummary { + id: string; + name: string; + fingerprint: string; + isDefault: boolean; + cert: CertDetails; +} + +declare global { + interface Window { mumh5Native?: NativeApi } +} + +export const native: NativeApi | null = typeof window !== 'undefined' ? window.mumh5Native ?? null : null; + +// Transport backed by a TLS socket in the Electron main process. +// `secure` resolves once the TLS handshake is done, before any Mumble data is sent, +// so the caller can check the server certificate before sending a password. +export class ElectronTransport implements Transport { + onData: ((chunk: Uint8Array) => void) | null = null; + onClose: ((reason: string) => void) | null = null; + readonly secure: Promise; + private id = crypto.randomUUID(); + private offs: (() => void)[] = []; + private api: NativeApi; + + constructor(api: NativeApi, host: string, port: number, identityId?: string) { + this.api = api; + let resolveSecure!: (i: ServerCertInfo) => void; + let rejectSecure!: (e: Error) => void; + this.secure = new Promise((res, rej) => { resolveSecure = res; rejectSecure = rej; }); + const id = this.id; + this.offs.push( + api.on('secure', (cid, info) => { if (cid === id) resolveSecure(info); }), + api.on('data', (cid, chunk) => { if (cid === id) this.onData?.(chunk); }), + api.on('close', (cid, reason) => { + if (cid !== id) return; + this.offs.forEach(off => off()); + rejectSecure(new Error(reason)); + this.onClose?.(reason); + }) + ); + api.open(id, host, port, identityId).catch(e => rejectSecure(e)); + } + + send(bytes: Uint8Array): void { this.api.send(this.id, bytes); } + close(): void { this.api.close(this.id); } +} diff --git a/src/lib/session.svelte.ts b/src/lib/session.svelte.ts new file mode 100644 index 0000000..11504d4 --- /dev/null +++ b/src/lib/session.svelte.ts @@ -0,0 +1,678 @@ +import { createCodec } from '../core/proto.ts'; +import { MumbleClient, describeDenial, type Channel, type User, type TextMessage } from '../core/client.ts'; +import { native, ElectronTransport } from './native.ts'; +import type { ServerCertInfo } from '../core/transport.ts'; +import { store, type SavedServer } from './settings.svelte.ts'; +import { renderIncoming, textToHtml, escapeHtml, formatBytes, type Embed } from './html.ts'; +import { uploadFile, makeThumbnail, makeInlineImage, fetchUploadInfo, checkFile, type UploadInfo } from './upload.ts'; +import { voice } from './audio/voice.svelte.ts'; + +const codec = createCodec(); + +// A chat view is a channel ("ch:") or a direct conversation with a user ("dm:"). +export type ViewKey = `ch:${number}` | `dm:${number}`; + +export interface ChatEntry { + id: number; + kind: 'text' | 'system'; + author: string; + authorSession: number | null; + self: boolean; + html: string; + embeds: Embed[]; + time: number; +} + +export interface UploadJob { + id: number; + key: string; + name: string; + progress: number; + error: string; +} + +export interface CertPrompt { + server: SavedServer; + hostPort: string; + fingerprint: string; + previous: string; +} + +// Right-hand panel: member list, a channel (description preview or its side chat), or a user profile +export type Panel = + | { kind: 'members' } + | { kind: 'channel'; id: number; tab: 'info' | 'chat' } + | { kind: 'user'; session: number }; + +let nextId = 1; + +export class Session { + status = $state<'idle' | 'connecting' | 'connected'>('idle'); + error = $state(''); + server = $state(null); + // Bumped on every client state change; views read it to re-derive from the client's maps + tick = $state(0); + view = $state('ch:0'); + panel = $state({ kind: 'members' }); + logs = $state>({}); + unread = $state>({}); + uploads = $state([]); + certPrompt = $state(null); + rtt = $state(0); + // TLS details of the current server, for the certificate viewer and the trusted badge + serverCert = $state(null); + uploadInfo = $state(null); + // Channel tree expansion the user chose; channels not in here follow the default + expanded = $state>({}); + + client: MumbleClient | null = null; + // Incremented per connect so a slow, superseded attempt can tell it is stale + private attempt = 0; + private uploadInfoFor = ''; + // Blob hashes already requested, so a preview does not ask the server repeatedly + private requested = new Set(); + // Last message with an inline image, resent as a plain link if the server refuses it + private lastImageSend: { key: string; fallback: string; at: number } | null = null; + // Channel we asked to move to; the chat view switches when the server confirms it + private pendingJoin: number | null = null; + + // ─── Derived helpers ──────────────────────────────────────────────────────── + // The client mutates its objects in place. These return shallow copies and read `tick`, + // so every state change hands Svelte new objects and dependent views update. + + get self(): User | null { + void this.tick; + const u = this.client?.self; + return u ? { ...u } : null; + } + + channel(id: number): Channel | undefined { + void this.tick; + const c = this.client?.channels.get(id); + return c && { ...c }; + } + + user(session: number): User | undefined { + void this.tick; + const u = this.client?.users.get(session); + return u && { ...u }; + } + + childrenOf(parent: number | null): Channel[] { + void this.tick; + if (!this.client) return []; + return [...this.client.channels.values()] + .filter(c => (parent === null ? c.id === 0 : c.parent === parent && c.id !== 0)) + .sort((a, b) => a.position - b.position || a.name.localeCompare(b.name)) + .map(c => ({ ...c })); + } + + usersIn(channelId: number): User[] { + void this.tick; + if (!this.client) return []; + return [...this.client.users.values()] + .filter(u => u.channelId === channelId) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(u => ({ ...u })); + } + + allUsers(): User[] { + void this.tick; + return this.client ? [...this.client.users.values()].sort((a, b) => a.name.localeCompare(b.name)).map(u => ({ ...u })) : []; + } + + // Users in a channel and all of its subchannels + subtreeUserCount(id: number): number { + void this.tick; + const client = this.client; + if (!client) return 0; + const ids = new Set([id]); + let grew = true; + while (grew) { + grew = false; + for (const c of client.channels.values()) { + if (c.parent != null && ids.has(c.parent) && !ids.has(c.id)) { ids.add(c.id); grew = true; } + } + } + let n = 0; + for (const u of client.users.values()) if (ids.has(u.channelId)) n++; + return n; + } + + // Collapsed by default; channels with people in them open up, like the desktop client + isExpanded(id: number): boolean { + if (id === 0) return true; + return this.expanded[id] ?? this.subtreeUserCount(id) > 0; + } + + toggleExpanded(id: number): void { + this.expanded[id] = !this.isExpanded(id); + } + + setAllExpanded(open: boolean): void { + const all: Record = {}; + for (const id of this.client?.channels.keys() ?? []) all[id] = open; + this.expanded = all; + } + + viewTitle(key: string = this.view): string { + const [kind, id] = key.split(':'); + if (kind === 'ch') return this.channel(Number(id))?.name || 'Root'; + return this.user(Number(id))?.name ?? 'Offline user'; + } + + // Keys whose chat is on screen right now (main view and an open side chat) + private visibleKeys(): string[] { + const keys: string[] = [this.view]; + if (this.panel.kind === 'channel' && this.panel.tab === 'chat') keys.push(`ch:${this.panel.id}`); + return keys; + } + + // ─── Connection ───────────────────────────────────────────────────────────── + + async connect(server: SavedServer): Promise { + this.disconnect(); + const attempt = ++this.attempt; + this.server = server; + this.error = ''; + this.logs = {}; + this.unread = {}; + this.expanded = {}; + this.panel = { kind: 'members' }; + this.requested.clear(); + this.serverCert = null; + this.status = 'connecting'; + + if (!native) { + this.fail('Connecting needs the desktop app for now. The web version will connect through a proxy.'); + return; + } + + const transport = new ElectronTransport(native, server.host, server.port, server.identityId); + let info; + try { + info = await transport.secure; + } catch (e) { + if (attempt === this.attempt) this.fail((e as Error).message); + return; + } + if (attempt !== this.attempt) return transport.close(); + this.serverCert = info; + + // Trust on first use: pin the certificate, and stop before sending credentials if it changed + const hostPort = `${server.host.toLowerCase()}:${server.port}`; + const pinned = store.pins[hostPort]; + if (!pinned) { + store.pin(hostPort, info.fingerprint); + } else if (pinned !== info.fingerprint && !info.authorized) { + transport.close(); + this.certPrompt = { server, hostPort, fingerprint: info.fingerprint, previous: pinned }; + this.fail('The server certificate changed.'); + return; + } + + const client = new MumbleClient(codec); + this.client = client; + this.wire(client); + // TLS is up but the server may still never finish the login + setTimeout(() => { if (this.client === client && !client.synced) client.disconnect('Login timed out'); }, 20000); + const platform = await native.platformInfo(); + if (attempt !== this.attempt) return transport.close(); + client.connect(transport, { + username: server.username, + password: server.password, + os: platform.os, + osVersion: platform.osVersion + }); + } + + acceptCertificate(): void { + const p = this.certPrompt; + if (!p) return; + store.pin(p.hostPort, p.fingerprint); + this.certPrompt = null; + this.connect(p.server); + } + + disconnect(): void { + this.attempt++; + const client = this.client; + this.client = null; + client?.disconnect(); + sessions.onClosed(this); + this.status = 'idle'; + this.tick++; + } + + private fail(msg: string): void { + this.status = 'idle'; + this.error = msg; + } + + private wire(client: MumbleClient): void { + const live = () => this.client === client; + const bump = () => { if (live()) this.tick++; }; + + client.on('synced', () => { + if (!live()) return; + this.status = 'connected'; + this.view = `ch:${client.self?.channelId ?? 0}`; + sessions.onSynced(this); + if (client.welcomeText) this.system(this.view, client.welcomeText, true); + bump(); + }); + client.on('channel', bump); + client.on('channelRemove', id => { + if (!live()) return; + if (this.panel.kind === 'channel' && this.panel.id === id) this.panel = { kind: 'members' }; + bump(); + }); + client.on('ping', rtt => { if (live()) this.rtt = rtt; }); + + client.on('user', (user, changed, _actor, isNew) => { + if (!live()) return; + const self = client.self; + if (client.synced && self) { + if (isNew) { + if (user.channelId === self.channelId) this.system(`ch:${self.channelId}`, `${escapeHtml(user.name)} connected`); + } else if (changed.includes('userId') && user.userId != null) { + const who = user.session === self.session ? 'You are' : `${escapeHtml(user.name)} is`; + this.system(this.view, `${who} now registered on this server`); + } else if (changed.includes('channelId')) { + if (user.session === self.session) { + // Follow our own channel moves in the chat view (also from a DM when we asked to join) + if (this.view.startsWith('ch:') || this.pendingJoin === user.channelId) this.selectView(`ch:${user.channelId}`); + this.pendingJoin = null; + this.system(`ch:${user.channelId}`, `You joined ${escapeHtml(this.channel(user.channelId)?.name || 'Root')}`); + } else if (user.channelId === self.channelId) { + this.system(`ch:${self.channelId}`, `${escapeHtml(user.name)} joined the channel`); + } + } + } + bump(); + }); + + client.on('userRemove', (user, actor, reason, ban) => { + if (!live()) return; + const self = client.self; + if (self && user.channelId === self.channelId) { + const by = actor != null && actor !== user.session ? client.users.get(actor)?.name : null; + const what = by ? `${ban ? 'was banned' : 'was kicked'} by ${escapeHtml(by)}` : 'disconnected'; + this.system(`ch:${self.channelId}`, `${escapeHtml(user.name)} ${what}${reason ? `: ${escapeHtml(reason)}` : ''}`); + } + if (this.panel.kind === 'user' && this.panel.session === user.session) this.panel = { kind: 'members' }; + bump(); + }); + + client.on('text', msg => { if (live()) this.receive(client, msg); }); + + client.on('permissionDenied', d => { + if (!live()) return; + // An image preview can exceed what the server accepts; the link alone still gets through + const img = this.lastImageSend; + if (d.type === 4 && img && Date.now() - img.at < 5000) { + this.lastImageSend = null; + this.system(img.key, 'The server refused the image preview, sent the link only'); + client.sendText(this.target(img.key), img.fallback); + return; + } + if (d.channelId != null && d.channelId === this.pendingJoin) this.pendingJoin = null; + const ch = d.channelId != null ? client.channels.get(d.channelId)?.name : undefined; + this.system(this.view, escapeHtml(describeDenial(d, ch))); + }); + + client.on('close', reason => { + if (this.client !== client) return; + this.client = null; + sessions.onClosed(this); + this.status = 'idle'; + this.error = reason === 'Disconnected' ? '' : reason; + this.tick++; + }); + } + + // ─── Panels, descriptions, comments ───────────────────────────────────────── + + showChannel(id: number, tab: 'info' | 'chat' = 'info'): void { + this.panel = { kind: 'channel', id, tab }; + if (tab === 'chat') delete this.unread[`ch:${id}`]; + this.loadDescription(id); + } + + showUser(session: number): void { + this.panel = { kind: 'user', session }; + this.loadComment(session); + } + + closePanel(): void { + this.panel = { kind: 'members' }; + } + + // Long descriptions and comments are only announced by hash; fetch the text on demand + loadDescription(id: number): void { + const c = this.client?.channels.get(id); + if (!c?.descriptionHash || c.description) return; + const tag = `d:${id}:${[...c.descriptionHash].join(',')}`; + if (this.requested.has(tag)) return; + this.requested.add(tag); + this.client!.requestBlob({ descriptions: [id] }); + } + + loadComment(session: number): void { + const u = this.client?.users.get(session); + if (!u?.commentHash || u.comment) return; + const tag = `c:${session}:${[...u.commentHash].join(',')}`; + if (this.requested.has(tag)) return; + this.requested.add(tag); + this.client!.requestBlob({ comments: [session] }); + } + + register(session: number): void { + this.client?.register(session); + } + + // Mumble has no rename while connected: reconnect under the new name (saved for this server) + changeName(name: string): void { + const server = this.server; + if (!server || !name.trim()) return; + const updated = { ...$state.snapshot(server), username: name.trim() }; + store.upsertServer(updated); + this.connect(updated); + } + + // html comes from the rich editor, already sanitized XHTML + setComment(html: string): void { + this.client?.setComment(html); + } + + // Largest description or image message the server accepts (0: no limit) + get htmlLimit(): number { + void this.tick; + return this.client?.config.imageMessageLength ?? 131072; + } + + // ─── Chat ─────────────────────────────────────────────────────────────────── + + private push(key: string, entry: Omit): void { + // Assign first, then push through the stored proxy; pushing onto the raw array would not be reactive + if (!this.logs[key]) this.logs[key] = []; + this.logs[key].push({ ...entry, id: nextId++, time: Date.now() }); + if (sessions.active !== this || !this.visibleKeys().includes(key)) this.unread[key] = (this.unread[key] ?? 0) + 1; + } + + private system(key: string, html: string, trusted = false): void { + const rendered = trusted ? renderIncoming(html, store.trustedHosts) : { html, embeds: [] }; + this.push(key, { kind: 'system', author: '', authorSession: null, self: false, ...rendered }); + } + + private receive(client: MumbleClient, msg: TextMessage): void { + const self = client.self; + const sender = msg.actor != null ? client.users.get(msg.actor) : undefined; + let key: string; + if (msg.actor == null) key = this.view; + else if (msg.channels.length || msg.trees.length) key = `ch:${msg.channels[0] ?? self?.channelId ?? 0}`; + else key = `dm:${msg.actor}`; + + const rendered = renderIncoming(msg.html, store.trustedHosts); + if (msg.actor == null) { + this.push(key, { kind: 'system', author: '', authorSession: null, self: false, ...rendered }); + } else { + this.push(key, { kind: 'text', author: sender ? store.displayName(sender) : `#${msg.actor}`, authorSession: msg.actor, self: false, ...rendered }); + } + } + + // Opening a server marks what is on screen as read + markVisibleRead(): void { + for (const k of this.visibleKeys()) delete this.unread[k]; + } + + selectView(key: ViewKey): void { + this.view = key; + delete this.unread[key]; + } + + // The view follows once the server confirms the move (see the 'user' handler). Murmur + // silently drops own state changes sent too quickly (leaky bucket, about one per second), + // so an unconfirmed join is sent once more before giving up. + joinChannel(id: number): void { + const client = this.client; + if (!client) return; + // Joining a channel is where you want to talk: voice moves to this server + sessions.setVoice(this); + if (this.self?.channelId === id) { + this.selectView(`ch:${id}`); + return; + } + this.pendingJoin = id; + client.joinChannel(id); + const moved = () => this.client !== client || this.pendingJoin !== id || client.self?.channelId === id; + setTimeout(() => { + if (moved()) return; + client.joinChannel(id); + setTimeout(() => { + if (moved()) return; + this.pendingJoin = null; + this.system(this.view, `The server did not move you to ${escapeHtml(client.channels.get(id)?.name || 'Root')}. Try again in a moment.`); + }, 1500); + }, 1200); + } + + private target(key: string): { channels?: number[]; users?: number[] } { + const [kind, id] = key.split(':'); + return kind === 'ch' ? { channels: [Number(id)] } : { users: [Number(id)] }; + } + + // Send HTML to a view and echo it locally (the server does not echo our messages). + sendHtml(html: string, key: string = this.view): boolean { + const client = this.client; + if (!client || this.status !== 'connected') return false; + const limit = / limit) { + this.system(key, `Message too long for this server (${html.length} / ${limit} characters)`); + return false; + } + client.sendText(this.target(key), html); + const rendered = renderIncoming(html, store.trustedHosts); + this.push(key, { kind: 'text', author: client.self?.name ?? '', authorSession: client.session, self: true, ...rendered }); + return true; + } + + sendText(text: string, key: string = this.view): boolean { + const trimmed = text.trim(); + return trimmed ? this.sendHtml(textToHtml(trimmed), key) : false; + } + + async uploadFiles(files: File[], key: string = this.view): Promise { + const { uploadHost, uploadKey, uploadExpiry, sendThumbnails } = store.settings; + if (!uploadHost || !uploadKey) return this.sendInlineImages(files, key); + let info: UploadInfo; + try { + info = await this.loadUploadInfo(); + } catch (e) { + this.system(key, `File sharing unavailable: ${escapeHtml((e as Error).message)}`); + return; + } + for (const f of files) { + const problem = checkFile(info, f); + if (problem) this.system(key, escapeHtml(problem)); + } + for (const file of files.filter(f => !checkFile(info, f))) { + const job = { id: nextId++, key, name: file.name, progress: 0, error: '' }; + this.uploads.push(job); + const live = () => this.uploads.find(j => j.id === job.id)!; + try { + const res = await uploadFile(uploadHost, uploadKey, file, uploadExpiry, p => { live().progress = p; }); + // Well-formed XHTML (
, ): Murmur rejects long messages that do not parse as XML + const link = `${escapeHtml(res.name)} (${formatBytes(res.size)})`; + let html = link; + const cfg = this.client?.config; + if (sendThumbnails && cfg?.allowHtml !== false) { + const budget = Math.min(cfg?.imageMessageLength || 131072, 100000) - link.length - 64; + const thumb = await makeThumbnail(file, budget); + if (thumb) html += `
`; + } + if (html !== link) this.lastImageSend = { key, fallback: link, at: Date.now() }; + this.sendHtml(html, key); + this.uploads = this.uploads.filter(j => j.id !== job.id); + } catch (e) { + live().error = (e as Error).message; + setTimeout(() => { this.uploads = this.uploads.filter(j => j.id !== job.id); }, 6000); + } + } + } + + // Without file hosting: images go inline like a pasted picture in the desktop client, + // scaled to the server's image limit. Other files need an upload host. + private async sendInlineImages(files: File[], key: string): Promise { + const cfg = this.client?.config; + for (const file of files) { + if (!file.type.startsWith('image/')) { + this.system(key, `${escapeHtml(file.name)}: sending files needs file hosting. Set up an upload host in Settings; without one only images can be sent.`); + continue; + } + if (cfg?.allowHtml === false) { + this.system(key, 'This server does not allow images in messages.'); + return; + } + // 0 means no limit; stay reasonable anyway + const limit = cfg?.imageMessageLength || 1024 * 1024; + const uri = await makeInlineImage(file, limit - 64); + if (!uri) { + this.system(key, `${escapeHtml(file.name)}: could not make this image small enough for the server (limit ${formatBytes(limit)})`); + continue; + } + this.sendHtml(``, key); + } + } + + // Cached per host+key; refreshed when settings change + async loadUploadInfo(): Promise { + const { uploadHost, uploadKey } = store.settings; + const id = `${uploadHost}|${uploadKey}`; + if (this.uploadInfo && this.uploadInfoFor === id) return this.uploadInfo; + const info = await fetchUploadInfo(uploadHost, uploadKey); + this.uploadInfo = info; + this.uploadInfoFor = id; + return info; + } + + // ─── Voice state and moderation ───────────────────────────────────────────── + + setSelfMute(v: boolean) { this.client?.setSelfMute(v); } + setSelfDeaf(v: boolean) { this.client?.setSelfDeaf(v); } + + kick(session: number, reason: string, ban = false) { this.client?.kick(session, reason, ban); } + setServerMute(session: number, v: boolean) { this.client?.setUserState(session, { mute: v }); } + setServerDeaf(session: number, v: boolean) { this.client?.setUserState(session, { deaf: v }); } + setPrioritySpeaker(session: number, v: boolean) { this.client?.setUserState(session, { priority_speaker: v }); } + moveUser(session: number, channelId: number) { this.client?.setUserState(session, { channel_id: channelId }); } +} + +// ─── Several servers at once ─────────────────────────────────────────────────── +// Every server has its own Session. Text works on all of them; voice runs on one (like +// Discord): the server where you last joined a channel. On the others you are shown as +// deafened, so nobody expects you to hear them, and your state is restored when voice returns. + +class Sessions { + list = $state([]); + activeId = $state(null); + voiceId = $state(null); + private idle = new Session(); + // Self mute/deaf before we deafened a background server, to restore later + private saved = new Map(); + + get active(): Session { + return this.list.find(s => s.server?.id === this.activeId) ?? this.idle; + } + + byServer(id: string): Session | undefined { + return this.list.find(s => s.server?.id === id); + } + + // Show a server, connecting if needed + open(server: SavedServer): void { + let s = this.byServer(server.id); + this.activeId = server.id; + s?.markVisibleRead(); + if (!s) { + s = new Session(); + this.list.push(s); + s = this.list[this.list.length - 1]; + } + if (s.status === 'idle') s.connect(server); + } + + select(id: string): void { + this.activeId = id; + this.active.markVisibleRead(); + } + + close(id: string): void { + const s = this.byServer(id); + if (!s) return; + s.disconnect(); + this.list = this.list.filter(x => x !== s); + if (this.activeId === id) this.activeId = this.list.find(x => x.status !== 'idle')?.server?.id ?? null; + } + + get voiceSession(): Session | undefined { + return this.list.find(s => s.server?.id === this.voiceId); + } + + onSynced(s: Session): void { + if (!this.voiceId || !this.voiceSession?.client) this.setVoice(s, true); + else this.toBackground(s); + } + + onClosed(s: Session): void { + this.saved.delete(s); + if (this.voiceId !== s.server?.id) return; + voice.detach(); + // Hand voice to another connected server, if any + const next = this.list.find(x => x !== s && x.client && x.status === 'connected'); + this.voiceId = null; + if (next) this.setVoice(next); + } + + setVoice(s: Session, first = false): void { + const id = s.server?.id ?? null; + if (!id || (this.voiceId === id && !first)) return; + const prev = this.voiceSession; + if (prev && prev !== s) { + voice.detach(); + this.toBackground(prev); + } + this.voiceId = id; + this.fromBackground(s); + if (s.client) voice.attach(s.client); + } + + private toBackground(s: Session): void { + const self = s.client?.self; + if (!s.client || !self || this.saved.has(s)) return; + this.saved.set(s, { mute: self.selfMute, deaf: self.selfDeaf }); + if (!self.selfDeaf) s.client.setSelfDeaf(true); + } + + private fromBackground(s: Session): void { + const prev = this.saved.get(s); + this.saved.delete(s); + if (!prev || !s.client || prev.deaf) return; + s.client.send('UserState', { self_deaf: false, self_mute: prev.mute }); + } +} + +export const sessions = new Sessions(); + +// The session of the server on screen. Forwards to the active Session so components keep +// using `session.x`; methods are bound to the real instance so async work stays with its server. +export const session = new Proxy({} as Session, { + get(_t, prop) { + const a = sessions.active; + const v = Reflect.get(a, prop, a); + return typeof v === 'function' ? v.bind(a) : v; + }, + set(_t, prop, value) { + return Reflect.set(sessions.active, prop, value, sessions.active); + } +}); diff --git a/src/lib/settings.svelte.ts b/src/lib/settings.svelte.ts new file mode 100644 index 0000000..b92b7ac --- /dev/null +++ b/src/lib/settings.svelte.ts @@ -0,0 +1,100 @@ +import { load, loadList, save } from './storage.ts'; + +export interface SavedServer { + id: string; + label: string; + host: string; + port: number; + username: string; + password: string; + identityId?: string; // certificate to connect with; the default identity when unset + icon?: string; // custom rail icon, a small square data: image +} + +export interface Settings { + uploadHost: string; // f0ckm base URL, e.g. https://f0ck.dev + uploadKey: string; // upload-only key from scripts/chat-upload-key.mjs + uploadExpiry: string; // 1h / 24h / 7d / 30d + embedHosts: string[]; // extra hosts whose media links render inline + sendThumbnails: boolean; // include a small inline image for vanilla Mumble clients + compact: boolean; + youtube: 'off' | 'click' | 'thumbnail'; // YouTube links: plain, click-to-play card, card with thumbnail +} + +const defaults: Settings = { + uploadHost: '', + uploadKey: '', + uploadExpiry: '30d', + embedHosts: [], + sendThumbnails: true, + compact: false, + youtube: 'click' +}; + +class SettingsStore { + settings = $state(load('mumh5.settings', defaults)); + servers = $state(loadList('mumh5.servers')); + // Pinned sha256 fingerprints of server certificates, keyed by host:port (trust on first use) + pins = $state>(load('mumh5.pins', {})); + // Local nicknames for other users, only visible to us; keyed by certificate hash (or name) + nicknames = $state>(load('mumh5.nicknames', {})); + + saveSettings() { save('mumh5.settings', $state.snapshot(this.settings)); } + + upsertServer(s: SavedServer) { + const i = this.servers.findIndex(x => x.id === s.id); + if (i === -1) this.servers.push(s); + else this.servers[i] = s; + save('mumh5.servers', $state.snapshot(this.servers)); + } + + // Reorder the server list (drag and drop in the rail) + moveServer(id: string, toIndex: number) { + const from = this.servers.findIndex(s => s.id === id); + if (from < 0) return; + const list = [...this.servers]; + const [item] = list.splice(from, 1); + list.splice(Math.max(0, Math.min(list.length, toIndex)), 0, item); + this.servers = list; + save('mumh5.servers', $state.snapshot(this.servers)); + } + + setServerIcon(id: string, icon: string | undefined) { + const s = this.servers.find(x => x.id === id); + if (!s) return; + if (icon) s.icon = icon; + else delete s.icon; + save('mumh5.servers', $state.snapshot(this.servers)); + } + + removeServer(id: string) { + this.servers = this.servers.filter(s => s.id !== id); + save('mumh5.servers', $state.snapshot(this.servers)); + } + + setNickname(key: string, nick: string) { + if (nick.trim()) this.nicknames[key] = nick.trim(); + else delete this.nicknames[key]; + save('mumh5.nicknames', $state.snapshot(this.nicknames)); + } + + // Name to show for a user: our local nickname, else their name + displayName(u: { hash: string; name: string }): string { + return this.nicknames[u.hash || `name:${u.name}`] ?? u.name; + } + + pin(hostPort: string, fingerprint: string) { + this.pins[hostPort] = fingerprint; + save('mumh5.pins', $state.snapshot(this.pins)); + } + + // Hosts whose media links are embedded: the upload host plus user-added ones. + // Other links stay plain links, so opening a chat never pings arbitrary servers. + get trustedHosts(): string[] { + const hosts = [...this.settings.embedHosts]; + try { if (this.settings.uploadHost) hosts.push(new URL(this.settings.uploadHost).host); } catch { /* invalid URL */ } + return hosts.map(h => h.trim().toLowerCase()).filter(Boolean); + } +} + +export const store = new SettingsStore(); diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..f16853b --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,23 @@ +// localStorage wrapper that tolerates missing or broken storage. +export function load(key: string, fallback: T): T { + try { + const raw = localStorage.getItem(key); + return raw ? { ...fallback, ...JSON.parse(raw) } : fallback; + } catch { + return fallback; + } +} + +export function loadList(key: string): T[] { + try { + const raw = localStorage.getItem(key); + const v = raw ? JSON.parse(raw) : []; + return Array.isArray(v) ? v : []; + } catch { + return []; + } +} + +export function save(key: string, value: unknown): void { + try { localStorage.setItem(key, JSON.stringify(value)); } catch { /* storage unavailable */ } +} diff --git a/src/lib/tray.svelte.ts b/src/lib/tray.svelte.ts new file mode 100644 index 0000000..3c8cd0a --- /dev/null +++ b/src/lib/tray.svelte.ts @@ -0,0 +1,80 @@ +import { native } from './native.ts'; +import { ICON_PATHS } from './icons.ts'; +import { session, sessions } from './session.svelte.ts'; +import { voice, type TransmitMode } from './audio/voice.svelte.ts'; +import { ui } from './ui.svelte.ts'; + +type TrayLook = 'offline' | 'idle' | 'talking' | 'muted' | 'deafened'; + +const LOOK: Record = { + offline: { bg: '#4a4c54', glyph: 'mic' }, + idle: { bg: '#7c6cff', glyph: 'mic' }, + talking: { bg: '#2fb863', glyph: 'mic' }, + muted: { bg: '#e0443e', glyph: 'mic-off' }, + deafened: { bg: '#e0443e', glyph: 'headphones-off' } +}; + +const cache = new Map(); + +// Square tile with a white glyph, like the rail tiles; 64 px so the OS can scale it down sharply +function drawIcon(look: TrayLook): string { + const hit = cache.get(look); + if (hit) return hit; + const size = 64; + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = size; + const ctx = canvas.getContext('2d')!; + ctx.fillStyle = LOOK[look].bg; + ctx.fillRect(0, 0, size, size); + ctx.translate(8, 8); + ctx.scale(2, 2); // 24 unit glyph into 48 px + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2.6; + ctx.lineCap = 'square'; + ctx.lineJoin = 'miter'; + ctx.stroke(new Path2D(ICON_PATHS[LOOK[look].glyph])); + const url = canvas.toDataURL('image/png'); + cache.set(look, url); + return url; +} + +// Mirrors voice state into the system tray and handles its menu +export function startTray(): void { + if (!native) return; + const api = native; + + $effect.root(() => { + $effect(() => { + const self = session.self; + const voiceSession = sessions.voiceSession; + const connected = session.status === 'connected'; + const vSelf = voiceSession?.self; + const muted = !!(vSelf ? vSelf.selfMute || vSelf.mute : self?.selfMute); + const deafened = !!(vSelf ? vSelf.selfDeaf || vSelf.deaf : self?.selfDeaf); + const look: TrayLook = !connected && !voiceSession ? 'offline' + : deafened ? 'deafened' : muted ? 'muted' : voice.transmitting ? 'talking' : 'idle'; + const server = voiceSession?.server?.label ?? session.server?.label ?? ''; + const state = { offline: 'not connected', idle: 'connected', talking: 'talking', muted: 'muted', deafened: 'deafened' }[look]; + api.updateTray({ + icon: drawIcon(look), + tooltip: server ? `mumh5: ${state} on ${server}` : 'mumh5: not connected', + connected: !!(voiceSession?.status === 'connected' || connected), + server, + muted, + deafened, + mode: voice.settings.mode + }); + }); + }); + + api.onTrayAction(action => { + // Tray actions apply where voice runs + const target = sessions.voiceSession ?? sessions.active; + const self = target.self; + if (action === 'toggle-mute' && self) target.setSelfMute(!self.selfMute); + else if (action === 'toggle-deafen' && self) target.setSelfDeaf(!self.selfDeaf); + else if (action.startsWith('mode:')) voice.update({ mode: action.slice(5) as TransmitMode }); + else if (action === 'settings') { ui.settingsTab = 'voice'; ui.settingsOpen = true; } + else if (action === 'disconnect' && target.server) sessions.close(target.server.id); + }); +} diff --git a/src/lib/ui.svelte.ts b/src/lib/ui.svelte.ts new file mode 100644 index 0000000..24ebb73 --- /dev/null +++ b/src/lib/ui.svelte.ts @@ -0,0 +1,35 @@ +import type { CertDetails } from '../core/transport.ts'; + +export interface PromptRequest { + title: string; + label: string; + confirm: string; + danger?: boolean; + input?: 'text' | 'password' | 'none'; + initial?: string; + text?: string; + onsubmit: (value: string) => void; +} + +export interface CertRequest { + title: string; + chain: CertDetails[]; + status?: { ok: boolean; text: string }; +} + +// App-wide dialogs that can be opened from anywhere (menus, panels) +class Ui { + prompt = $state(null); + cert = $state(null); + settingsOpen = $state(false); + wizard = $state<'setup' | 'add' | null>(null); + // Quick audio popover from right-clicking mute (input) or deafen (output) + quickAudio = $state<{ kind: 'input' | 'output'; x: number; y: number } | null>(null); + settingsTab = $state<'voice' | 'files' | 'identities'>('voice'); + // Session whose connection information is shown + userInfo = $state(null); + // Set when a panel was opened on a layout where it is an overlay + panelOpen = $state(false); +} + +export const ui = new Ui(); diff --git a/src/lib/upload.ts b/src/lib/upload.ts new file mode 100644 index 0000000..69b95fe --- /dev/null +++ b/src/lib/upload.ts @@ -0,0 +1,118 @@ +export interface UploadResult { + url: string; + name: string; + mime: string; + inline: boolean; + size: number; + expires_at: number; + delete_token: string; +} + +export interface UploadInfo { + allowed_mimes: string[]; + allowed_extensions: string[]; + max_bytes: number; +} + +const endpoint = (host: string) => host.replace(/\/+$/, '') + '/api/chat/upload'; + +// Allowed types and limits of the upload host; also serves as a key check. +export async function fetchUploadInfo(host: string, key: string): Promise { + const r = await fetch(endpoint(host), { headers: { 'X-API-Key': key } }); + const body = await r.json().catch(() => null); + if (r.status === 401) throw new Error('Key rejected'); + if (!r.ok || !body?.success) throw new Error(body?.msg || `Unexpected response (HTTP ${r.status})`); + return body; +} + +// Client-side pre-check; the host verifies the real type again after upload +export function checkFile(info: UploadInfo, file: File): string | null { + const ext = /\.([a-z0-9]+)$/i.exec(file.name)?.[1]?.toLowerCase() ?? ''; + const typeOk = file.type ? info.allowed_mimes.includes(file.type) : info.allowed_extensions.includes(ext); + if (!typeOk) return `${file.name}: file type not allowed on this upload host`; + if (file.size > info.max_bytes) return `${file.name}: too large (max ${Math.round(info.max_bytes / 1048576)} MB)`; + return null; +} + +// Value for +export function acceptAttr(info: UploadInfo | null): string { + if (!info) return ''; + return [...info.allowed_mimes, ...info.allowed_extensions.map(e => '.' + e)].join(','); +} + +// Upload one file to a f0ckm chat-upload endpoint. XHR instead of fetch for upload progress. +export function uploadFile( + host: string, key: string, file: File, expiry: string, onProgress: (fraction: number) => void +): Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', endpoint(host)); + xhr.setRequestHeader('X-API-Key', key); + xhr.setRequestHeader('X-Filename', encodeURIComponent(file.name)); + xhr.setRequestHeader('X-Upload-Expiry', expiry); + if (file.type) xhr.setRequestHeader('Content-Type', file.type); + xhr.upload.onprogress = e => { if (e.lengthComputable) onProgress(e.loaded / e.total); }; + xhr.onerror = () => reject(new Error('Network error')); + xhr.onload = () => { + let body: any = null; + try { body = JSON.parse(xhr.responseText); } catch { /* not JSON */ } + if (xhr.status === 200 && body?.success) resolve(body); + else reject(new Error(body?.msg || `Upload failed (HTTP ${xhr.status})`)); + }; + xhr.send(file); + }); +} + +const readDataUrl = (file: Blob) => new Promise((resolve, reject) => { + const r = new FileReader(); + r.onload = () => resolve(String(r.result)); + r.onerror = () => reject(r.error); + r.readAsDataURL(file); +}); + +// JPEG data URI of an image, scaled down until it fits in maxChars. Mumble clients render +// inline data: images, which is also how the desktop client sends pasted pictures. +export async function makeInlineImage(file: File, maxChars: number, sizes = [1280, 1024, 800, 640, 480, 320, 200, 120]): Promise { + if (!/^image\/(png|jpeg|gif|webp|avif|bmp)$/.test(file.type) || maxChars < 2000) return null; + // Small enough as it is: send the original (keeps GIF animation and PNG transparency) + if (/^image\/(png|jpeg|gif|webp)$/.test(file.type) && Math.ceil(file.size / 3) * 4 + 40 < maxChars) { + return readDataUrl(file); + } + try { + const bmp = await createImageBitmap(file); + for (const size of sizes) { + const scale = Math.min(1, size / Math.max(bmp.width, bmp.height)); + if (scale === 1 && size !== sizes[0] && Math.max(bmp.width, bmp.height) < size) continue; + const canvas = document.createElement('canvas'); + canvas.width = Math.max(1, Math.round(bmp.width * scale)); + canvas.height = Math.max(1, Math.round(bmp.height * scale)); + canvas.getContext('2d')!.drawImage(bmp, 0, 0, canvas.width, canvas.height); + for (const q of [0.85, 0.7, 0.5]) { + const uri = canvas.toDataURL('image/jpeg', q); + if (uri.length < maxChars) return uri; + } + } + } catch { /* undecodable image */ } + return null; +} + +// Small preview next to an f0ckm link, for vanilla Mumble clients +export function makeThumbnail(file: File, maxChars: number): Promise { + return makeInlineImage(file, maxChars, [320, 200, 120]); +} + +// Square server icon: center-cropped, scaled to 128x128, as a small WebP data URI +export async function makeServerIcon(file: File, size = 128): Promise { + const bmp = await createImageBitmap(file); + const side = Math.min(bmp.width, bmp.height); + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = size; + const ctx = canvas.getContext('2d')!; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(bmp, (bmp.width - side) / 2, (bmp.height - side) / 2, side, side, 0, 0, size, size); + bmp.close(); + const webp = canvas.toDataURL('image/webp', 0.85); + // Browsers without WebP encoding fall back to PNG + return webp.startsWith('data:image/webp') ? webp : canvas.toDataURL('image/png'); +} + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..037036c --- /dev/null +++ b/src/main.ts @@ -0,0 +1,7 @@ +import { mount } from 'svelte'; +import './app.css'; +import App from './App.svelte'; +import { startTray } from './lib/tray.svelte.ts'; + +mount(App, { target: document.getElementById('app')! }); +startTray(); diff --git a/src/ui/Avatar.svelte b/src/ui/Avatar.svelte new file mode 100644 index 0000000..d5e7796 --- /dev/null +++ b/src/ui/Avatar.svelte @@ -0,0 +1,19 @@ + + + + {(name.trim()[0] ?? '?').toUpperCase()} + + + diff --git a/src/ui/CertDialog.svelte b/src/ui/CertDialog.svelte new file mode 100644 index 0000000..53f6ab2 --- /dev/null +++ b/src/ui/CertDialog.svelte @@ -0,0 +1,22 @@ + + + (session.certPrompt = null)} width={520}> +

The certificate of {p.hostPort} is different from the one it had before. This happens when a server is reinstalled, but it can also mean someone is intercepting the connection. Your password has not been sent.

+

Previous

{p.previous} +

Now

{p.fingerprint} + {#snippet footer()} + + + {/snippet} + + + diff --git a/src/ui/CertViewer.svelte b/src/ui/CertViewer.svelte new file mode 100644 index 0000000..3532910 --- /dev/null +++ b/src/ui/CertViewer.svelte @@ -0,0 +1,73 @@ + + + + {#if status}

{status.text}

{/if} + {#if chain.length > 1} +
+ {#each chain as c, i (c.fingerprint256)} + + {/each} +
+ {/if} + {#if cert} +

Subject

+
{#each parts(cert.subject) as [k, v] (k + v)}
{names[k] ?? k}
{v}
{/each}
+

Issuer {#if cert.selfSigned}self-signed{/if}

+
{#each parts(cert.issuer) as [k, v] (k + v)}
{names[k] ?? k}
{v}
{/each}
+

Validity {#if expired}expired{/if}

+
+
Not before
{date(cert.validFrom)}
+
Not after
{date(cert.validTo)}
+
+

Details

+
+
Public key
{cert.keyType.toUpperCase()}{cert.keyBits ? ` ${cert.keyBits} bit` : ''}
+ {#if cert.subjectAltName}
Alt names
{cert.subjectAltName}
{/if} +
Serial
{cert.serialNumber}
+
SHA-1
{cert.fingerprint}
+
SHA-256
{cert.fingerprint256}
+
+ {/if} + {#snippet footer()} + + + {/snippet} +
+ + diff --git a/src/ui/ChannelNode.svelte b/src/ui/ChannelNode.svelte new file mode 100644 index 0000000..55b4941 --- /dev/null +++ b/src/ui/ChannelNode.svelte @@ -0,0 +1,116 @@ + + +
  • +
    menu.show(e, channelMenu(channel))} role="presentation"> + {#if children.length || users.length} + + {:else} + + {/if} + +
    + + {#if users.length && expanded} +
      + {#each users as u (u.session)} + {@const isSelf = u.session === session.self?.session} + {@const clicks = clickOrDouble( + () => { session.showUser(u.session); ui.panelOpen = true; }, + () => { if (!isSelf) { session.selectView(`dm:${u.session}`); onnavigate(); } } + )} +
    • + +
    • + {/each} +
    + {/if} + {#if children.length && expanded} +
      + {#each children as c (c.id)} + + {/each} +
    + {/if} +
  • + + diff --git a/src/ui/Chat.svelte b/src/ui/Chat.svelte new file mode 100644 index 0000000..f41c737 --- /dev/null +++ b/src/ui/Chat.svelte @@ -0,0 +1,58 @@ + + +
    +
    + {#if showMenu}{/if} + +

    {title}

    + {#if topic}{topic}{/if} + + +
    + + + {#snippet empty()} +
    + {#if session.status === 'connected'} +

    {isDm ? `Direct messages with ${title}` : `Welcome to #${title}`}

    +

    {isDm ? 'Messages here are only visible to the two of you.' : 'Messages go to everyone in this channel. Drop files here to share them.'}

    + {:else if session.status === 'connecting'} +

    Connecting...

    + {:else} +

    mumh5

    +

    Choose a server to get started.

    + {/if} +
    + {/snippet} +
    + + +
    + + diff --git a/src/ui/ChatLog.svelte b/src/ui/ChatLog.svelte new file mode 100644 index 0000000..bf236da --- /dev/null +++ b/src/ui/ChatLog.svelte @@ -0,0 +1,63 @@ + + +
    { if (e.dataTransfer?.types.includes('Files')) dragging++; }} + ondragleave={() => (dragging = Math.max(0, dragging - 1))} + ondragover={e => e.preventDefault()} {ondrop}> +
    (pinned = list.scrollHeight - list.scrollTop - list.clientHeight < 40)} + onloadcapture={onload} onloadedmetadatacapture={onload}> + {#if !entries.length}{@render empty()}{/if} + {#each entries as entry, i (entry.id)} + + {/each} +
    +
    + {#if dragging} +

    Drop to upload to {key.startsWith('dm:') ? '@' : '#'}{title}

    + {/if} +
    + + diff --git a/src/ui/Composer.svelte b/src/ui/Composer.svelte new file mode 100644 index 0000000..982cabd --- /dev/null +++ b/src/ui/Composer.svelte @@ -0,0 +1,87 @@ + + +
    + {#if uploads.length} +
      + {#each uploads as u (u.id)} +
    • + + {u.name} + {#if u.error}{u.error}{:else}{/if} +
    • + {/each} +
    + {/if} +
    + + { session.uploadFiles([...(picker.files ?? [])], target); picker.value = ''; }} /> + + +
    +
    + + diff --git a/src/ui/ConnectDialog.svelte b/src/ui/ConnectDialog.svelte new file mode 100644 index 0000000..68fcf1c --- /dev/null +++ b/src/ui/ConnectDialog.svelte @@ -0,0 +1,105 @@ + + + +
    + +
    + + +
    + + + + + {#if identities.list.length > 1} + + + {/if} + + + +
    + + {#if icon}{:else}{(label || host || '?').trim().slice(0, 2).toUpperCase()}{/if} + + + {#if icon}{/if} + +
    + {#if iconError}

    {iconError}

    {/if} +
    + {#snippet footer()} + {#if initial}{/if} + + + {/snippet} +
    + + diff --git a/src/ui/ContextMenu.svelte b/src/ui/ContextMenu.svelte new file mode 100644 index 0000000..f4870bc --- /dev/null +++ b/src/ui/ContextMenu.svelte @@ -0,0 +1,64 @@ + + + menu.close()} onresize={() => menu.close()} /> + +{#if menu.open} + + +{/if} + + diff --git a/src/ui/Icon.svelte b/src/ui/Icon.svelte new file mode 100644 index 0000000..1587ff1 --- /dev/null +++ b/src/ui/Icon.svelte @@ -0,0 +1,10 @@ + + + diff --git a/src/ui/IdentityList.svelte b/src/ui/IdentityList.svelte new file mode 100644 index 0000000..9df9019 --- /dev/null +++ b/src/ui/IdentityList.svelte @@ -0,0 +1,83 @@ + + +
      + {#each identities.list as i (i.id)} +
    • +
      + {i.name} + {#if i.isDefault}default{/if} +
      + {i.fingerprint} + {#if usedBy(i.id).length}

      Used for {usedBy(i.id).join(', ')}

      {/if} +
      + {#if !i.isDefault}{/if} + + + + +
      +
    • + {:else} +
    • No identity yet; one is created when you first connect.
    • + {/each} +
    + +{#if message}

    {message}

    {/if} + + diff --git a/src/ui/IdentityWizard.svelte b/src/ui/IdentityWizard.svelte new file mode 100644 index 0000000..24e77a1 --- /dev/null +++ b/src/ui/IdentityWizard.svelte @@ -0,0 +1,178 @@ + + + {} : onclose} width={560}> + {#if step === 'welcome'} +

    Mumble recognizes you by a certificate, not by a password. Registered names, admin rights and friends lists on servers are tied to it, so it is worth setting up once and keeping a backup.

    +
    + + {#if mumble} + + {/if} + + {#if existing} + + {/if} +
    + {:else if step === 'create'} +
    +

    The name is stored in the certificate and helps you tell identities apart. Servers show the username you enter when connecting, not this.

    + + + + + +
    + {:else if step === 'import'} +
    + + (file = (e.currentTarget as HTMLInputElement).files?.[0] ?? null)} /> + + +
    + {:else if step === 'backup' && result} +

    Identity {result.name} is ready.

    +

    {result.fingerprint}

    +

    If this computer dies, the certificate goes with it, and so do your registrations. Save a password-protected backup now; the desktop Mumble client can import it too.

    + + + + + {:else if step === 'done'} +

    You are set up{result ? ` as ${result.name}` : ''}.

    + {#if result}

    {result.fingerprint}

    {/if} + {#if savedTo}

    Backup saved to {savedTo}

    {/if} +

    Manage identities, pick one per server and export backups in Settings.

    + {/if} + + {#if error}

    {error}

    {/if} + + {#snippet footer()} + {#if step === 'welcome'} + {#if mode === 'add'}{/if} + {:else if step === 'create'} + + + {:else if step === 'import'} + + + {:else if step === 'backup'} + + + {:else} + + {/if} + {/snippet} +
    + + diff --git a/src/ui/Members.svelte b/src/ui/Members.svelte new file mode 100644 index 0000000..2901518 --- /dev/null +++ b/src/ui/Members.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/src/ui/Message.svelte b/src/ui/Message.svelte new file mode 100644 index 0000000..7505093 --- /dev/null +++ b/src/ui/Message.svelte @@ -0,0 +1,80 @@ + + +{#if entry.kind === 'system'} +
    + +
    {@html entry.html} {hhmm}
    +
    +{:else} +
    +
    + {#if grouped} + {hhmm} + {:else} + + {/if} +
    +
    + {#if !grouped} +
    + {entry.author} + +
    + {/if} + {#if entry.html}
    {@html entry.html}
    {/if} + {#each entry.embeds as m (m.url)} + {#if m.kind === 'youtube'} + {#if store.settings.youtube !== 'off'}
    {/if} + {:else} +
    + {#if m.kind === 'image'} + {m.name} + {:else if m.kind === 'video'} + + {:else} + + {/if} +
    + {/if} + {/each} +
    +
    +{/if} + + diff --git a/src/ui/Modal.svelte b/src/ui/Modal.svelte new file mode 100644 index 0000000..e65cdb5 --- /dev/null +++ b/src/ui/Modal.svelte @@ -0,0 +1,35 @@ + + + { if (e.key === 'Escape') onclose(); }} /> + + + + diff --git a/src/ui/PromptDialog.svelte b/src/ui/PromptDialog.svelte new file mode 100644 index 0000000..0b2827e --- /dev/null +++ b/src/ui/PromptDialog.svelte @@ -0,0 +1,29 @@ + + + +
    { e.preventDefault(); onsubmit(value); onclose(); }}> + {#if text}

    {text}

    {/if} + {#if input !== 'none'} + + + + {/if} +
    + {#snippet footer()} + + + {/snippet} +
    + + diff --git a/src/ui/QuickAudio.svelte b/src/ui/QuickAudio.svelte new file mode 100644 index 0000000..34b5906 --- /dev/null +++ b/src/ui/QuickAudio.svelte @@ -0,0 +1,77 @@ + + + { if (e.key === 'Escape') close(); }} onresize={close} /> + + + + + diff --git a/src/ui/RichEditor.svelte b/src/ui/RichEditor.svelte new file mode 100644 index 0000000..e63f83c --- /dev/null +++ b/src/ui/RichEditor.svelte @@ -0,0 +1,166 @@ + + +
    + + +
    + {#if source} + + {/if} + +
    + {#if error}{error}{/if} + + {formatBytes(source ? sourceText.length : size)}{limit ? ` / ${formatBytes(limit)}` : ''} +
    +
    + + diff --git a/src/ui/RightPanel.svelte b/src/ui/RightPanel.svelte new file mode 100644 index 0000000..1cb7d42 --- /dev/null +++ b/src/ui/RightPanel.svelte @@ -0,0 +1,239 @@ + + + setWidth(width)} /> + + + + diff --git a/src/ui/ServerRail.svelte b/src/ui/ServerRail.svelte new file mode 100644 index 0000000..40dc2b0 --- /dev/null +++ b/src/ui/ServerRail.svelte @@ -0,0 +1,127 @@ + + + + + diff --git a/src/ui/SettingsDialog.svelte b/src/ui/SettingsDialog.svelte new file mode 100644 index 0000000..9279113 --- /dev/null +++ b/src/ui/SettingsDialog.svelte @@ -0,0 +1,105 @@ + + + { saveFiles(); onclose(); }} width={640}> +
    + {#each tabs as t (t.id)} + + {/each} +
    + + {#if tab === 'voice'} + + {:else if tab === 'files'} +

    Files go to a f0ckm instance and are posted as links. Other mumh5 users see them inline; regular Mumble users get a link and a small preview. Without an upload host you can still send images, inline and scaled to the server's limit.

    + + + + +
    +
    + + +
    + +
    + {#if testResult}

    {testResult}

    {/if} + + +

    Embeds

    + + +

    Links from other hosts stay plain links, so opening a chat never contacts servers you did not choose.

    + + + {:else} +

    Servers recognize you by your certificate. The default is used unless a server is set to another one.

    + + {/if} + + {#snippet footer()} + + {/snippet} +
    + + diff --git a/src/ui/Sidebar.svelte b/src/ui/Sidebar.svelte new file mode 100644 index 0000000..d4a58ee --- /dev/null +++ b/src/ui/Sidebar.svelte @@ -0,0 +1,144 @@ + + + + + diff --git a/src/ui/UserInfoDialog.svelte b/src/ui/UserInfoDialog.svelte new file mode 100644 index 0000000..e33444a --- /dev/null +++ b/src/ui/UserInfoDialog.svelte @@ -0,0 +1,149 @@ + + + + {#if !stats} +

    Asking the server...

    + {:else} +

    Connection

    +
    +
    Online for
    {duration(stats.onlinesecs)}
    + {#if stats.idlesecs != null}
    Idle for
    {duration(stats.idlesecs)}
    {/if} +
    Ping (TCP)
    {ping(stats.tcp_ping_avg, stats.tcp_ping_var)}
    +
    Ping (UDP)
    {stats.udp_packets ? ping(stats.udp_ping_avg, stats.udp_ping_var) : 'not using UDP (voice over TCP)'}
    +
    Packets
    {stats.tcp_packets ?? 0} TCP, {stats.udp_packets ?? 0} UDP
    + {#if stats.bandwidth != null}
    Bandwidth
    {formatBytes(stats.bandwidth)}/s
    {/if} + {#if stats.address?.length}
    Address
    {address(stats.address)}
    {/if} +
    + + {#if stats.version} +

    Client

    +
    +
    Version
    {version || 'unknown'}
    + {#if stats.version.os}
    System
    {stats.version.os} {stats.version.os_version ?? ''}
    {/if} +
    Opus
    {stats.opus ? 'supported' : 'no (legacy codecs only)'}
    +
    + {/if} + + {#if stats.from_client || stats.from_server} +

    Voice packets

    + + + + + + +
    GoodLateLostResyncLoss
    From client{stats.from_client?.good ?? 0}{stats.from_client?.late ?? 0}{stats.from_client?.lost ?? 0}{stats.from_client?.resync ?? 0}{loss(stats.from_client)}
    To client{stats.from_server?.good ?? 0}{stats.from_server?.late ?? 0}{stats.from_server?.lost ?? 0}{stats.from_server?.resync ?? 0}{loss(stats.from_server)}
    +

    Counted on the encrypted UDP channel; clients sending voice over TCP show zeros.

    + {/if} + + {#if stats.certificates?.length} +

    Certificate

    +
    +
    Subject
    {certs[0]?.subject.replace(/\n/g, ', ') ?? '...'}
    +
    Verified
    {stats.strong_certificate ? 'yes, signed by a trusted authority' : 'no, self-signed'}
    +
    + {#if certs.length} + + {/if} + {:else if user?.hash} +

    Certificate

    +
    SHA-1
    {user.hash}
    +

    Only admins and the user themselves can see the full certificate.

    + {/if} + + {#if !stats.version} +

    Client details, address and certificate are only shown to admins and to the user themselves.

    + {/if} + {/if} + + {#snippet footer()} + {#if updated}Updates every 5 seconds{/if} + + {/snippet} +
    + + diff --git a/src/ui/VoiceSettings.svelte b/src/ui/VoiceSettings.svelte new file mode 100644 index 0000000..19c159e --- /dev/null +++ b/src/ui/VoiceSettings.svelte @@ -0,0 +1,173 @@ + + + + +
    + {#if voice.error}

    {voice.error}

    {/if} + +

    Devices

    +
    +
    + + +
    +
    + + +
    +
    + +

    Transmit

    +
    + {#each modes as m (m.id)} + + {/each} +
    + + {#if s.mode === 'ptt'} + +
    + + {voice.pttDown ? 'Talking' : 'Hold the key to talk'} +
    +

    Works while mumh5 is focused. System-wide push to talk is planned.

    + {/if} + + + + {#if s.mode === 'vad'} + voice.update({ vadThreshold: Number(e.currentTarget.value) })} aria-label="Voice activity threshold" /> +

    Speak normally and put the marker just below your voice; the bar turns green when you would be sending. Threshold {s.vadThreshold} dB.

    + + voice.update({ vadHoldMs: Number(e.currentTarget.value) })} /> + {/if} + +
    +
    + + voice.update({ inputGain: Number(e.currentTarget.value) })} /> +
    +
    + + voice.update({ outputVolume: Number(e.currentTarget.value) })} /> +
    +
    + +
    + + {voice.testing ? 'You should hear yourself. Use headphones to avoid feedback.' : 'Plays your microphone back to you'} +
    + +

    Processing

    + + + + +

    Quality

    +
    +
    + + voice.update({ bitrate: Number(e.currentTarget.value) })} /> + {#if voice.effectiveBitrate && voice.effectiveBitrate < s.bitrate && session.status === 'connected'} +

    Limited to {Math.round(voice.effectiveBitrate / 1000)} kbit/s by this server

    + {/if} +
    +
    + + +
    +
    + + voice.update({ jitterMs: Number(e.currentTarget.value) })} /> +

    Higher values smooth out bad connections at the cost of delay.

    + + {#if session.status === 'connected'} +

    Statistics

    +

    Sent {voice.stats.sent} packets, received {voice.stats.received}, decoded {voice.stats.decoded}, gaps {voice.stats.lost}. Transport: TCP tunnel.

    + {/if} +
    + + diff --git a/src/ui/YouTubeEmbed.svelte b/src/ui/YouTubeEmbed.svelte new file mode 100644 index 0000000..c0f4acb --- /dev/null +++ b/src/ui/YouTubeEmbed.svelte @@ -0,0 +1,42 @@ + + +
    + {#if playing} + + {:else} + + {/if} + +
    + + diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..36c98fd --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,3 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { preprocess: vitePreprocess() }; diff --git a/test/e2e/app.e2e.ts b/test/e2e/app.e2e.ts new file mode 100644 index 0000000..86235d1 --- /dev/null +++ b/test/e2e/app.e2e.ts @@ -0,0 +1,561 @@ +// Drives the built Electron app against a real Mumble server, with a headless second client. +// npm run build && MUMBLE_TEST_HOST=localhost:64739 npm run test:e2e +// Optional: UPLOAD_HOST + UPLOAD_KEY to also test file sharing, SHOTS_DIR to save screenshots. +import { _electron as electron } from 'playwright-core'; +import electronPath from 'electron'; +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import zlib from 'node:zlib'; +import { createCodec } from '../../src/core/proto.ts'; +import { encodeVoice, decodeVoice, type VoicePacket } from '../../src/core/voice-packet.ts'; +import { MumbleClient } from '../../src/core/client.ts'; +import type { Transport } from '../../src/core/transport.ts'; +import { openTls } from '../../electron/tls-transport.ts'; +import { generateIdentity } from '../../electron/identity.ts'; + +const target = process.env.MUMBLE_TEST_HOST; +if (!target) { + console.log('MUMBLE_TEST_HOST not set, skipping'); + process.exit(0); +} +const [host, port] = target.split(':'); +const shots = process.env.SHOTS_DIR; +const root = path.resolve(import.meta.dirname, '../..'); + +function headless(username: string, password = ''): Promise { + const codec = createCodec(); + const id = generateIdentity(username); + const client = new MumbleClient(codec); + const t: Transport = { onData: null, onClose: null, send: b => conn.send(b), close: () => conn.close() }; + const conn = openTls(host, Number(port), id.certPem, id.keyPem, { + onSecure: () => {}, onData: c => t.onData?.(c), onClose: r => t.onClose?.(r) + }); + client.connect(t, { username, password, os: 'test' }); + return new Promise((res, rej) => { client.on('synced', () => res(client)); client.on('close', rej); }); +} + +// Noisy PNG, no image library needed. Noise keeps the JPEG preview big enough (over the +// server's 5000-char text limit) to exercise Murmur's XML check on image messages. +function testPng(w = 400, h = 300): Buffer { + const raw = Buffer.alloc((w * 3 + 1) * h); + for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { + raw.set([(x * 7 + Math.random() * 90) & 255, (y * 5 + Math.random() * 90) & 255, Math.random() * 255], y * (w * 3 + 1) + 1 + x * 3); + } + const chunk = (type: string, data: Buffer) => { + const len = Buffer.alloc(4); len.writeUInt32BE(data.length); + const td = Buffer.concat([Buffer.from(type), data]); + const crc = Buffer.alloc(4); crc.writeUInt32BE(zlib.crc32(td)); + return Buffer.concat([len, td, crc]); + }; + const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr.set([8, 2, 0, 0, 0], 8); + return Buffer.concat([Buffer.from('89504e470d0a1a0a', 'hex'), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]); +} + +// Fails with a label instead of hanging when an expected event never comes +function within(p: Promise, label: string, ms = 10000): Promise { + return Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error(`Timed out waiting for: ${label}`)), ms))]); +} + +const { ELECTRON_RUN_AS_NODE, ...env } = process.env; +const userData = mkdtempSync(path.join(tmpdir(), 'mumh5-e2e-')); +const app = await electron.launch({ + executablePath: electronPath as unknown as string, + // Stay on $DISPLAY (e.g. Xvfb); Electron would otherwise pick the Wayland session automatically + // Fake microphone (a beeping tone) and auto-granted permission for the voice tests + args: [root, `--user-data-dir=${userData}`, '--ozone-platform=x11', '--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'], + env: env as Record +}); +const bob = await headless('bob'); +// SuperUser can create channels; needs MUMBLE_SUPERUSER_PASSWORD on the test server +const admin = process.env.MUMBLE_SUPERUSER_PASSWORD ? await headless('SuperUser', process.env.MUMBLE_SUPERUSER_PASSWORD) : null; +try { + const page = await app.firstWindow(); + page.on('console', m => { if (m.type() === 'error') console.log('[renderer]', m.text()); }); + await page.setViewportSize({ width: 1280, height: 800 }); + + // First start: identity setup wizard + await page.getByRole('dialog', { name: 'Set up your identity' }).waitFor(); + await page.getByRole('button', { name: /Create a new identity/ }).click(); + await page.getByLabel('Name').fill('alice'); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + await page.getByRole('button', { name: 'Skip for now' }).click(); + await page.getByRole('button', { name: 'Done' }).click(); + await page.getByRole('dialog', { name: 'Set up your identity' }).waitFor({ state: 'detached' }); + console.log('ok: identity wizard creates an identity'); + + // Settings: upload host + key + if (process.env.UPLOAD_HOST && process.env.UPLOAD_KEY) { + await page.getByTitle('Settings').click(); + await page.getByRole('tab', { name: 'Chat and files' }).click(); + await page.getByLabel('Upload host').fill(process.env.UPLOAD_HOST); + await page.getByLabel('Upload key').fill(process.env.UPLOAD_KEY); + await page.getByRole('button', { name: 'Test key' }).click(); + await page.getByText(/^Key works\./).waitFor(); + await page.getByRole('button', { name: 'Done', exact: true }).click(); + } + + // Add and connect to the server + await page.getByTitle('Add a server').click(); + await page.getByLabel('Address').fill(host); + await page.getByLabel('Port').fill(port); + await page.getByLabel('Username').fill('alice'); + await page.getByLabel('Label').fill('Test Server'); + await page.getByRole('button', { name: 'Save and connect' }).click(); + await page.getByText(/Connected/).first().waitFor(); + console.log('ok: connected'); + + // Bob is listed in the channel tree + await page.locator('.sidebar').getByText('bob').waitFor(); + console.log('ok: sees bob'); + + // ─── Voice ─────────────────────────────────────────────────────────────── + const aliceSession = () => [...bob.users.values()].find(u => u.name === 'alice')!.session; + const collect = (n: number, stopOnLast = false) => new Promise(res => { + const got: VoicePacket[] = []; + const off = bob.on('voice', raw => { + const p = decodeVoice(raw); + if (!p || p.session !== aliceSession()) return; + got.push(p); + if (got.length >= n || (stopOnLast && p.last)) { off(); res(got); } + }); + }); + const setMode = async (label: string) => { + await page.getByTitle('Settings').click(); + await page.getByRole('radio', { name: new RegExp(label) }).click(); + await page.getByRole('button', { name: 'Done', exact: true }).click(); + }; + + // Always on: a steady stream of 20 ms Opus packets + const stream = collect(25); + await setMode('Always on'); + const packets = await within(stream, 'voice packets from the app'); + assert.ok(packets.every(p => p.opus.length > 0 && !p.last)); + const steps = packets.slice(1).map((p, i) => p.frame - packets[i].frame); + assert.ok(steps.every(d => d === 2), `sequence advances 2 frames per 20 ms packet: ${steps.join(',')}`); + console.log(`ok: app sends Opus voice (${packets[0].opus.length} bytes per packet)`); + + // Echo alice's own audio back as bob: the app decodes it and shows bob speaking + const echo = packets.map((p, i) => encodeVoice({ target: 0, frame: i * 2, opus: p.opus, last: i === packets.length - 1 }, bob.protobufVoice)); + const bobRow = page.locator('.sidebar .user', { hasText: 'bob' }); + const pump = setInterval(() => { const pk = echo.shift(); if (pk) bob.sendVoiceTunnel(pk); }, 20); + await bobRow.locator('.avatar.speaking').waitFor({ timeout: 3000 }); + console.log('ok: incoming voice decoded, speaker highlighted'); + await bobRow.locator('.avatar.speaking').waitFor({ state: 'detached', timeout: 3000 }); + clearInterval(pump); + console.log('ok: speaking indicator clears after the terminator'); + + // Push to talk: silent until the key is held, terminator on release + const ended = collect(1000, true); + await setMode('Push to talk'); + const tail = await within(ended, 'terminator after leaving always-on'); + assert.equal(tail.at(-1)!.last, true); + let leaked = 0; + const offLeak = bob.on('voice', raw => { if (decodeVoice(raw)?.session === aliceSession()) leaked++; }); + await page.waitForTimeout(400); + offLeak(); + assert.equal(leaked, 0, 'no voice while the push-to-talk key is up'); + await page.locator('.chat h1').click(); + const ptt = collect(1000, true); + await page.keyboard.down('Backquote'); + await page.waitForTimeout(300); + await page.keyboard.up('Backquote'); + const burst = await within(ptt, 'push-to-talk burst'); + assert.ok(burst.length >= 8 && burst.length <= 30, `about 300 ms of audio (${burst.length} packets)`); + assert.equal(burst.at(-1)!.last, true); + console.log(`ok: push to talk (${burst.length} packets, ends with terminator)`); + + // Voice activity with the fake beeping microphone: transmits in bursts + await setMode('Voice activity'); + const vad = await within(collect(10), 'voice activity packets', 8000); + assert.ok(vad.length === 10); + const biggest = Math.max(...vad.map(p => p.opus.length)); + assert.ok(biggest > 40, `packets during the tone carry real audio (largest ${biggest} bytes)`); + console.log(`ok: voice activity detection transmits on the tone (up to ${biggest} bytes per packet)`); + + // The app's own statistics confirm it decoded the echoed audio + await page.getByTitle('Settings').click(); + const statsText = await page.locator('.stats').textContent(); + const decoded = Number(/decoded (\d+)/.exec(statsText ?? '')?.[1]); + assert.ok(decoded >= 20, `decoded frames reported: ${statsText}`); + await page.getByRole('button', { name: 'Done', exact: true }).click(); + console.log(`ok: ${decoded} frames decoded`); + await setMode('Push to talk'); + await page.waitForTimeout(300); + + // Alice -> channel + const gotText = new Promise(res => bob.on('text', m => res(m.html))); + await page.getByLabel('Message').fill('hello alice\nsecond line https://example.com/x'); + await page.getByLabel('Message').press('Enter'); + const html = await within(gotText, 'channel text'); + assert.equal(html, 'hello <from> alice
    second line https://example.com/x'); + console.log('ok: bob received channel text'); + + // Bob -> channel, rendered sanitized + bob.sendText({ channels: [0] }, 'hi alice bold'); + await page.locator('.msg', { hasText: 'hi alice' }).waitFor(); + assert.equal(await page.evaluate(() => (window as any).pwned), undefined); + assert.equal(await page.locator('.msg b', { hasText: 'bold' }).count(), 1); + assert.equal(await page.locator('img[src^="https://evil"]').count(), 0); + console.log('ok: incoming text rendered and sanitized'); + + // YouTube links become a click-to-play card; nothing loads from YouTube before the click + bob.sendText({ channels: [0] }, 'watch https://youtu.be/dQw4w9WgXcQ?t=1m30s'); + const card = page.locator('.msg', { hasText: 'watch' }).locator('.yt'); + await card.getByText('from 1:30, click to play').waitFor(); + assert.equal(await card.locator('iframe').count(), 0); + assert.equal(await card.locator('.facade').evaluate(el => getComputedStyle(el).backgroundImage), 'none', 'no thumbnail request by default'); + await card.locator('.facade').click(); + assert.equal(await card.locator('iframe').getAttribute('src'), 'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?autoplay=1&rel=0&start=90'); + console.log('ok: YouTube link embedded as click-to-play'); + + // Bob -> DM + const alice = [...bob.users.values()].find(u => u.name === 'alice')!; + bob.sendText({ users: [alice.session] }, 'psst'); + await page.locator('.sidebar').getByText('Direct messages').waitFor(); + await page.locator('.dm', { hasText: 'bob' }).click(); + await page.locator('.msg', { hasText: 'psst' }).waitFor(); + console.log('ok: direct message shown in its own view'); + await page.locator('.channel .name').first().dblclick(); + + // Mute toggles propagate + const muted = new Promise(res => bob.on('user', (u, changed) => { if (u.name === 'alice' && changed.includes('selfMute')) res(u.selfMute); })); + await page.locator('.me').getByRole('button', { name: 'Mute', exact: true }).click(); + assert.equal(await within(muted, 'mute state'), true); + await page.locator('.me').getByRole('button', { name: 'Unmute', exact: true }).waitFor(); + + // Right-click on mute / deafen opens quick audio options + await page.locator('.me').getByRole('button', { name: 'Unmute', exact: true }).click({ button: 'right' }); + const micPop = page.getByRole('dialog', { name: 'Microphone' }); + await micPop.getByLabel('Microphone').waitFor(); + await micPop.getByRole('radio', { name: 'Always on' }).waitFor(); + await micPop.getByLabel(/Input volume/).fill('1.5'); + await micPop.getByText('Input volume: 150%').waitFor(); + await page.keyboard.press('Escape'); + await micPop.waitFor({ state: 'detached' }); + await page.locator('.me').getByRole('button', { name: 'Deafen', exact: true }).click({ button: 'right' }); + const outPop = page.getByRole('dialog', { name: 'Output' }); + await outPop.getByLabel('Output device').waitFor(); + await outPop.getByRole('button', { name: 'Voice settings...' }).click(); + await page.getByRole('tab', { name: 'Voice', selected: true }).waitFor(); + await page.getByText('Microphone volume: 150%').waitFor(); + await page.getByRole('button', { name: 'Done', exact: true }).click(); + console.log('ok: quick audio options on right-click'); + + // Tray icon mirrors the voice state; its menu acts on it + const trayState = () => app.evaluate(() => (globalThis as any).__mumh5Tray && { + tooltip: (globalThis as any).__mumh5Tray.tooltip, items: (globalThis as any).__mumh5Tray.items, icon: (globalThis as any).__mumh5Tray.state.icon + }); + const trayUntil = async (label: string, pred: (t: any) => boolean) => { + for (let i = 0; i < 50; i++) { const t = await trayState(); if (t && pred(t)) return t; await page.waitForTimeout(100); } + throw new Error(`Tray never reached: ${label} (${JSON.stringify(await trayState())})`); + }; + const tMuted = await trayUntil('muted', t => /muted on Test Server/.test(t.tooltip)); + assert.equal(tMuted.icon, 'data:image/png;base64,'); + assert.equal(tMuted.items.find((i: any) => i.label === 'Mute').checked, true); + await app.evaluate(() => (globalThis as any).__mumh5Tray.click('Mute')); + await within(new Promise(res => bob.on('user', (u, ch) => { if (u.name === 'alice' && ch.includes('selfMute') && !u.selfMute) res(); })), 'unmuted from the tray'); + await trayUntil('connected', t => /connected on Test Server/.test(t.tooltip) && !t.items.find((i: any) => i.label === 'Mute').checked); + await app.evaluate(() => (globalThis as any).__mumh5Tray.click('Mute')); + await trayUntil('muted again', t => /muted/.test(t.tooltip)); + console.log('ok: tray icon follows voice state and its menu mutes'); + await page.locator('.channel + .users, .users').getByTitle('Muted', { exact: true }).first().waitFor(); + console.log('ok: self mute propagated and shown'); + + if (process.env.UPLOAD_HOST) { + const png = path.join(userData, 'gradient.png'); + writeFileSync(png, testPng()); + const gotUpload = new Promise(res => bob.on('text', m => res(m.html))); + await page.locator('.chat .composer input[type=file]').setInputFiles(png); + const up = await within(gotUpload, 'upload message'); + assert.match(up, /gradient\.png<\/a>/); + assert.match(up, /$/); + assert.ok(up.length > 5000, `preview should exceed the text limit (${up.length})`); + await page.locator('.embed img').waitFor(); + await page.waitForFunction(() => (document.querySelector('.embed img') as HTMLImageElement)?.naturalWidth === 400); + console.log('ok: upload posted with vanilla thumbnail, embed loads for mumh5'); + + // Types the upload host does not allow are refused before uploading + const txt = path.join(userData, 'notes.txt'); + writeFileSync(txt, 'plain text'); + let leaked = false; + const off = bob.on('text', () => { leaked = true; }); + await page.locator('.chat .composer input[type=file]').setInputFiles(txt); + await page.getByText('notes.txt: file type not allowed on this upload host').waitFor(); + await page.waitForTimeout(500); + off(); + assert.equal(leaked, false); + console.log('ok: disallowed file type refused'); + } + + // Server certificate viewer from the header (the Docker server is self-signed, so no green header) + assert.equal(await page.locator('.sidebar header.trusted').count(), 0); + await page.locator('.sidebar header .title').click(); + await page.getByRole('dialog', { name: /Certificate of/ }).getByText('self-signed').waitFor(); + await page.getByRole('button', { name: 'Close', exact: true }).last().click(); + console.log('ok: server certificate viewer'); + + // Own description: rich text with a picture, as Mumble-compatible XHTML + await page.locator('.me .profile').click(); + await page.getByRole('button', { name: 'Edit description' }).click(); + const area = page.locator('.rich .area'); + await area.click(); + await page.keyboard.type('I test things '); + await page.keyboard.press('Control+b'); + await page.keyboard.type('bold'); + await page.keyboard.press('Control+b'); + await page.keyboard.press('Enter'); + await page.keyboard.type('second line'); + const pic = path.join(userData, 'avatar.png'); + writeFileSync(pic, testPng(300, 200)); + await page.locator('.rich input[type=file]').setInputFiles(pic); + await area.locator('img').waitFor(); + const aliceId = [...bob.users.values()].find(u => u.name === 'alice')!.session; + const commented = new Promise(res => bob.on('user', (u, changed) => { + if (u.session !== aliceId) return; + if (changed.includes('comment') && u.comment) res(u.comment); + // Long comments are announced by hash only; fetch the text + else if (changed.includes('commentHash')) bob.requestBlob({ comments: [aliceId] }); + })); + await page.getByRole('button', { name: 'Save', exact: true }).click(); + const comment = await within(commented, 'comment update'); + assert.match(comment, /^

    I test things bold<\/span><\/p>

    second line<\/p>$/); + assert.ok(comment.length > 5000, 'long enough that the server ran its XML check and accepted it'); + await page.locator('.panel .html img').waitFor(); + console.log('ok: own description edited'); + + // Other user's profile and context menu + await page.locator('.sidebar .user', { hasText: 'bob' }).click({ button: 'right' }); + for (const item of ['View profile', 'Send message', 'Server mute', 'Kick...', 'Ban...']) { + await page.getByRole('menuitem', { name: item }).waitFor(); + } + await page.getByRole('menuitem', { name: 'View profile' }).click(); + await page.locator('.panel h3', { hasText: 'bob' }).waitFor(); + console.log('ok: user context menu and profile'); + + // Information: own connection shows client details and the full certificate + await page.locator('.sidebar .user.self').click({ button: 'right' }); + await page.getByRole('menuitem', { name: 'Information' }).click(); + const info = page.getByRole('dialog', { name: /^Information: alice/ }); + await info.getByText('mumh5 0.1.0').waitFor(); + await info.getByText('linux', { exact: false }).first().waitFor(); + await info.getByText('Online for').waitFor(); + await info.getByRole('button', { name: 'View certificate' }).click(); + await page.getByRole('dialog', { name: /^Certificate of alice/ }).getByText('alice', { exact: true }).first().waitFor(); + await page.getByRole('dialog', { name: /^Certificate of alice/ }).getByRole('button', { name: 'Close', exact: true }).last().click(); + await info.getByRole('button', { name: 'Close', exact: true }).last().click(); + console.log('ok: own connection information with certificate chain'); + + // Someone else as a normal user: times and pings, details withheld by the server + await page.locator('.sidebar .user', { hasText: 'bob' }).click({ button: 'right' }); + await page.getByRole('menuitem', { name: 'Information' }).click(); + const bobInfo = page.getByRole('dialog', { name: 'Information: bob' }); + await bobInfo.getByText('Ping (TCP)').waitFor(); + await bobInfo.getByText('only shown to admins', { exact: false }).first().waitFor(); + assert.equal(await bobInfo.getByText('mumh5 0.1.0').count(), 0); + await bobInfo.getByRole('button', { name: 'Close', exact: true }).last().click(); + console.log('ok: other user information respects server permissions'); + + if (admin) { + // Remove channels left over from an earlier run (the server refuses duplicate names) + for (const c of [...admin.channels.values()]) { + if (c.id !== 0 && c.parent === 0) admin.send('ChannelRemove', { channel_id: c.id }); + } + await new Promise(r => setTimeout(r, 300)); + + // Channels: a long description (only sent as a hash), and a nested pair + const longDesc = 'Welcome to the lobby. ' + 'This description is long enough that the server only announces its hash. '.repeat(3); + const created = new Promise(res => { + let n = 0; + admin.on('channel', c => { if (['Lobby', 'Games', 'Minecraft'].includes(c.name) && ++n === 3) res(); }); + }); + admin.send('ChannelState', { parent: 0, name: 'Lobby', description: longDesc }); + admin.send('ChannelState', { parent: 0, name: 'Games' }); + await new Promise(r => setTimeout(r, 300)); + const games = [...admin.channels.values()].find(c => c.name === 'Games')!; + admin.send('ChannelState', { parent: games.id, name: 'Minecraft' }); + await within(created, 'channels created'); + + await page.locator('.channel', { hasText: 'Games' }).waitFor(); + assert.equal(await page.locator('.channel', { hasText: 'Minecraft' }).count(), 0, 'empty subchannels start collapsed'); + console.log('ok: channels collapsed by default'); + + // Single click previews the description (fetched on demand), without joining + await page.locator('.channel .name', { hasText: 'Lobby' }).click(); + await page.locator('.panel .html', { hasText: 'Welcome to the lobby' }).waitFor(); + assert.equal(await page.locator('.me .who span').textContent(), 'Root'); + console.log('ok: single click shows description without joining'); + + // Side chat sends to that channel while staying in Root + const lobby = [...bob.channels.values()].find(c => c.name === 'Lobby')!; + await page.locator('.panel [role=tab]', { hasText: 'Chat' }).click(); + // Channel messages only reach people in that channel, so listen from inside Lobby + admin.joinChannel(lobby.id); + await new Promise(r => setTimeout(r, 300)); + const sideMsg = new Promise(res => admin.on('text', m => { if (m.html === 'side hello') res(m.channels); })); + await page.locator('.panel textarea').fill('side hello'); + await page.locator('.panel textarea').press('Enter'); + assert.deepEqual(await within(sideMsg, 'side chat message'), [lobby.id]); + await page.locator('.panel .msg', { hasText: 'side hello' }).waitFor(); + assert.equal(await page.locator('.chat .msg', { hasText: 'side hello' }).count(), 0); + console.log('ok: side chat sends to the previewed channel'); + + // Double click joins; the joined channel opens up + await page.locator('.channel .name', { hasText: 'Games' }).dblclick(); + await page.locator('.me .who span', { hasText: 'Games' }).waitFor(); + await page.locator('.channel', { hasText: 'Minecraft' }).waitFor(); + console.log('ok: double click joins and expands'); + + // Channel context menu + await page.locator('.channel .name', { hasText: 'Lobby' }).click({ button: 'right' }); + await page.getByRole('menuitem', { name: 'Join channel' }).click(); + await page.locator('.me .who span', { hasText: 'Lobby' }).waitFor(); + console.log('ok: channel context menu joins'); + + // Side panel can be resized by dragging its left edge + const panelBox = (await page.locator('aside.panel').boundingBox())!; + const handle = (await page.locator('aside.panel .resize').boundingBox())!; + await page.mouse.move(handle.x + 4, handle.y + 200); + await page.mouse.down(); + await page.mouse.move(handle.x - 150, handle.y + 200, { steps: 5 }); + await page.mouse.up(); + const grown = (await page.locator('aside.panel').boundingBox())!; + assert.ok(Math.abs(grown.width - (panelBox.width + 154)) < 6, `panel resized ${panelBox.width} -> ${grown.width}`); + console.log('ok: side panel resizable'); + } + + // Without an upload host, images are sent inline within Mumble's image limit + await page.locator('.channel .name', { hasText: 'Root' }).dblclick(); + await page.locator('.me .who span', { hasText: 'Root' }).waitFor(); + await page.getByTitle('Settings').click(); + await page.getByRole('tab', { name: 'Chat and files' }).click(); + await page.getByLabel('Upload host').fill(''); + await page.getByRole('button', { name: 'Done', exact: true }).click(); + const big = path.join(userData, 'big.png'); + writeFileSync(big, testPng(1600, 1200)); + const gotInline = new Promise(res => bob.on('text', m => { if (m.html.startsWith('$/); + assert.ok(inline.length <= 131072, `inline image within the server limit (${inline.length})`); + console.log(`ok: inline image without upload host (${inline.length} chars)`); + + // ─── Several servers at once ───────────────────────────────────────────── + const userState = (name: string) => [...bob.users.values()].find(u => u.name === name); + const until2 = (label: string, cond: () => boolean) => within(new Promise(res => { + if (cond()) return res(); + const off = bob.on('user', () => { if (cond()) { off(); res(); } }); + }), label); + + await page.getByTitle('Add a server').click(); + await page.getByLabel('Address').fill(host); + await page.getByLabel('Port').fill(port); + await page.getByLabel('Username').fill('alice-b'); + await page.getByLabel('Label').fill('Second'); + await page.getByRole('button', { name: 'Save and connect' }).click(); + await page.locator('.rail .tile.live').nth(1).waitFor(); + assert.equal(await page.locator('.rail .tile.live').count(), 2, 'both servers connected'); + await until2('background server shows deafened', () => !!userState('alice-b')?.selfDeaf); + assert.equal(await page.locator('.rail .tile.voice').getAttribute('title').then(t => t?.startsWith('Test Server')), true); + await page.getByText('Voice is on Test Server').waitFor(); + console.log('ok: second server connected, deafened in the background'); + + // Joining a channel on the second server moves voice there + await page.locator('.channel .name', { hasText: 'Root' }).dblclick(); + await until2('voice moved to the second server', () => !userState('alice-b')?.selfDeaf && !!userState('alice')?.selfDeaf); + assert.equal(await page.locator('.rail .tile.voice').getAttribute('title').then(t => t?.startsWith('Second')), true); + console.log('ok: voice follows the channel you join'); + + // Messages on the server not on screen show as unread on its tile + bob.sendText({ channels: [0] }, 'ping both'); + await page.locator('.rail .tile[title^="Test Server"] .unread').waitFor(); + console.log('ok: unread badge on the background server'); + + // Reorder by dragging Second above Test Server + await page.locator('.rail .tile[title^="Second"]').dragTo(page.locator('.rail .tile[title^="Test Server"]'), { targetPosition: { x: 22, y: 4 } }); + assert.match(await page.locator('.rail .tile').first().getAttribute('title') ?? '', /^Second/); + console.log('ok: servers reordered by drag and drop'); + + // Custom icon from the tile's context menu: square-cropped and shown instead of initials + const iconFile = path.join(userData, 'icon.png'); + writeFileSync(iconFile, testPng(300, 200)); + await page.locator('.rail .tile[title^="Second"]').click({ button: 'right' }); + const chooser = page.waitForEvent('filechooser'); + await page.getByRole('menuitem', { name: 'Change icon...' }).click(); + await (await chooser).setFiles(iconFile); + const icon = page.locator('.rail .tile[title^="Second"] img.icon'); + await icon.waitFor(); + assert.deepEqual(await icon.evaluate((i: HTMLImageElement) => [i.naturalWidth, i.naturalHeight, i.src.slice(0, 15)]), [128, 128, 'data:image/webp']); + console.log('ok: custom server icon'); + + // Disconnecting the voice server hands voice back and restores the earlier state + await page.locator('.rail .tile[title^="Second"]').click({ button: 'right' }); + await page.getByRole('menuitem', { name: 'Disconnect' }).click(); + await until2('voice back on the first server', () => !userState('alice-b') && !userState('alice')?.selfDeaf); + assert.equal(userState('alice')?.selfMute, true, 'earlier self-mute kept'); + await page.locator('.rail .tile[title^="Test Server"]').click(); + await page.locator('.rail .tile[title^="Test Server"] .unread').waitFor({ state: 'detached' }); + console.log('ok: voice returns when the other server disconnects'); + + // Change name: Mumble needs a reconnect, the app does it and remembers the name. + // Unique per run: a reused test server may still have an earlier run's name registered. + const newName = `alice${Date.now() % 100000}`; + await page.locator('.me .profile').click(); + await page.getByRole('button', { name: 'Change name' }).click(); + await page.getByLabel('New name').fill(newName); + await page.getByRole('button', { name: 'Reconnect with this name' }).click(); + await page.locator('.me .who strong', { hasText: newName }).waitFor(); + await within(new Promise(res => { + if ([...bob.users.values()].some(u => u.name === newName)) return res(); + bob.on('user', u => { if (u.name === newName) res(); }); + }), 'bob sees the new name'); + console.log('ok: name changed by reconnecting'); + + // Self-registration: guest badge becomes registered + await page.locator('.me .profile').click(); + await page.locator('.panel .badge-reg', { hasText: 'Guest' }).waitFor(); + await page.locator('.panel .register').click(); + await page.locator('.panel .badge-reg.registered', { hasText: 'Registered' }).waitFor(); + await page.locator('.sidebar .user', { hasText: newName }).locator('.reg').waitFor(); + console.log('ok: registered on the server'); + + if (shots) { + await page.waitForTimeout(300); + for (const [name, w, h] of [['wide', 1280, 800], ['medium', 900, 700], ['narrow', 400, 780]] as const) { + await page.setViewportSize({ width: w, height: h }); + await page.waitForTimeout(200); + await page.screenshot({ path: path.join(shots, `${name}.png`) }); + } + await page.setViewportSize({ width: 1280, height: 900 }); + await page.locator('.me').getByRole('button', { name: /^(Mute|Unmute)$/ }).click({ button: 'right' }); + await page.waitForTimeout(300); + await page.screenshot({ path: path.join(shots, 'quick-audio.png') }); + await page.keyboard.press('Escape'); + await page.locator('.me .profile').click(); + await page.getByRole('button', { name: 'Edit description' }).click(); + await page.waitForTimeout(300); + await page.screenshot({ path: path.join(shots, 'editor.png') }); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.getByTitle('Settings').click(); + await page.waitForTimeout(600); + await page.screenshot({ path: path.join(shots, 'settings-voice.png') }); + await page.getByRole('button', { name: 'Done', exact: true }).click(); + await page.locator('.sidebar .user', { hasText: 'bob' }).click(); + await page.waitForTimeout(400); + await page.screenshot({ path: path.join(shots, 'profile.png') }); + await page.setViewportSize({ width: 400, height: 780 }); + await page.waitForTimeout(200); + await page.getByLabel('Open channels').click(); + await page.waitForTimeout(200); + await page.screenshot({ path: path.join(shots, 'narrow-drawer.png') }); + } + console.log('E2E PASSED'); +} catch (e) { + if (shots) await (await app.firstWindow()).screenshot({ path: path.join(shots, 'failure.png') }); + throw e; +} finally { + bob.disconnect(); + admin?.disconnect(); + await app.close(); +} diff --git a/test/e2e/screenshots.ts b/test/e2e/screenshots.ts new file mode 100644 index 0000000..35dc6f5 --- /dev/null +++ b/test/e2e/screenshots.ts @@ -0,0 +1,137 @@ +// Generates the README screenshots in docs/screenshots from a scripted scene. +// docker run -d --name mumh5-shots -p 64740:64738 -e MUMBLE_SUPERUSER_PASSWORD=shots \ +// -e MUMBLE_CONFIG_AUTOBANATTEMPTS=0 -e "MUMBLE_CONFIG_WELCOMETEXT=Welcome to the community voice server." \ +// mumblevoip/mumble-server +// npm run build && MUMBLE_TEST_HOST=localhost:64740 MUMBLE_SUPERUSER_PASSWORD=shots npm run screenshots +import { _electron as electron } from 'playwright-core'; +import electronPath from 'electron'; +import { mkdtempSync, mkdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { createCodec } from '../../src/core/proto.ts'; +import { MumbleClient } from '../../src/core/client.ts'; +import type { Transport } from '../../src/core/transport.ts'; +import { openTls } from '../../electron/tls-transport.ts'; +import { generateIdentity } from '../../electron/identity.ts'; + +const target = process.env.MUMBLE_TEST_HOST; +const superPw = process.env.MUMBLE_SUPERUSER_PASSWORD; +if (!target || !superPw) { + console.log('Set MUMBLE_TEST_HOST and MUMBLE_SUPERUSER_PASSWORD'); + process.exit(1); +} +const [host, port] = target.split(':'); +const root = path.resolve(import.meta.dirname, '../..'); +const out = path.join(root, 'docs', 'screenshots'); +mkdirSync(out, { recursive: true }); +const wait = (ms: number) => new Promise(r => setTimeout(r, ms)); + +function headless(username: string, password = ''): Promise { + const id = generateIdentity(username); + const client = new MumbleClient(createCodec()); + const t: Transport = { onData: null, onClose: null, send: b => conn.send(b), close: () => conn.close() }; + const conn = openTls(host, Number(port), id.certPem, id.keyPem, { + onSecure: () => {}, onData: c => t.onData?.(c), onClose: r => t.onClose?.(r) + }); + client.connect(t, { username, password, os: 'Linux' }); + return new Promise((res, rej) => { client.on('synced', () => res(client)); client.on('close', rej); }); +} + +const byName = (c: MumbleClient, name: string) => [...c.channels.values()].find(x => x.name === name)!; + +// ─── Server scene ───────────────────────────────────────────────────────────── +const admin = await headless('SuperUser', superPw); +// Start from an empty tree when the server is reused +for (const c of [...admin.channels.values()]) if (c.id !== 0 && c.parent === 0) admin.send('ChannelRemove', { channel_id: c.id }); +await wait(500); +admin.send('ChannelState', { parent: 0, name: 'Lobby', position: 0, description: '

    Welcome! Hang out, say hi, and drop your clips here.

    Be nice. Push to talk is appreciated in busy rooms.

    ' }); +admin.send('ChannelState', { parent: 0, name: 'Gaming', position: 1 }); +admin.send('ChannelState', { parent: 0, name: 'Music', position: 2, description: 'Listening parties every Friday.' }); +admin.send('ChannelState', { parent: 0, name: 'AFK', position: 3 }); +await wait(1500); +admin.send('ChannelState', { parent: byName(admin, 'Gaming').id, name: 'Minecraft' }); +admin.send('ChannelState', { parent: byName(admin, 'Gaming').id, name: 'Valorant' }); +await wait(1500); +const lobby = byName(admin, 'Lobby').id; +admin.disconnect(); + +const sarah = await headless('sarah'); +const max = await headless('max'); +const jonas = await headless('jonas'); +const lena = await headless('lena'); +sarah.joinChannel(lobby); +max.joinChannel(lobby); +await wait(300); +jonas.joinChannel(byName(jonas, 'Minecraft').id); +lena.joinChannel(byName(lena, 'Music').id); +await wait(1200); +lena.setSelfMute(true); +max.setSelfDeaf(true); +sarah.setComment('

    sarah streamer, night owl

    Mostly playing Minecraft and Valorant. Ask me about modpacks.

    • EU evenings
    • Push to talk
    '); + +// ─── App ────────────────────────────────────────────────────────────────────── +const { ELECTRON_RUN_AS_NODE, ...env } = process.env; +const app = await electron.launch({ + executablePath: electronPath as unknown as string, + args: [root, `--user-data-dir=${mkdtempSync(path.join(tmpdir(), 'mumh5-e2e-'))}`, '--ozone-platform=x11', + '--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'], + env: env as Record +}); +const page = await app.firstWindow(); +await page.setViewportSize({ width: 1440, height: 900 }); + +await page.getByRole('button', { name: /Create a new identity/ }).click(); +await page.getByLabel('Name').fill('kibi'); +await page.getByRole('button', { name: 'Create', exact: true }).click(); +await page.getByRole('button', { name: 'Skip for now' }).click(); +await page.getByRole('button', { name: 'Done' }).click(); + +await page.getByTitle('Settings').click(); +await page.getByRole('tab', { name: 'Chat and files' }).click(); +await page.getByLabel('YouTube links').selectOption('thumbnail'); +await page.getByRole('tab', { name: 'Voice' }).click(); +await page.getByRole('radio', { name: /Always on/ }).click(); +await page.getByRole('button', { name: 'Done', exact: true }).click(); + +await page.getByTitle('Add a server').click(); +await page.getByLabel('Address').fill(host); +await page.getByLabel('Port').fill(port); +await page.getByLabel('Username').fill('kibi'); +await page.getByLabel('Label').fill('Community'); +await page.getByRole('button', { name: 'Save and connect' }).click(); +await page.getByText(/Connected/).first().waitFor(); +await page.locator('.channel .name', { hasText: 'Lobby' }).dblclick(); +await page.locator('.me .who span', { hasText: 'Lobby' }).waitFor(); +await wait(800); + +sarah.sendText({ channels: [lobby] }, 'anyone up for a round later?'); +await wait(1200); +max.sendText({ channels: [lobby] }, 'yes, after dinner. 8pm?'); +await wait(1200); +await page.getByLabel('Message').fill('works for me. meanwhile, a classic: https://www.youtube.com/watch?v=dQw4w9WgXcQ'); +await page.getByLabel('Message').press('Enter'); +await wait(1200); +sarah.sendText({ channels: [lobby] }, 'no. not again (ok it is a good song)'); +await wait(3000); + +await page.screenshot({ path: path.join(out, 'main.png') }); + +await page.locator('.sidebar .user', { hasText: 'sarah' }).click(); +await page.locator('.panel .html', { hasText: 'streamer' }).waitFor(); +await wait(500); +await page.screenshot({ path: path.join(out, 'profile.png') }); + +await page.getByTitle('Settings').click(); +await wait(800); +await page.screenshot({ path: path.join(out, 'voice-settings.png') }); +await page.getByRole('button', { name: 'Done', exact: true }).click(); + +await page.setViewportSize({ width: 390, height: 844 }); +await page.locator('.scrim').first().click().catch(() => {}); +await wait(500); +await page.screenshot({ path: path.join(out, 'mobile.png') }); + +await app.close(); +for (const c of [sarah, max, jonas, lena]) c.disconnect(); +console.log(`Screenshots written to ${out}`); +process.exit(0); diff --git a/test/identity.test.ts b/test/identity.test.ts new file mode 100644 index 0000000..aa3e1f6 --- /dev/null +++ b/test/identity.test.ts @@ -0,0 +1,70 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, writeFileSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { generateIdentity, identityFromP12, identityToP12 } from '../electron/identity.ts'; +import { IdentityStore } from '../electron/identity-store.ts'; + +const tmp = () => mkdtempSync(path.join(tmpdir(), 'mumh5-id-')); + +test('PKCS#12 export and import round trip', () => { + const id = generateIdentity('roundtrip', 'rt@example.com'); + const p12 = identityToP12(id, 'hunter22', 'roundtrip'); + const back = identityFromP12(p12, 'hunter22'); + assert.equal(back.fingerprint, id.fingerprint); + assert.throws(() => identityFromP12(p12, 'wrong'), /Wrong password/); +}); + +test('store migrates the legacy identity and still asks for setup', async () => { + const dir = tmp(); + const legacy = generateIdentity('legacy user'); + writeFileSync(path.join(dir, 'identity.json'), JSON.stringify(legacy)); + const store = new IdentityStore(dir); + const { identities, setupDone } = await store.list(); + assert.equal(setupDone, false); + assert.equal(identities.length, 1); + assert.equal(identities[0].fingerprint, legacy.fingerprint); + assert.equal(identities[0].name, 'legacy user'); + assert.ok(identities[0].isDefault); + assert.equal(statSync(path.join(dir, 'identities.json')).mode & 0o077, 0, 'identities file is owner-only'); + await store.finishSetup(); + assert.equal((await new IdentityStore(dir).list()).setupDone, true); +}); + +test('store create, import, duplicates, default and remove', async () => { + const store = new IdentityStore(tmp()); + const a = await store.create('alice', ''); + const other = generateIdentity('bob'); + const b = await store.importP12(identityToP12(other, 'pw', 'bob'), 'pw', ''); + assert.equal(b.name, 'bob'); + await assert.rejects(store.importP12(identityToP12(other, 'x', 'bob'), 'x', ''), /already stored/); + assert.equal((await store.get()).id, a.id, 'first identity is the default'); + assert.equal((await store.get(b.id)).fingerprint, other.fingerprint); + await store.setDefault(b.id); + await store.remove(b.id); + const { identities } = await store.list(); + assert.deepEqual(identities.map(i => [i.name, i.isDefault]), [['alice', true]]); +}); + +test('imports the desktop Mumble client certificate', async () => { + const cfg = tmp(); + const mumble = generateIdentity('Mumble User'); + mkdirSync(path.join(cfg, 'Mumble', 'Mumble'), { recursive: true }); + const b64 = Buffer.from(identityToP12(mumble, '', 'Mumble User')).toString('base64'); + writeFileSync(path.join(cfg, 'Mumble', 'Mumble', 'mumble_settings.json'), JSON.stringify({ certificate: b64 })); + const prev = { xdg: process.env.XDG_CONFIG_HOME, appdata: process.env.APPDATA, home: process.env.HOME }; + process.env.XDG_CONFIG_HOME = cfg; + process.env.APPDATA = path.join(cfg, 'none'); + process.env.HOME = path.join(cfg, 'none'); + try { + const store = new IdentityStore(tmp()); + assert.deepEqual(await store.findMumbleCertificate().then(f => f?.name), 'Mumble User'); + const imported = await store.importFromMumble(''); + assert.equal(imported.fingerprint, mumble.fingerprint); + } finally { + for (const [k, v] of Object.entries({ XDG_CONFIG_HOME: prev.xdg, APPDATA: prev.appdata, HOME: prev.home })) { + if (v === undefined) delete process.env[k]; else process.env[k] = v; + } + } +}); diff --git a/test/proto.test.ts b/test/proto.test.ts new file mode 100644 index 0000000..e03b78d --- /dev/null +++ b/test/proto.test.ts @@ -0,0 +1,26 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { createCodec, FrameReader } from '../src/core/proto.ts'; + +const codec = createCodec(); + +test('encode/decode roundtrip with framing split across chunks', () => { + const a = codec.encode('TextMessage', { channel_id: [0], message: 'hello world' }); + const b = codec.encode('Ping', { timestamp: 1790796000000 }); + const stream = new Uint8Array([...a, ...b]); + const reader = new FrameReader(); + const out: any[] = []; + for (let i = 0; i < stream.length; i += 3) { + reader.push(stream.subarray(i, i + 3), (id, body) => out.push(codec.decode(id, body))); + } + assert.equal(out.length, 2); + assert.equal(out[0].name, 'TextMessage'); + assert.equal(out[0].msg.message, 'hello world'); + assert.deepEqual(out[0].msg.channel_id, [0]); + assert.equal(out[1].name, 'Ping'); + assert.equal(out[1].msg.timestamp, 1790796000000); +}); + +test('encode rejects invalid payloads', () => { + assert.throws(() => codec.encode('ChannelRemove', { channel_id: 'nope' })); +}); diff --git a/test/server.test.ts b/test/server.test.ts new file mode 100644 index 0000000..098af4b --- /dev/null +++ b/test/server.test.ts @@ -0,0 +1,141 @@ +// Integration test against a real Mumble server. Skipped unless MUMBLE_TEST_HOST is set, e.g. +// docker run -d -p 64739:64738 mumblevoip/mumble-server && MUMBLE_TEST_HOST=localhost:64739 npm test +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { createCodec } from '../src/core/proto.ts'; +import { MumbleClient } from '../src/core/client.ts'; +import type { Transport } from '../src/core/transport.ts'; +import { openTls } from '../electron/tls-transport.ts'; +import { generateIdentity } from '../electron/identity.ts'; +import { encodeVoice, decodeVoice } from '../src/core/voice-packet.ts'; + +const target = process.env.MUMBLE_TEST_HOST; +const codec = createCodec(); + +function connect(name: string): Promise { + const [host, port] = target!.split(':'); + const id = generateIdentity(name); + const client = new MumbleClient(codec); + const transport: Transport = { onData: null, onClose: null, send: b => conn.send(b), close: () => conn.close() }; + const conn = openTls(host, Number(port || 64738), id.certPem, id.keyPem, { + onSecure: () => {}, + onData: c => transport.onData?.(c), + onClose: r => transport.onClose?.(r) + }); + client.connect(transport, { username: name, os: 'test' }); + return new Promise((resolve, reject) => { + client.on('synced', () => resolve(client)); + client.on('close', reject); + }); +} + +const until = (fn: (resolve: (v: T) => void) => void, ms = 5000) => + new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error('timeout')), ms); + fn(v => { clearTimeout(t); resolve(v); }); + }); + +test('two clients sync, see each other, exchange text, and update state', { skip: !target }, async () => { + const alice = await connect('alice' + Date.now() % 1000); + const bob = await connect('bob' + Date.now() % 1000); + try { + assert.ok(alice.channels.has(0), 'root channel present'); + assert.ok(alice.session != null && bob.session != null); + + // Alice learns about Bob via UserState + await until(res => { + if (alice.users.has(bob.session!)) return res(); + alice.on('user', u => { if (u.session === bob.session) res(); }); + }); + + // Channel text message + const got = until(res => bob.on('text', m => res(m.html))); + alice.sendText({ channels: [0] }, 'hi from alice'); + assert.equal(await got, 'hi from alice'); + + // Self-mute propagates to other clients + const muted = until(res => bob.on('user', (u, changed) => { + if (u.session === alice.session && changed.includes('selfMute')) res(u.selfMute); + })); + alice.setSelfMute(true); + assert.equal(await muted, true); + + // Plugin data between clients (used for rich chat) + const pd = until(res => bob.on('pluginData', (s, id, data) => res(`${s}:${id}:${new TextDecoder().decode(data)}`))); + alice.sendPluginData([bob.session!], 'mumh5', new TextEncoder().encode('{"t":"typing"}')); + assert.equal(await pd, `${alice.session}:mumh5:{"t":"typing"}`); + + // Disconnect is seen as UserRemove + const gone = until(res => bob.on('userRemove', u => res(u.session))); + const aliceSession = alice.session; + alice.disconnect(); + assert.equal(await gone, aliceSession); + } finally { + alice.disconnect(); + bob.disconnect(); + } +}); + +test('image messages over the text limit must be well-formed XML', { skip: !target }, async () => { + const alice = await connect('imgA' + Date.now() % 1000); + const bob = await connect('imgB' + Date.now() % 1000); + try { + // Longer than the default 5000-char text limit, under the 128 KB image limit + const img = ``; + + const denied = until(res => alice.on('permissionDenied', d => res(d.type))); + alice.sendText({ channels: [0] }, `
    f.png
    ${img}`); + assert.equal(await denied, 4, 'unclosed
    is invalid XML, server denies as TextTooLong'); + + const got = until(res => bob.on('text', m => res(m.html))); + const ok = `f.png
    ${img}`; + alice.sendText({ channels: [0] }, ok); + assert.equal(await got, ok); + } finally { + alice.disconnect(); + bob.disconnect(); + } +}); + +test('bursts of messages are paced instead of dropped by the server', { skip: !target }, async () => { + const alice = await connect('burstA' + Date.now() % 1000); + const bob = await connect('burstB' + Date.now() % 1000); + try { + const got: string[] = []; + const all = until(res => bob.on('text', m => { got.push(m.html); if (got.length === 9) res(); }), 15000); + for (let i = 0; i < 9; i++) alice.sendText({ channels: [0] }, `msg ${i}`); + assert.ok(alice.queued > 0, 'some messages wait for the rate limit'); + await all; + assert.deepEqual(got, Array.from({ length: 9 }, (_, i) => `msg ${i}`)); + } finally { + alice.disconnect(); + bob.disconnect(); + } +}); + +test('voice packets are relayed through the TCP tunnel in the negotiated format', { skip: !target }, async () => { + const alice = await connect('voiceA' + Date.now() % 1000); + const bob = await connect('voiceB' + Date.now() % 1000); + try { + assert.equal(alice.protobufVoice, true, '1.5 server uses the protobuf voice format'); + const frames: { session: number; frame: number; last: boolean; bytes: number }[] = []; + const done = until(res => bob.on('voice', raw => { + const p = decodeVoice(raw); + if (!p) return; + frames.push({ session: p.session, frame: p.frame, last: p.last, bytes: p.opus.length }); + if (p.last) res(); + })); + // Opus TOC byte 0x78: SILK wideband 20 ms frame, followed by filler + const opus = new Uint8Array([0x78, ...new Array(40).fill(7)]); + for (let i = 0; i < 5; i++) alice.sendVoiceTunnel(encodeVoice({ target: 0, frame: i * 2, opus, last: i === 4 }, alice.protobufVoice)); + await done; + assert.equal(frames.length, 5); + assert.ok(frames.every(f => f.session === alice.session && f.bytes === opus.length)); + assert.deepEqual(frames.map(f => f.frame), [0, 2, 4, 6, 8]); + assert.equal(frames[4].last, true); + } finally { + alice.disconnect(); + bob.disconnect(); + } +}); + diff --git a/test/voice-packet.test.ts b/test/voice-packet.test.ts new file mode 100644 index 0000000..8243918 --- /dev/null +++ b/test/voice-packet.test.ts @@ -0,0 +1,38 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { writeVarint, readVarint, encodeVoice, decodeVoice, relayAsServer } from '../src/core/voice-packet.ts'; + +test('Mumble varints round trip at every size boundary', () => { + for (const v of [0, 1, 0x7f, 0x80, 0x3fff, 0x4000, 0x1fffff, 0x200000, 0xfffffff, 0x10000000, 0xffffffff, 0x100000000, 2 ** 40 + 5]) { + const out: number[] = []; + writeVarint(out, v); + assert.deepEqual(readVarint(new Uint8Array(out), 0), [v, out.length], `value ${v}`); + } + const small: number[] = []; + writeVarint(small, 300); + assert.deepEqual(small, [0x81, 0x2c], 'matches PacketDataStream encoding'); +}); + +for (const protobuf of [false, true]) { + test(`${protobuf ? 'protobuf' : 'legacy'} voice packets survive a server relay`, () => { + const opus = new Uint8Array([0xfc, 1, 2, 3, 250]); + for (const last of [false, true]) { + const sent = encodeVoice({ target: 0, frame: 1234, opus, last }, protobuf); + const got = decodeVoice(relayAsServer(sent, 42, protobuf)); + assert.deepEqual(got, { session: 42, target: 0, frame: 1234, opus, last }); + } + }); +} + +test('legacy header layout: opus type, terminator bit in the size field', () => { + const p = encodeVoice({ target: 3, frame: 5, opus: new Uint8Array(10), last: true }, false); + assert.equal(p[0], (4 << 5) | 3); + assert.equal(p[1], 5); + assert.deepEqual([p[2], p[3]], [0x80 | 0x20, 10], 'size 10 | 0x2000 as a 2-byte varint'); +}); + +test('non-audio packets are ignored', () => { + assert.equal(decodeVoice(new Uint8Array([1, 2, 3])), null, 'protobuf ping'); + assert.equal(decodeVoice(new Uint8Array([0x20, 5])), null, 'legacy ping'); + assert.equal(decodeVoice(new Uint8Array([0x80, 1])), null, 'truncated'); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7b82203 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["node", "svelte", "vite/client"] + }, + "include": ["src/**/*", "electron/**/*", "test/**/*"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..44208e2 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +export default defineConfig({ + base: './', + plugins: [svelte()], + build: { target: 'chrome120', outDir: 'dist', emptyOutDir: true } +});