diff --git a/package.json b/package.json index 1d80085..faa6bff 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "dev": "ts-node src/index.ts", "lint": "eslint \"src/**/*.ts\"", - "generate-proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=proto/ --ts_proto_opt=esModuleInterop=true --ts_proto_opt=exportCommonSymbols=false --ts_proto_opt=outputTypeRegistry=true Mumble.proto", + "generate-proto": "protoc --ts_out proto/ --ts_opt eslint_disable --proto_path . Mumble.proto", "prebuild": "yarn generate-proto", "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json", "test": "jest --watch", @@ -35,11 +35,12 @@ "bot" ], "dependencies": { + "@protobuf-ts/runtime": "2.5.0", "lodash": "^4.17.21", - "rxjs": "7.5.5", - "ts-proto": "^1.112.1" + "rxjs": "7.5.5" }, "devDependencies": { + "@protobuf-ts/plugin": "2.5.0", "@release-it/conventional-changelog": "5.0.0", "@tsconfig/node16": "1.0.2", "@types/jest": "27.5.1", diff --git a/src/channel-manager.ts b/src/channel-manager.ts index 77f3ed9..5990887 100644 --- a/src/channel-manager.ts +++ b/src/channel-manager.ts @@ -1,7 +1,8 @@ import { ChannelRemove, ChannelState, PermissionQuery } from '@proto/Mumble'; -import { filter, map, tap } from 'rxjs'; +import { tap } from 'rxjs'; import { Channel } from './channel'; import { Client } from './client'; +import { filterPacket } from './rxjs-operators/filter-packet'; import { MumbleSocket } from './mumble-socket'; export class ChannelManager { @@ -13,24 +14,21 @@ export class ChannelManager { socket.packet .pipe( - filter(packet => packet.$type === ChannelState.$type), - map(packet => packet as ChannelState), + filterPacket(ChannelState), tap(channelState => this.syncChannelState(channelState)), ) .subscribe(); socket.packet .pipe( - filter(packet => packet.$type === ChannelRemove.$type), - map(packet => packet as ChannelRemove), + filterPacket(ChannelRemove), tap(channelRemove => this.removeChannel(channelRemove)), ) .subscribe(); socket.packet .pipe( - filter(packet => packet.$type === PermissionQuery.$type), - map(packet => packet as PermissionQuery), + filterPacket(PermissionQuery), tap(permissionQuery => this.syncChannelPermissions(permissionQuery)), ) .subscribe(); @@ -89,9 +87,16 @@ export class ChannelManager { } private syncChannelState(channelState: ChannelState) { + if (channelState.channelId === undefined) { + return; + } + let channel = this.byId(channelState.channelId); if (!channel) { - channel = new Channel(this.client, channelState); + channel = new Channel( + this.client, + channelState as ChannelState & { channelId: number }, + ); this._channels.set(channel.id, channel); /** * Emitted whenever a channel is created. @@ -105,6 +110,9 @@ export class ChannelManager { } private syncChannelPermissions(permissionQuery: PermissionQuery) { + if (permissionQuery.channelId === undefined) { + return; + } this.byId(permissionQuery.channelId)?.sync(permissionQuery); } diff --git a/src/channel.spec.ts b/src/channel.spec.ts index 0978b04..022c2b6 100644 --- a/src/channel.spec.ts +++ b/src/channel.spec.ts @@ -7,9 +7,7 @@ jest.mock('./client'); jest.mock('./commands', () => ({ fetchChannelPermissions: jest .fn() - .mockResolvedValue( - PermissionQuery.fromPartial({ permissions: 0x1 | 0x40 }), - ), + .mockResolvedValue(PermissionQuery.create({ permissions: 0x1 | 0x40 })), })); describe('Channel', () => { @@ -27,11 +25,11 @@ describe('Channel', () => { it('should assign properties', () => { const channel = new Channel( client, - ChannelState.fromPartial({ + ChannelState.create({ channelId: 7, name: 'FAKE_CHANNEL_NAME', parent: 6, - }), + }) as ChannelState & { channelId: number }, ); expect(channel.id).toBe(7); expect(channel.name).toEqual('FAKE_CHANNEL_NAME'); @@ -41,16 +39,21 @@ describe('Channel', () => { let channel: Channel; beforeEach(() => { - channel = new Channel(client, ChannelState.fromPartial({})); + channel = new Channel( + client, + ChannelState.create({ channelId: 0 }) as ChannelState & { + channelId: number; + }, + ); }); it('should update name', () => { - channel.sync(ChannelState.fromPartial({ name: 'NEW_CHANNEL_NAME' })); + channel.sync(ChannelState.create({ name: 'NEW_CHANNEL_NAME' })); expect(channel.name).toEqual('NEW_CHANNEL_NAME'); }); it('should update parent', () => { - channel.sync(ChannelState.fromPartial({ parent: 10 })); + channel.sync(ChannelState.create({ parent: 10 })); expect(channel.parent).toEqual(10); }); }); @@ -59,7 +62,12 @@ describe('Channel', () => { let channel: Channel; beforeEach(() => { - channel = new Channel(client, ChannelState.fromPartial({ channelId: 7 })); + channel = new Channel( + client, + ChannelState.create({ channelId: 7 }) as ChannelState & { + channelId: number; + }, + ); }); it('should attempt to create channel', async () => { @@ -72,7 +80,12 @@ describe('Channel', () => { let channel: Channel; beforeEach(() => { - channel = new Channel(client, ChannelState.fromPartial({ channelId: 7 })); + channel = new Channel( + client, + ChannelState.create({ channelId: 7 }) as ChannelState & { + channelId: number; + }, + ); }); it('should attempt to remove the channel', async () => { diff --git a/src/channel.ts b/src/channel.ts index 586e01f..73a811d 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,6 +1,4 @@ import { ChannelState, PermissionQuery } from '@proto/Mumble'; -import { UnknownMessage } from '@proto/typeRegistry'; -import { isEmpty } from 'lodash'; import { Client } from './client'; import { fetchChannelPermissions } from './commands'; import { InsufficientPermissionsError } from './errors'; @@ -9,11 +7,14 @@ import { User } from './user'; export class Channel { readonly id: number; - name: string; - parent: number; + name?: string; + parent?: number; private permissions?: Permissions; - constructor(public readonly client: Client, channelState: ChannelState) { + constructor( + public readonly client: Client, + channelState: ChannelState & { channelId: number }, + ) { this.id = channelState.channelId; this.name = channelState.name; this.parent = channelState.parent; @@ -22,25 +23,18 @@ export class Channel { /** * @internal */ - sync(message: UnknownMessage) { - switch (message.$type) { - case ChannelState.$type: { - const channelState = message as ChannelState; - - if (!isEmpty(channelState.name)) { - this.name = channelState.name; - } - - if (channelState.parent) { - this.parent = channelState.parent; - } - break; + sync(message: unknown) { + if (ChannelState.is(message)) { + if (message.name !== undefined) { + this.name = message.name; } - case PermissionQuery.$type: { - const permissionQuery = message as PermissionQuery; - this.permissions = new Permissions(permissionQuery.permissions); - break; + if (message.parent !== undefined) { + this.parent = message.parent; + } + } else if (PermissionQuery.is(message)) { + if (message.permissions !== undefined) { + this.permissions = new Permissions(message.permissions); } } } @@ -81,7 +75,8 @@ export class Channel { } return new Permissions( - (await fetchChannelPermissions(this.client.socket, this.id)).permissions, + (await fetchChannelPermissions(this.client.socket, this.id)) + .permissions ?? 0, ); } } diff --git a/src/client.spec.ts b/src/client.spec.ts index a409074..f4ca142 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -5,7 +5,6 @@ import { ServerSync, Version, } from '@proto/Mumble'; -import { UnknownMessage } from '@proto/typeRegistry'; import { Subject } from 'rxjs'; import { Client } from './client'; import { MumbleSocket } from './mumble-socket'; @@ -40,21 +39,51 @@ describe(Client.name, () => { }); describe('when connected', () => { - let socket: jest.Mocked<MumbleSocket> & { packet: Subject<UnknownMessage> }; + let socket: jest.Mocked<MumbleSocket> & { packet: Subject<unknown> }; beforeEach(async () => { client.on('socketConnected', s => { socket = s; - socket.send.mockImplementation(message => { - switch (message.$type) { - case Authenticate.$type: - socket.packet.next(Version.fromPartial({})); - socket.packet.next(ServerSync.fromPartial({ session: 1234 })); - socket.packet.next(ServerConfig.fromPartial({})); + socket.send.mockImplementation(type => { + switch (type.typeName) { + case Authenticate.typeName: + socket.packet.next( + Version.create({ + version: 66790, + release: '1.4.230', + os: 'Linux', + osVersion: 'Ubuntu 20.04.4 LTS [x64]', + }), + ); + socket.packet.next( + ServerSync.create({ + session: 2, + maxBandwidth: 558000, + welcomeText: '', + permissions: BigInt(134744846), + }), + ); + socket.packet.next( + ServerConfig.create({ + allowHtml: true, + messageLength: 5000, + imageMessageLength: 131072, + maxUsers: 100, + }), + ); break; - case Ping.$type: - socket.packet.next(Ping.fromPartial({})); + + case Ping.typeName: + socket.packet.next( + Ping.create({ + timestamp: BigInt(0), + good: 0, + late: 0, + lost: 0, + resync: 0, + }), + ); break; } diff --git a/src/client.ts b/src/client.ts index bde1264..6be9fac 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,7 +16,6 @@ import { ChannelRemove, ChannelState, PermissionDenied, - permissionDenied_DenyTypeToJSON, Ping, Reject, ServerConfig, @@ -25,7 +24,7 @@ import { Version, } from '@proto/Mumble'; import { User } from './user'; -import { isEmpty, merge } from 'lodash'; +import { merge } from 'lodash'; import { ChannelManager } from './channel-manager'; import { Channel } from './channel'; import { UserManager } from './user-manager'; @@ -33,6 +32,7 @@ import EventEmitter from 'events'; import { encodeMumbleVersion } from './encode-mumble-version'; import { ClientOptions } from './client-options'; import { ConnectionRejectedError } from './errors'; +import { filterPacket } from './rxjs-operators/filter-packet'; const defaultOptions: Partial<ClientOptions> = { port: 64738, @@ -64,22 +64,19 @@ export class Client extends EventEmitter { race( zip( (this.socket as MumbleSocket).packet.pipe( - filter(packet => packet.$type === ServerSync.$type), - map(packet => packet as ServerSync), + filterPacket(ServerSync), take(1), ), (this.socket as MumbleSocket).packet.pipe( - filter(packet => packet.$type === ServerConfig.$type), - map(packet => packet as ServerConfig), + filterPacket(ServerConfig), take(1), ), (this.socket as MumbleSocket).packet.pipe( - filter(packet => packet.$type === Version.$type), - map(packet => packet as Version), + filterPacket(Version), take(1), ), (this.socket as MumbleSocket).packet.pipe( - filter(packet => packet.$type === Ping.$type), + filterPacket(Ping), take(1), ), ).pipe( @@ -90,7 +87,9 @@ export class Client extends EventEmitter { // FIXME Find a way to detect rejected connection without adding a delay delay(1000), tap(([serverSync, serverConfig, version]) => { - this.user = this.users.bySession(serverSync.session); + if (serverSync.session) { + this.user = this.users.bySession(serverSync.session); + } this.welcomeText = serverSync.welcomeText; this.serverVersion = version; this.serverConfig = serverConfig; @@ -99,13 +98,10 @@ export class Client extends EventEmitter { }), map(([serverSync]) => serverSync), ), - (this.socket as MumbleSocket).packet.pipe( - filter(packet => packet.$type === Reject.$type), - map(packet => packet as Reject), - ), + (this.socket as MumbleSocket).packet.pipe(filterPacket(Reject)), ).subscribe(message => { - if (message.$type === Reject.$type) { - reject(new ConnectionRejectedError(message as Reject)); + if (Reject.is(message)) { + reject(new ConnectionRejectedError(message)); } else { resolve(this); } @@ -134,34 +130,29 @@ export class Client extends EventEmitter { race( this.socket.packet.pipe( - filter(packet => packet.$type === ChannelState.$type), - map(packet => packet as ChannelState), + filterPacket(ChannelState), filter( channelState => channelState.parent === parent && channelState.name === name, ), take(1), ), - this.socket.packet.pipe( - filter(message => message.$type === PermissionDenied.$type), - map(message => message as PermissionDenied), - take(1), - ), + this.socket.packet.pipe(filterPacket(PermissionDenied), take(1)), ).subscribe(packet => { - if (packet.$type === PermissionDenied.$type) { - const reason = isEmpty(packet.reason) - ? permissionDenied_DenyTypeToJSON(packet.type) - : packet.reason; + if (PermissionDenied.is(packet)) { + const reason = packet.reason; reject(new Error(`failed to create channel (${reason})`)); } else { - const channel = this.channels.byId(packet.channelId); - if (channel) { - resolve(channel); + if (packet.channelId) { + const channel = this.channels.byId(packet.channelId); + if (channel) { + resolve(channel); + } } } }); - this.socket.send(ChannelState.fromPartial({ parent, name })); + this.socket.send(ChannelState, ChannelState.create({ parent, name })); }); } @@ -174,28 +165,21 @@ export class Client extends EventEmitter { race( this.socket.packet.pipe( - filter(packet => packet.$type === ChannelRemove.$type), - map(packet => packet as ChannelRemove), + filterPacket(ChannelRemove), filter(channelRemove => channelRemove.channelId === channelId), take(1), ), - this.socket.packet.pipe( - filter(message => message.$type === PermissionDenied.$type), - map(message => message as PermissionDenied), - take(1), - ), + this.socket.packet.pipe(filterPacket(PermissionDenied), take(1)), ).subscribe(packet => { - if (packet.$type === PermissionDenied.$type) { - const reason = isEmpty(packet.reason) - ? permissionDenied_DenyTypeToJSON(packet.type) - : packet.reason; + if (PermissionDenied.is(packet)) { + const reason = packet.reason; reject(new Error(`failed to remove channel (${reason})`)); } else { resolve(); } }); - this.socket.send(ChannelRemove.fromPartial({ channelId })); + this.socket.send(ChannelRemove, ChannelRemove.create({ channelId })); }); } @@ -222,24 +206,17 @@ export class Client extends EventEmitter { race( this.socket.packet.pipe( - filter(packet => packet.$type === UserState.$type), - map(packet => packet as UserState), + filterPacket(UserState), filter( userState => userState.session === userSession && userState.channelId === channelId, ), ), - this.socket.packet.pipe( - filter(message => message.$type === PermissionDenied.$type), - map(message => message as PermissionDenied), - take(1), - ), + this.socket.packet.pipe(filterPacket(PermissionDenied), take(1)), ).subscribe(packet => { - if (packet.$type === PermissionDenied.$type) { - const reason = isEmpty(packet.reason) - ? permissionDenied_DenyTypeToJSON(packet.type) - : packet.reason; + if (PermissionDenied.is(packet)) { + const reason = packet.reason; reject(new Error(`failed to remove channel (${reason})`)); } else { const user = this.users.bySession(userSession); @@ -250,7 +227,8 @@ export class Client extends EventEmitter { }); this.socket.send( - UserState.fromPartial({ session: userSession, channelId }), + UserState, + UserState.create({ session: userSession, channelId }), ); }); } @@ -262,7 +240,8 @@ export class Client extends EventEmitter { patch: 230, }); return await this.socket?.send( - Version.fromPartial({ + Version, + Version.create({ release: 'simple mumble bot', version, }), @@ -271,12 +250,13 @@ export class Client extends EventEmitter { private async authenticate(): Promise<void> { return await this.socket?.send( - Authenticate.fromPartial({ username: this.options.username }), + Authenticate, + Authenticate.create({ username: this.options.username }), ); } private async ping() { - return await this.socket?.send(Ping.fromPartial({})); + return await this.socket?.send(Ping, Ping.create()); } private startPinger() { diff --git a/src/commands/fetch-channel-permissions.ts b/src/commands/fetch-channel-permissions.ts index ce64245..8363798 100644 --- a/src/commands/fetch-channel-permissions.ts +++ b/src/commands/fetch-channel-permissions.ts @@ -1,6 +1,7 @@ import { MumbleSocket } from '@/mumble-socket'; +import { filterPacket } from '@/rxjs-operators/filter-packet'; import { PermissionQuery } from '@proto/Mumble'; -import { filter, map, take } from 'rxjs'; +import { filter, take } from 'rxjs'; export const fetchChannelPermissions = async ( socket: MumbleSocket, @@ -9,12 +10,11 @@ export const fetchChannelPermissions = async ( return new Promise(resolve => { socket.packet .pipe( - filter(packet => packet.$type === PermissionQuery.$type), - map(packet => packet as PermissionQuery), + filterPacket(PermissionQuery), filter(permissionQuery => permissionQuery.channelId === channelId), take(1), ) .subscribe(resolve); - socket.send(PermissionQuery.fromPartial({ channelId })); + socket.send(PermissionQuery, PermissionQuery.create({ channelId })); }); }; diff --git a/src/mumble-socket.ts b/src/mumble-socket.ts index 08a65ab..b742f11 100644 --- a/src/mumble-socket.ts +++ b/src/mumble-socket.ts @@ -1,7 +1,7 @@ import { Observable, Subject } from 'rxjs'; import { TLSSocket } from 'tls'; -import { messageTypeRegistry, UnknownMessage } from '@proto/typeRegistry'; -import { packetName, packetType } from './packet-type-registry'; +import { packetForType, packetType } from './packet-type-registry'; +import { MessageType } from '@protobuf-ts/runtime'; interface MumbleSocketReader { length: number; @@ -9,7 +9,7 @@ interface MumbleSocketReader { } export class MumbleSocket { - private _packet = new Subject<UnknownMessage>(); + private _packet = new Subject<unknown>(); private buffers: Buffer[] = []; private length = 0; private readers: MumbleSocketReader[] = []; @@ -19,7 +19,7 @@ export class MumbleSocket { this.readPrefix(); } - get packet(): Observable<UnknownMessage> { + get packet(): Observable<unknown> { return this._packet.asObservable(); } @@ -30,22 +30,20 @@ export class MumbleSocket { } } - async send(message: UnknownMessage): Promise<void> { - const messageType = messageTypeRegistry.get(message.$type); - if (!messageType) { - throw new Error(`unknown message type (${message.$type})`); - } - - const typeNumber = packetType(message.$type); + async send<T extends object>( + message: MessageType<T>, + payload: T, + ): Promise<void> { + const typeNumber = packetType(message); if (typeNumber === undefined) { - throw new Error(`unknown message type (${message.$type})`); + throw new Error(`unknown message type (${message.typeName})`); } - const payload = messageType.encode(message).finish(); + const encoded = message.toBinary(payload); const prefix = Buffer.alloc(6); prefix.writeUint16BE(typeNumber, 0); - prefix.writeUint32BE(payload.length, 2); - await this.write(Buffer.concat([prefix, payload])); + prefix.writeUint32BE(encoded.length, 2); + await this.write(Buffer.concat([prefix, encoded])); } write(buffer: Buffer | Uint8Array): Promise<void> { @@ -117,14 +115,9 @@ export class MumbleSocket { private readPacket(type: number, length: number) { this.read(length, data => { - const packetTypeName = packetName(type); - if (packetTypeName) { - const message = messageTypeRegistry.get(packetTypeName); - if (message) { - this._packet.next(message?.decode(data)); - } else { - console.error(`Unrecognized packet type (${packetTypeName})`); - } + const message = packetForType(type); + if (message) { + this._packet.next(message.fromBinary(data)); } else { console.error(`Unrecognized packet type (${type})`); } diff --git a/src/packet-type-registry.ts b/src/packet-type-registry.ts index f01773c..e3927c7 100644 --- a/src/packet-type-registry.ts +++ b/src/packet-type-registry.ts @@ -26,45 +26,50 @@ import { Version, VoiceTarget, } from '@proto/Mumble'; +import { MessageType } from '@protobuf-ts/runtime'; -const packetTypeForName = new Map<string, number>([ - [Version.$type, 0], - [UDPTunnel.$type, 1], - [Authenticate.$type, 2], - [Ping.$type, 3], - [Reject.$type, 4], - [ServerSync.$type, 5], - [ChannelRemove.$type, 6], - [ChannelState.$type, 7], - [UserRemove.$type, 8], - [UserState.$type, 9], - [BanList.$type, 10], - [TextMessage.$type, 11], - [PermissionDenied.$type, 12], - [ACL.$type, 13], - [QueryUsers.$type, 14], - [CryptSetup.$type, 15], - [ContextActionModify.$type, 16], - [ContextAction.$type, 17], - [UserList.$type, 18], - [VoiceTarget.$type, 19], - [PermissionQuery.$type, 20], - [CodecVersion.$type, 21], - [UserStats.$type, 22], - [RequestBlob.$type, 23], - [ServerConfig.$type, 24], - [SuggestConfig.$type, 25], +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyMessage = MessageType<any>; + +// https://buildmedia.readthedocs.org/media/pdf/mumble-protocol/latest/mumble-protocol.pdf +const packetForPacketType = new Map<number, AnyMessage>([ + [0, Version], + [1, UDPTunnel], + [2, Authenticate], + [3, Ping], + [4, Reject], + [5, ServerSync], + [6, ChannelRemove], + [7, ChannelState], + [8, UserRemove], + [9, UserState], + [10, BanList], + [11, TextMessage], + [12, PermissionDenied], + [13, ACL], + [14, QueryUsers], + [15, CryptSetup], + [16, ContextActionModify], + [17, ContextAction], + [18, UserList], + [19, VoiceTarget], + [20, PermissionQuery], + [21, CodecVersion], + [22, UserStats], + [23, RequestBlob], + [24, ServerConfig], + [25, SuggestConfig], ]); -const packetNameForType = new Map<number, string>(); -for (const [key, value] of packetTypeForName.entries()) { - packetNameForType.set(value, key); +const packetTypeForPacket = new Map<AnyMessage, number>(); +for (const [key, value] of packetForPacketType.entries()) { + packetTypeForPacket.set(value, key); } -export const packetName = (type: number) => { - return packetNameForType.get(type); +export const packetForType = (type: number) => { + return packetForPacketType.get(type); }; -export const packetType = (name: string) => { - return packetTypeForName.get(name); +export const packetType = (packet: AnyMessage) => { + return packetTypeForPacket.get(packet); }; diff --git a/src/rxjs-operators/filter-packet.ts b/src/rxjs-operators/filter-packet.ts new file mode 100644 index 0000000..3c50e1b --- /dev/null +++ b/src/rxjs-operators/filter-packet.ts @@ -0,0 +1,12 @@ +import { MessageType } from '@protobuf-ts/runtime'; +import { filter, map, Observable } from 'rxjs'; + +export function filterPacket<T extends object>( + messageType: MessageType<T>, +): (source: Observable<unknown>) => Observable<T> { + return source => + source.pipe( + filter(packet => messageType.is(packet)), + map(packet => packet as T), + ); +} diff --git a/src/user-manager.ts b/src/user-manager.ts index 9cf32ed..a6469cd 100644 --- a/src/user-manager.ts +++ b/src/user-manager.ts @@ -1,6 +1,7 @@ import { UserRemove, UserState } from '@proto/Mumble'; -import { filter, map, tap } from 'rxjs'; +import { tap } from 'rxjs'; import { Client } from './client'; +import { filterPacket } from './rxjs-operators/filter-packet'; import { MumbleSocket } from './mumble-socket'; import { User } from './user'; @@ -13,16 +14,14 @@ export class UserManager { socket.packet .pipe( - filter(packet => packet.$type === UserState.$type), - map(packet => packet as UserState), + filterPacket(UserState), tap(userState => this.syncUser(userState)), ) .subscribe(); socket.packet .pipe( - filter(packet => packet.$type === UserRemove.$type), - map(packet => packet as UserRemove), + filterPacket(UserRemove), tap(userRemove => this.removeUser(userRemove)), ) .subscribe(); @@ -38,9 +37,16 @@ export class UserManager { } private syncUser(userState: UserState) { + if (userState.session === undefined) { + return; + } + let user = this.bySession(userState.session); if (!user) { - user = new User(this.client, userState); + user = new User( + this.client, + userState as UserState & { session: number }, + ); this._users.set(user.session, user); this.client.emit('userCreate', user); } else { diff --git a/src/user.ts b/src/user.ts index 567e3a4..c537d70 100644 --- a/src/user.ts +++ b/src/user.ts @@ -1,23 +1,26 @@ -import { filter, map, takeWhile } from 'rxjs'; +import { filter, takeWhile } from 'rxjs'; import { UserState } from '@proto/Mumble'; import { Client } from './client'; -import { isEmpty } from 'lodash'; import { Channel } from './channel'; import { InsufficientPermissionsError, NoSuchChannelError } from './errors'; +import { filterPacket } from './rxjs-operators/filter-packet'; export class User { readonly session: number; - name: string; + name?: string; channelId: number; selfMute: boolean; selfDeaf: boolean; - constructor(private readonly client: Client, userState: UserState) { + constructor( + private readonly client: Client, + userState: UserState & { session: number }, + ) { this.session = userState.session; this.name = userState.name; - this.channelId = userState.channelId; - this.selfMute = userState.selfMute; - this.selfDeaf = userState.selfDeaf; + this.channelId = userState.channelId ?? 0; + this.selfMute = userState.selfMute ?? false; + this.selfDeaf = userState.selfDeaf ?? false; } get channel(): Channel { @@ -30,16 +33,23 @@ export class User { * @internal */ sync(userState: UserState) { - if (!isEmpty(userState.name)) { + if (userState.name !== undefined) { this.name = userState.name; } - if (this.channelId !== userState.channelId) { + if ( + userState.channelId !== undefined && + this.channelId !== userState.channelId + ) { this.channelId = userState.channelId; } - this.selfMute = userState.selfMute; - this.selfDeaf = userState.selfDeaf; + if (userState.selfMute !== undefined) { + this.selfMute = userState.selfMute; + } + if (userState.selfDeaf !== undefined) { + this.selfDeaf = userState.selfDeaf; + } } async moveToChannel(channelId: number): Promise<User> { @@ -59,15 +69,15 @@ export class User { return new Promise(resolve => { this.client.socket?.packet .pipe( - filter(message => message.$type === UserState.$type), - map(message => message as UserState), + filterPacket(UserState), filter(userState => userState.session === this.session), takeWhile(userState => userState.selfMute === selfMute, true), ) .subscribe(() => resolve(this)); this.client.socket?.send( - UserState.fromPartial({ session: this.session, selfMute }), + UserState, + UserState.create({ session: this.session, selfMute }), ); }); } @@ -76,15 +86,15 @@ export class User { return new Promise(resolve => { this.client.socket?.packet .pipe( - filter(message => message.$type === UserState.$type), - map(message => message as UserState), + filterPacket(UserState), filter(userState => userState.session === this.session), takeWhile(userState => userState.selfDeaf === selfDeaf, true), ) .subscribe(() => resolve()); this.client.socket?.send( - UserState.fromPartial({ session: this.session, selfDeaf }), + UserState, + UserState.create({ session: this.session, selfDeaf }), ); }); } diff --git a/yarn.lock b/yarn.lock index 82553b3..fa948db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -932,76 +932,54 @@ __metadata: languageName: node linkType: hard -"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/aspromise@npm:1.1.2" - checksum: 011fe7ef0826b0fd1a95935a033a3c0fd08483903e1aa8f8b4e0704e3233406abb9ee25350ec0c20bbecb2aad8da0dcea58b392bbd77d6690736f02c143865d2 - languageName: node - linkType: hard - -"@protobufjs/base64@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/base64@npm:1.1.2" - checksum: 67173ac34de1e242c55da52c2f5bdc65505d82453893f9b51dc74af9fe4c065cf4a657a4538e91b0d4a1a1e0a0642215e31894c31650ff6e3831471061e1ee9e - languageName: node - linkType: hard - -"@protobufjs/codegen@npm:^2.0.4": - version: 2.0.4 - resolution: "@protobufjs/codegen@npm:2.0.4" - checksum: 59240c850b1d3d0b56d8f8098dd04787dcaec5c5bd8de186fa548de86b86076e1c50e80144b90335e705a044edf5bc8b0998548474c2a10a98c7e004a1547e4b - languageName: node - linkType: hard - -"@protobufjs/eventemitter@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/eventemitter@npm:1.1.0" - checksum: 0369163a3d226851682f855f81413cbf166cd98f131edb94a0f67f79e75342d86e89df9d7a1df08ac28be2bc77e0a7f0200526bb6c2a407abbfee1f0262d5fd7 - languageName: node - linkType: hard - -"@protobufjs/fetch@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/fetch@npm:1.1.0" +"@protobuf-ts/plugin-framework@npm:^2.5.0": + version: 2.5.0 + resolution: "@protobuf-ts/plugin-framework@npm:2.5.0" dependencies: - "@protobufjs/aspromise": ^1.1.1 - "@protobufjs/inquire": ^1.1.0 - checksum: 3fce7e09eb3f1171dd55a192066450f65324fd5f7cc01a431df01bb00d0a895e6bfb5b0c5561ce157ee1d886349c90703d10a4e11a1a256418ff591b969b3477 - languageName: node - linkType: hard - -"@protobufjs/float@npm:^1.0.2": - version: 1.0.2 - resolution: "@protobufjs/float@npm:1.0.2" - checksum: 5781e1241270b8bd1591d324ca9e3a3128d2f768077a446187a049e36505e91bc4156ed5ac3159c3ce3d2ba3743dbc757b051b2d723eea9cd367bfd54ab29b2f + "@protobuf-ts/runtime": ^2.5.0 + typescript: ^3.9 + checksum: e98cddf849380d9fd334746d9417d02ae8102fbc54c619e6adbba5b23e42b3751484e105ff6877634cf774fef0e8239c2d88268df299e1dc67e3b95c2a16f48f languageName: node linkType: hard -"@protobufjs/inquire@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/inquire@npm:1.1.0" - checksum: ca06f02eaf65ca36fb7498fc3492b7fc087bfcc85c702bac5b86fad34b692bdce4990e0ef444c1e2aea8c034227bd1f0484be02810d5d7e931c55445555646f4 +"@protobuf-ts/plugin@npm:2.5.0": + version: 2.5.0 + resolution: "@protobuf-ts/plugin@npm:2.5.0" + dependencies: + "@protobuf-ts/plugin-framework": ^2.5.0 + "@protobuf-ts/protoc": ^2.5.0 + "@protobuf-ts/runtime": ^2.5.0 + "@protobuf-ts/runtime-rpc": ^2.5.0 + typescript: ^3.9 + bin: + protoc-gen-dump: bin/protoc-gen-dump + protoc-gen-ts: bin/protoc-gen-ts + checksum: 4d8d9aa3384b14401db6ef1803c97a4493cf388e6d3a28c4b742c9f4c7f0e6d9275b7b07a99a3782e68418de01f839af62f3838856f3060df249405800065d20 languageName: node linkType: hard -"@protobufjs/path@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/path@npm:1.1.2" - checksum: 856eeb532b16a7aac071cacde5c5620df800db4c80cee6dbc56380524736205aae21e5ae47739114bf669ab5e8ba0e767a282ad894f3b5e124197cb9224445ee +"@protobuf-ts/protoc@npm:^2.5.0": + version: 2.5.0 + resolution: "@protobuf-ts/protoc@npm:2.5.0" + bin: + protoc: protoc.js + checksum: d13c45c1e2b5773a7f9d643712162fcf5ac11b6d30bc7698d588a3b292c0761220baeb2e84f139b5b95482b77f829e60468419df9b94f72ec694ff2b9ccbaaeb languageName: node linkType: hard -"@protobufjs/pool@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/pool@npm:1.1.0" - checksum: d6a34fbbd24f729e2a10ee915b74e1d77d52214de626b921b2d77288bd8f2386808da2315080f2905761527cceffe7ec34c7647bd21a5ae41a25e8212ff79451 +"@protobuf-ts/runtime-rpc@npm:^2.5.0": + version: 2.5.0 + resolution: "@protobuf-ts/runtime-rpc@npm:2.5.0" + dependencies: + "@protobuf-ts/runtime": ^2.5.0 + checksum: ce80c7aa279ad39437e186f919e7df70a6db80137566ff2194300f73eeb5f51b6e2c00549023270e4af7d225dc455870632dfba46d12440e9d432bc9397a7115 languageName: node linkType: hard -"@protobufjs/utf8@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/utf8@npm:1.1.0" - checksum: f9bf3163d13aaa3b6f5e6fbf37a116e094ea021c0e1f2a7ccd0e12a29e2ce08dafba4e8b36e13f8ed7397e1591610ce880ed1289af4d66cf4ace8a36a9557278 +"@protobuf-ts/runtime@npm:2.5.0, @protobuf-ts/runtime@npm:^2.5.0": + version: 2.5.0 + resolution: "@protobuf-ts/runtime@npm:2.5.0" + checksum: 1a689171e8fa9d17bcde82233a64b496187561dc40dbef0aa9a5ba05594a61f2d2c70dae3eb0c936c7df89f8b09f27a71af7c2142dffe3d9998a3e3a3a5b143b languageName: node linkType: hard @@ -1079,6 +1057,8 @@ __metadata: version: 0.0.0-use.local resolution: "@tf2pickup-org/simple-mumble-bot@workspace:." dependencies: + "@protobuf-ts/plugin": 2.5.0 + "@protobuf-ts/runtime": 2.5.0 "@release-it/conventional-changelog": 5.0.0 "@tsconfig/node16": 1.0.2 "@types/jest": 27.5.1 @@ -1097,7 +1077,6 @@ __metadata: trace-unhandled: 2.0.1 ts-jest: 28.0.2 ts-node: 10.7.0 - ts-proto: ^1.112.1 tsc-alias: 1.6.7 tsconfig-paths: 4.0.0 typescript: 4.6.4 @@ -1280,13 +1259,6 @@ __metadata: languageName: node linkType: hard -"@types/long@npm:^4.0.1": - version: 4.0.2 - resolution: "@types/long@npm:4.0.2" - checksum: d16cde7240d834cf44ba1eaec49e78ae3180e724cd667052b194a372f350d024cba8dd3f37b0864931683dab09ca935d52f0c4c1687178af5ada9fc85b0635f4 - languageName: node - linkType: hard - "@types/minimist@npm:^1.2.0": version: 1.2.2 resolution: "@types/minimist@npm:1.2.2" @@ -1294,7 +1266,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:17.0.31, @types/node@npm:>=13.7.0": +"@types/node@npm:*, @types/node@npm:17.0.31": version: 17.0.31 resolution: "@types/node@npm:17.0.31" checksum: 704618350f8420d5c47db0f7778398e821b7724369946f5c441a7e6b9343295553936400eb8309f0b07d5e39c240988ab3456b983712ca86265dabc9aee4ad3d @@ -1308,13 +1280,6 @@ __metadata: languageName: node linkType: hard -"@types/object-hash@npm:^1.3.0": - version: 1.3.4 - resolution: "@types/object-hash@npm:1.3.4" - checksum: fe4aa041427f3c69cbcf63434af6e788329b40d7208b30aa845cfc1aa6bf9b0c11b23fa33a567d85ba7f2574a95c3b4a227f4b9b9b55da1eaea68ab94b4058d9 - languageName: node - linkType: hard - "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -2643,13 +2608,6 @@ __metadata: languageName: node linkType: hard -"dataloader@npm:^1.4.0": - version: 1.4.0 - resolution: "dataloader@npm:1.4.0" - checksum: e2c93d43afde68980efc0cd9ff48e9851116e27a9687f863e02b56d46f7e7868cc762cd6dcbaf4197e1ca850a03651510c165c2ae24b8e9843fd894002ad0e20 - languageName: node - linkType: hard - "dateformat@npm:^3.0.0": version: 3.0.3 resolution: "dateformat@npm:3.0.3" @@ -5450,13 +5408,6 @@ __metadata: languageName: node linkType: hard -"long@npm:^4.0.0": - version: 4.0.0 - resolution: "long@npm:4.0.0" - checksum: 16afbe8f749c7c849db1f4de4e2e6a31ac6e617cead3bdc4f9605cb703cd20e1e9fc1a7baba674ffcca57d660a6e5b53a9e236d7b25a295d3855cca79cc06744 - languageName: node - linkType: hard - "lowercase-keys@npm:^1.0.0, lowercase-keys@npm:^1.0.1": version: 1.0.1 resolution: "lowercase-keys@npm:1.0.1" @@ -5988,13 +5939,6 @@ __metadata: languageName: node linkType: hard -"object-hash@npm:^1.3.1": - version: 1.3.1 - resolution: "object-hash@npm:1.3.1" - checksum: fdcb957a2f15a9060e30655a9f683ba1fc25dfb8809a73d32e9634bec385a2f1d686c707ac1e5f69fb773bc12df03fb64c77ce3faeed83e35f4eb1946cb1989e - languageName: node - linkType: hard - "object-inspect@npm:^1.12.0, object-inspect@npm:^1.9.0": version: 1.12.0 resolution: "object-inspect@npm:1.12.0" @@ -6447,7 +6391,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:2.6.2, prettier@npm:^2.5.1": +"prettier@npm:2.6.2": version: 2.6.2 resolution: "prettier@npm:2.6.2" bin: @@ -6527,30 +6471,6 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^6.8.8": - version: 6.11.2 - resolution: "protobufjs@npm:6.11.2" - dependencies: - "@protobufjs/aspromise": ^1.1.2 - "@protobufjs/base64": ^1.1.2 - "@protobufjs/codegen": ^2.0.4 - "@protobufjs/eventemitter": ^1.1.0 - "@protobufjs/fetch": ^1.1.0 - "@protobufjs/float": ^1.0.2 - "@protobufjs/inquire": ^1.1.0 - "@protobufjs/path": ^1.1.2 - "@protobufjs/pool": ^1.1.0 - "@protobufjs/utf8": ^1.1.0 - "@types/long": ^4.0.1 - "@types/node": ">=13.7.0" - long: ^4.0.0 - bin: - pbjs: bin/pbjs - pbts: bin/pbts - checksum: 80e9d9610c3eb66f9eae4e44a1ae30381cedb721b7d5f635d781fe4c507e2c77bb7c879addcd1dda79733d3ae589d9e66fd18d42baf99b35df7382a0f9920795 - languageName: node - linkType: hard - "protocols@npm:^1.1.0, protocols@npm:^1.4.0": version: 1.4.8 resolution: "protocols@npm:1.4.8" @@ -7764,42 +7684,6 @@ __metadata: languageName: node linkType: hard -"ts-poet@npm:^4.11.0": - version: 4.11.0 - resolution: "ts-poet@npm:4.11.0" - dependencies: - lodash: ^4.17.15 - prettier: ^2.5.1 - checksum: 58141523080aafdce397338c8db435a6055385ed919d77c9c77bed76f2f8f2bbe924f26e69d2681ca27270519ea0ec7e041fbadc765a4a25528fcbf2548822f5 - languageName: node - linkType: hard - -"ts-proto-descriptors@npm:1.6.0": - version: 1.6.0 - resolution: "ts-proto-descriptors@npm:1.6.0" - dependencies: - long: ^4.0.0 - protobufjs: ^6.8.8 - checksum: 72de9826c2408efb979495c66add49200ea550c6540ebbf05c144cf5d66d21c09722a56cd183e25936196e04a24258e6af2dbdc2ef9e47e8dd19f094de681e89 - languageName: node - linkType: hard - -"ts-proto@npm:^1.112.1": - version: 1.112.1 - resolution: "ts-proto@npm:1.112.1" - dependencies: - "@types/object-hash": ^1.3.0 - dataloader: ^1.4.0 - object-hash: ^1.3.1 - protobufjs: ^6.8.8 - ts-poet: ^4.11.0 - ts-proto-descriptors: 1.6.0 - bin: - protoc-gen-ts_proto: protoc-gen-ts_proto - checksum: 59755e1ae3932dafe02667ea9724b8c644deddab9ef29e255a9dff8b60a500f15f25712a8ae0a15207fe529260ef9acb88e71f0892dffc1d9616ddd528d1f60a - languageName: node - linkType: hard - "tsc-alias@npm:1.6.7": version: 1.6.7 resolution: "tsc-alias@npm:1.6.7" @@ -7945,6 +7829,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^3.9": + version: 3.9.10 + resolution: "typescript@npm:3.9.10" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 46c842e2cd4797b88b66ef06c9c41dd21da48b95787072ccf39d5f2aa3124361bc4c966aa1c7f709fae0509614d76751455b5231b12dbb72eb97a31369e1ff92 + languageName: node + linkType: hard + "typescript@patch:typescript@4.6.4#~builtin<compat/typescript>": version: 4.6.4 resolution: "typescript@patch:typescript@npm%3A4.6.4#~builtin<compat/typescript>::version=4.6.4&hash=7ad353" @@ -7955,6 +7849,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@^3.9#~builtin<compat/typescript>": + version: 3.9.10 + resolution: "typescript@patch:typescript@npm%3A3.9.10#~builtin<compat/typescript>::version=3.9.10&hash=7ad353" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: dc7141ab555b23a8650a6787f98845fc11692063d02b75ff49433091b3af2fe3d773650dea18389d7c21f47d620fb3b110ea363dab4ab039417a6ccbbaf96fc2 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.15.5 resolution: "uglify-js@npm:3.15.5"