-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.ts
More file actions
129 lines (109 loc) · 3.76 KB
/
app.ts
File metadata and controls
129 lines (109 loc) · 3.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import 'dotenv/config';
import { spawn } from 'node:child_process';
import player from 'play-sound';
import { handleMessage } from './messages';
import { nextBackoffMs } from './backoff';
type PlayClip = {
file: string;
volume: number;
};
process.on('unhandledRejection', reason => {
console.warn('Unhandled promise rejection:', reason);
});
// connect to socket for bot commands
// the basic idea is that we just proxy commands to the referenced HTTP API
function urlForRoom(roomName: string): string {
return `${process.env.SONOS_BRIDGE_URL}/${encodeURIComponent(roomName)}`;
}
function playClip(roomName: string, data: PlayClip): void {
const file = encodeURIComponent(data.file);
const volume = encodeURIComponent(data.volume);
if (process.env.USE_LOCAL_SOUNDS === 'true') {
player().play(`static/clips/${data.file}`);
} else {
fetch(`${urlForRoom(roomName)}/clip/${file}/${volume}`).catch(err =>
console.warn('Sonos bridge request failed:', err)
);
}
}
function localSay(text: string): void {
if (process.platform === 'darwin') {
spawn('say', [text]);
} else {
// NOTE: simple Windows support could be done with https://github.com/p-groarke/wsay or similar
console.warn('localSay is not supported on this platform.');
}
}
function sayClip(
roomName: string,
data: { text: string; volume: number }
): void {
const text = encodeURIComponent(data.text);
const volume = encodeURIComponent(data.volume);
if (process.env.USE_LOCAL_SOUNDS === 'true') {
localSay(text);
} else {
fetch(`${urlForRoom(roomName)}/say/${text}/${volume}`).catch(err =>
console.warn('Sonos bridge request failed:', err)
);
}
}
// Rooms to play on, from SONOS_ROOMS (comma-separated for multiple speakers).
const rooms = (process.env.SONOS_ROOMS ?? 'Back Office')
.split(',')
.map(room => room.trim())
.filter(Boolean);
function enumeratePlayers(callback: (roomName: string) => void): void {
rooms.forEach(callback);
}
function connect(): void {
const serviceUrl = process.env.CLEARBOT_URL;
const token = process.env.RELAY_TOKEN;
if (!serviceUrl) throw new Error('CLEARBOT_URL was not defined.');
if (!token) throw new Error('RELAY_TOKEN was not defined.');
let attempt = 0;
const open = () => {
console.log('Connecting to', serviceUrl);
// token rides as the WebSocket subprotocol (Sec-WebSocket-Protocol)
const ws = new WebSocket(serviceUrl, token);
ws.addEventListener('open', () => {
attempt = 0;
console.log(`Connected to server: ${serviceUrl}`);
});
ws.addEventListener('message', event => {
handleMessage(String(event.data), {
onPlayUrl: data => {
console.log('Received play_url: ', data);
// HACK: switch to new format for now...
enumeratePlayers(roomName =>
playClip(roomName, { file: data.url, volume: 20 })
);
},
onPlayText: data => {
console.log('Received say: ', data);
enumeratePlayers(roomName => sayClip(roomName, data));
},
onClose: () => console.log('Server asked us to close; will reconnect.'),
});
});
// A 1006 close right after connecting usually means a rejected token, not a down server.
ws.addEventListener('close', event => {
const delay = nextBackoffMs(attempt++);
console.log(
`Disconnected (code ${event.code}${event.reason ? `: ${event.reason}` : ''}); reconnecting in ${Math.round(delay / 1000)}s`
);
setTimeout(open, delay);
});
ws.addEventListener('error', () => {
// 'close' fires after 'error'; avoid double-scheduling
console.warn('WebSocket error; closing to trigger reconnect');
try {
ws.close();
} catch {
/* noop */
}
});
};
open();
}
connect();