diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aee9c65..99c932a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,6 +68,7 @@ jobs: -p 64738:64738/tcp \ -p 64738:64738/udp \ -e MUMBLE_CONFIG_AUTOBAN_ATTEMPTS=0 \ + -e MUMBLE_SUPERUSER_PASSWORD=123456 \ --volume ${{ github.workspace }}/e2e/mumble-data:/data \ --user root \ mumblevoip/mumble-server:latest diff --git a/e2e/manages-as-super-user.e2e-spec.ts b/e2e/manages-as-super-user.e2e-spec.ts new file mode 100644 index 0000000..93c4a26 --- /dev/null +++ b/e2e/manages-as-super-user.e2e-spec.ts @@ -0,0 +1,59 @@ +import { Channel, Client } from '../src'; +import { waitABit } from './utils/wait-a-bit'; + +describe('Manages server as the super-user (e2e)', () => { + let client: Client; + + beforeAll(async () => { + client = new Client({ + host: 'localhost', + port: 64738, + username: 'superuser', + password: '123456', + rejectUnauthorized: false, + }); + await client.connect(); + await waitABit(1000); + }); + + afterAll(async () => { + await waitABit(1000); + client.disconnect(); + }); + + it('should create channels', async () => { + let channelCreatedEventEmitted = false; + client.once('channelCreate', (channel: Channel) => { + expect(channel.name).toEqual('sub10'); + channelCreatedEventEmitted = true; + }); + + expect(client.user?.channel).toBeTruthy(); + const channel = client.user!.channel; + expect(channel).toBeTruthy(); + const sub1 = await channel.createSubChannel('sub10'); + expect(sub1.parent).toEqual(channel.id); + expect(channelCreatedEventEmitted).toBe(true); + + const sub2 = await sub1.createSubChannel('sub11'); + expect(sub2.parent).toEqual(sub1.id); + }); + + it('should remove channels', async () => { + let channelRemoveEventEmitted = false; + + client.once('channelRemove', (channel: Channel) => { + expect(channel.name).toEqual('sub11'); + channelRemoveEventEmitted = true; + }); + + const sub2 = client.channels.byName('sub11'); + await sub2?.remove(); + expect(client.channels.byName('sub11')).toBe(undefined); + expect(channelRemoveEventEmitted).toBe(true); + + const sub1 = client.channels.byName('sub10'); + await sub1?.remove(); + expect(client.channels.byName('sub10')).toBe(undefined); + }); +}); diff --git a/e2e/mumble-data/murmur.sqlite b/e2e/mumble-data/murmur.sqlite index cf109ea..aae710c 100644 Binary files a/e2e/mumble-data/murmur.sqlite and b/e2e/mumble-data/murmur.sqlite differ diff --git a/src/channel.ts b/src/channel.ts index fdf6525..8de6e5b 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -142,6 +142,10 @@ export class Channel { * @returns Permissions. */ async getPermissions(): Promise { + if (this.client.user?.userId === 0) { + return Permissions.superUser(); + } + if (this.client.permissions.has(this.id)) { return this.client.permissions.get(this.id)!; } diff --git a/src/permissions.ts b/src/permissions.ts index fecd12e..4b1a78b 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -42,6 +42,13 @@ enum Permission { } export class Permissions { + static superUser(): Permissions { + return new Permissions( + // https://github.com/mumble-voip/mumble/blob/edd4588c8ae03d785d59102e2435151a682ec51d/src/ACL.cpp#L106 + Permission.All & ~(Permission.Speak | Permission.Whisper), + ); + } + constructor(public readonly permissions: number) {} get canJoinChannel(): boolean { diff --git a/src/user.ts b/src/user.ts index 90150de..79e720b 100644 --- a/src/user.ts +++ b/src/user.ts @@ -26,6 +26,7 @@ export class User { readonly session: number; name?: string; channelId = 0; + userId?: number; mute = false; deaf = false; suppress = false; @@ -55,6 +56,7 @@ export class User { const changes: UserChanges = { ...syncProperty(this, 'name', userState.name), ...syncProperty(this, 'channelId', userState.channelId), + ...syncProperty(this, 'userId', userState.userId), ...syncProperty(this, 'mute', userState.mute), ...syncProperty(this, 'deaf', userState.deaf), ...syncProperty(this, 'suppress', userState.suppress),