UNPKG

@tf2pickup-org/mumble-client

Version:
184 lines 7.43 kB
import { tlsConnect } from './tls-connect.js'; import { MumbleSocket } from './mumble-socket.js'; import { concatMap, debounceTime, exhaustMap, filter, groupBy, interval, lastValueFrom, map, race, switchMap, take, tap, throwError, timeout, zip, } from 'rxjs'; import { Authenticate, PermissionDenied, PermissionQuery, Ping, Reject, ServerConfig, ServerSync, UserList, UserRemove, Version, } from '@tf2pickup-org/mumble-protocol'; import { ChannelManager } from './channel-manager.js'; import { UserManager } from './user-manager.js'; import { encodeMumbleVersion } from './encode-mumble-version.js'; import { ClientDisconnectedError, CommandTimedOutError, ConnectionRejectedError, PermissionDeniedError, } from './errors/index.js'; import { filterPacket } from './rxjs-operators/filter-packet.js'; import { platform, release } from 'os'; import { Permissions } from './permissions.js'; import { encodeMumbleVersionLegacy } from './encode-mumble-version-legacy.js'; import { TypedEventEmitter } from './typed-event-emitter.js'; import { CommandTimeout } from './config.js'; const defaultOptions = { port: 64738, clientName: 'simple mumble bot', pingInterval: 10000, }; export class Client extends TypedEventEmitter { channels = new ChannelManager(this); users = new UserManager(this); serverVersion; serverConfig; socket; session; welcomeText; options; permissions = new Map(); constructor(options) { super(); this.options = { ...defaultOptions, ...options }; } get user() { return this.self; } get self() { if (!this.session) { return undefined; } return this.users.bySession(this.session); } isConnected() { return !!this.socket; } assertConnected() { if (!this.socket) { throw new ClientDisconnectedError(); } } async connect() { this.socket = new MumbleSocket(await tlsConnect(this.options.host, this.options.port, this.options)); this.socket.audioPacket .pipe(groupBy(packet => packet.source)) .subscribe(group => { const user = this.users.bySession(group.key); if (!user) { console.warn(`unknown session: ${group.key}`); return; } let speaking = false; group.pipe(filter(() => !speaking)).subscribe(() => { speaking = true; this.emit('speakingStateChange', { user, speaking }); }); group.pipe(debounceTime(40)).subscribe(() => { speaking = false; this.emit('speakingStateChange', { user, speaking }); }); }); this.emit('socketConnect', this.socket); this.socket.packet .pipe(filterPacket(PermissionQuery), filter(permissionQuery => permissionQuery.channelId !== undefined), map(permissionQuery => ({ channelId: permissionQuery.channelId, permissions: new Permissions(permissionQuery.permissions ?? 0), }))) .subscribe(({ channelId, permissions }) => { this.permissions.set(channelId, permissions); }); this.socket.packet .pipe(filterPacket(UserRemove), filter(userRemove => userRemove.session === this.session)) .subscribe(userRemove => { this.emit('disconnect', { reason: userRemove.reason ?? 'unknown reason', }); delete this.socket; }); const initialize = lastValueFrom(race(zip(this.socket.packet.pipe(filterPacket(ServerSync), take(1)), this.socket.packet.pipe(filterPacket(ServerConfig), take(1)), this.socket.packet.pipe(filterPacket(Version), take(1))).pipe(tap(([serverSync, serverConfig, version]) => { if (serverSync.session) { this.session = serverSync.session; } this.welcomeText = serverSync.welcomeText ?? ''; this.serverVersion = version; this.serverConfig = serverConfig; this.emit('connect'); this.startPinger(); }), map(() => this)), this.socket.packet.pipe(filterPacket(Reject), switchMap(reject => throwError(() => new ConnectionRejectedError(reject)))))); await this.authenticate(); await this.sendVersion(); await this.ping(); return await initialize; } disconnect() { this.emit('disconnect'); this.socket?.end(); delete this.socket; return this; } async command(name, props) { this.assertConnected(); const ret = lastValueFrom(race(this.socket.packet.pipe(filterPacket(props.expectPacket[0]), filter(props.expectPacket[1]), take(1), timeout({ first: CommandTimeout, with: () => throwError(() => new CommandTimedOutError(name)), })), this.socket.packet.pipe(filterPacket(PermissionDenied), filter(pd => pd.session === this.session), take(1), concatMap(pd => throwError(() => new PermissionDeniedError(pd)))))); if ('sendPackets' in props) { for (const [type, payload] of props.sendPackets) { await this.socket.send(type, payload); } } else { await this.socket.send(props.sendPacket[0], props.sendPacket[1]); } return ret; } async fetchRegisteredUsers() { const ret = await this.command('fetchRegisteredUsers', { sendPacket: [UserList, UserList.create()], expectPacket: [UserList, () => true], }); return ret.users; } async deregisterUser(userId) { await this.command('deregisterUser', { sendPackets: [ [UserList, UserList.create({ users: [{ userId }] })], [UserList, UserList.create()], ], expectPacket: [UserList, () => true], }); } async renameRegisteredUser(userId, name) { await this.command('renameRegisteredUser', { sendPackets: [ [UserList, UserList.create({ users: [{ userId, name }] })], [UserList, UserList.create()], ], expectPacket: [UserList, () => true], }); } async sendVersion() { const version = { major: 1, minor: 4, patch: 287, }; return await this.socket?.send(Version, Version.create({ release: this.options.clientName ?? '', versionV1: encodeMumbleVersionLegacy(version), versionV2: encodeMumbleVersion(version), os: platform(), osVersion: release(), })); } async authenticate() { return await this.socket?.send(Authenticate, Authenticate.create({ username: this.options.username, ...(this.options.password ? { password: this.options.password } : {}), tokens: this.options.tokens ?? [], opus: true, })); } async ping() { return await this.socket?.send(Ping, Ping.create()); } startPinger() { const subscription = interval(this.options.pingInterval) .pipe(exhaustMap(() => this.ping())) .subscribe(); this.on('disconnect', () => { subscription.unsubscribe(); }); } } //# sourceMappingURL=client.js.map