From 0db599e59561262056fb3273a0b0db98fc976e73 Mon Sep 17 00:00:00 2001 From: David Mohundro Date: Thu, 4 Jun 2026 15:26:02 -0500 Subject: [PATCH 1/3] feat: convert to bun websockets --- .env.example | 6 +- .gitignore | 1 + Makefile | 15 ++- bun.lockb | Bin 186777 -> 181095 bytes package.json | 1 - src/app.ts | 13 +-- src/relay-auth.ts | 19 ++++ src/relay-registry.ts | 52 +++++++++++ src/sonos.ts | 179 ++++++++++++++++++++++++++---------- test/relay-auth.test.ts | 26 ++++++ test/relay-registry.test.ts | 50 ++++++++++ test/relay-server.test.ts | 37 ++++++++ 12 files changed, 340 insertions(+), 59 deletions(-) create mode 100644 src/relay-auth.ts create mode 100644 src/relay-registry.ts create mode 100644 test/relay-auth.test.ts create mode 100644 test/relay-registry.test.ts create mode 100644 test/relay-server.test.ts diff --git a/.env.example b/.env.example index acd6c27..2020551 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ -SLACK_SIGNING_SECRET=FILL THIS OUT -SLACK_BOT_TOKEN=FILL THIS OUT +SLACK_BOT_TOKEN=xoxb-... +SLACK_APP_TOKEN=xapp-... +RELAY_TOKEN= +PORT=3000 diff --git a/.gitignore b/.gitignore index 73cfcbf..fe017cf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/**/* npm-debug.* *.orig.* build/ +*.bun-build *.swp .env .DS_Store diff --git a/Makefile b/Makefile index 9c0b090..6224848 100644 --- a/Makefile +++ b/Makefile @@ -46,10 +46,23 @@ logging: webapp open: logging open https://$(APPNAME).azurewebsites.net +# Run `websockets` and `appsettings` after `webapp` (post-deploy). +# Socket Mode means SLACK_SIGNING_SECRET is no longer used; the bot needs +# SLACK_APP_TOKEN and RELAY_TOKEN instead. WebSockets must be enabled on the +# App Service (off by default) for the native Bun.serve relay connections. +websockets: + az webapp config set -n $(APPNAME) -g $(RG) --web-sockets-enabled true + +appsettings: + az webapp config appsettings set -n $(APPNAME) -g $(RG) --settings \ + SLACK_BOT_TOKEN="$$SLACK_BOT_TOKEN" \ + SLACK_APP_TOKEN="$$SLACK_APP_TOKEN" \ + RELAY_TOKEN="$$RELAY_TOKEN" + logs: az webapp log tail -n $(APPNAME) -g $(RG) rollback: az group delete -n $(RG) -y -.PHONY: build logs rollback open webapp rg plan all +.PHONY: build logs rollback open webapp rg plan all websockets appsettings diff --git a/bun.lockb b/bun.lockb index 02c85c6444a459bb04921bb21160b19413625289..1e36e2faa1b735e711500dd2a84839409b79e5e5 100755 GIT binary patch delta 32188 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 delta 35396 zcmeIbcU%=m7dO7Ua+Ry1pdg^2UB z_p+);QpzxMQf_iq-Y3xQB&o=38J3orCrQ>7B&jm=p^&za!_31osYsIGI}6zp(un*) zW^=YznnikoR;ok414(=zgQo9-r25uFR_lnwB^XqN+y&VPGR@*OWLQ?R z;)i5r<(egFmCy&LWe)Pn&z4fHwfuf8N_H&lhqlOiF09DiThOVw!NZdB^2~#zq2_|& zS3y$7Fexc_NOn@L#he75I>~EkavCJ+OD}5Vs6~SYED6#3 z3KfyzQb-Ddo0BA=Y|&Kc&X7YP$uWI$Z3kHgdK2`?1#&rbR9U1)yL1#;QUyn2Lzs}8 z&TM>zCWW(MOWHA4&1pYWOnM_o@?ke5jqKngOI{8JAlIC4NgG@s>7CVm{W%CXS5u{PdKAZc1{ge0pcLQl?3N-<*wWjE3IuOP`e>!zB0hBEJ$)Nvbh%JIgm7B zK9HD`MI*#CZw#I69(rkZSSD)DSZ{3#4o|}3!jY1dH8jm!C`rjl7IRQQ+MuMA6mxc- zMOx{j^>m?-8EF}2uR+PuHB?9|W4^Bz&)*- zQqvKws^GJ|u+R(h&C-k(n#U)EjDi7GxEhjrvKW#o=0dL&sLiXskkmkur64n9Xj-0L zE@@<17Iis2NXzdHN%L|c9HNFlgQWUHP+uW6@V)SKzmNk$w6VGYSrhpskmSI)P)+(l zr*a)det4MH<1pwoz@LInj(s2+ENCfKH6$&F{*dIz>Ts!0>sh$ATBksg!AMB*I914A zf=?HNJhQdtNcxB*8cL~Ngw}uo5(~K~H)(`da@L@N{LJjMZ0wzGk=l@c21yM&KvIXI zqtFTwmb|3Ae2du<5XC$z)$QOPqt)jQX@_h(Nb<)3N#?twwem|LDG=%&m7hDzV$K`{ zccdra$?ixr`=Cc5Audmp^Ii$-ek z(;Z(82WgWy)k&-Ogpdo_5^G0)-Q5gxUTW4Li}dQk%FW8pGv`WD&At7 z7E4M-wj>Qt%QIW@Bxz29wsku}RzYKvAZa#s?yfazNgI-uIXFwo2Tz0J14&Vc>Y;U} z0d$H|33S@xvy<{tF&Cu4;AxWegrr&8L6e0=trN8h6cE&4xG<>PQ*-1c@`?WxI#s}h zOi4{jqhYZT{H^`}wE|A%fv{iP4r2G&_vabtC%c2q_o!p+G ze(FeW*05oNFk?PXktBDtUo@Q(keQHFK}E!idYXqzv1Ih%DP{+SbQgG<7nh+sKn@zL z4M7)_s|Vc+vOZ*G$Oe#CP?+??kT@w8t%9U>CPFrX90FNL4VsXk9yNueimZixBT4Jw z&ydvMe#jb-*;$sn6pKZ=F;pA63y?H1Gm|p2(%@W{b_pcm5-Kk*&74c)K#mW>DxtBz z14$kFBNOqbvB^lwOv^~Kf`B*GzSN4Nn=A+(q?hSOY*fE`Uf zKgep3w{x`oBur$o-vLSG2WRDanUhjdeX*3O{4kWowM@~Oe8iu|C_xx58?N;y^APh$x~e0G8zP@(e@#egsIsu1gYwisB?i_@r)cFjL$`-MAJSFof=eGH zsDTNPR1nKmjX(?}t?T>KG_Dq__oi!e;cr(c8~at;XH_^ZWwMuNU7K-^LkpJw z-gN2D_gvS$9bMd=4Nv&!-Jpm|$0}L1AG>*G_>qkV=U?|>1L}Hh3thOV_HTzBYj6M2 z*mi=PF!iUkb$fQGYsII zkC1P(_y&|e7oVxDq=Cu!DONPj?M2pV{$rNY&}0n2Ob7y3f!%eFfJbs8lW{pXs;2@w z=@ubhWV0HXWDj-``D->6pO4tdCMIK~jUB=dWbA_F6r#34_LJEjaNmKWIuzCSx-7oAN$$<&;{^)QpII+_2C^miEM!;ld64-95L1B9pV^)O zlOYD1Ts%A5C`SH}#Rr-&YIE`F$4UZC#sX{~;idj>|qP?PMyoWo3VJC=mcacmVnce187oL3aq42guH>-g`<6N zc9)*}T*4oq7 z%?`Fc%4AS*^BTr>c*V%!EGpV$90i?xuf&qDw!UU3!Pzy{%2iTh9LJJkOvXZRv=$8P zu4jb2gWU!9LfBcUb|Gwh8?c1t3pidmrtj??I!PqOc`nBaFvHo)xVFyIPo{ zG;{R`A&Mcv#&oc4h@~hR)!AmBk*WY!71h zI>s6eh>hmZEVv#4jkZm7nku7$S?|uV#>*=c2h&OaO-YYplPD)4rTL@`2d-8#?mvZvnB0=mCj+zGd|Xi!Hx1AuBn#% zBaCg0k2N-eS17EV!<2VhvJMHc#_vH=2{mAfb2vK<@+&M+im5i<51@5bHJq1>ZCYss z)E&t<4H|i4#r)zUjNb`O%d3qlw5qYU8wWzeWJc>4CuMGHR=Y>6@n=EG%+D)A@sD7i z^@uf$kC3Fk?5uYiYb0o9R-l10dPZtXK^-=webjDnd zr1OYzCQ|AYr&(b<08JZ1nrW3W!1Ijh)}py_QEtT0^1D2B(1r5rzZM!qu9U zM3FMaA*J;k%g`_bT99h7uhgO*rU>>1^pWB{5Lz%a_0(LQ!9{4 zQ&YAB@ar8?ZLE;!qf$qa(#(7YilWNQfoiSSK_rrqF*-@>lD56hfEEJRw2kaoiX_2M zXkLTLYcrrBL?X|@tVIjurune0F4lB;VeAf8n{>GBGQIE&C`gjSY!6dI>--4A89Co|Z16fCVe)gNPxe67FOPqQMF4*9J2#8~4Lkl06HLfihI(6s45TVKNP z^2IkpOHeIodAAy&g+N>EBcO?faE{&qO`GU+h4AoimOdl(IZ3sQfTneyEcZa8#je%) z7c?yxbkYee&`&z#=X_{oUdyBOJgQ|Sw05PPwi>O?0fo)NE?26cO2Zgv0d#|FSXY|5 zgj5h~8yaJ*Jw~5Av`4))hJ80J*0>6!HYv&bm(bd%EwvAgkYm`MnU3uJ`o4BE#tJJj z@VGql2{t|gixUNUhkF^tG>*OfZmjX$ae9Tct6m^Yy(9=J)VoA8X(BXrSjx{N<$NK# zHY3*PU!=znCmLl;5gRo#)>tA)?d;!dyf$~Nm>e2m%z&l^kJ{V=t(_VL(J!sm@qtKs(7Or-K@@yuQ)`h$zXxcQOlh+Dp9jRvHeWbLhLstxKCqkok z3>+zSRU3aps=rz*`P6C>Rt{O%%|@ytc;#xIj6Yn(|bAV3B8thnCD{)QvH2M5>MI$lbIE<0EK+Y8je3LDRIj8P#*;G-!R0 zS6#iOa-B{mnKeCPj3bev8gMh4hTEm-Y~Gw$<*(`N!kpH04p4&LW&Y{0O7^>KRC;Ti z8LX0Q&`*LDU;uqcg^iu(Xh$q&yLynUz}C!jY)@)WfO6Gt2uX3lNT@tkoO+NXyM97qI;jUq zD`qp_v5<08H1dCv)QVZymzPuz=A(L$Gy*t@s0T^P$0=;Vge)_;sUQ+1;Ko#I5K)(JA zSOdQS3bR?@7=?s%0U+y(LSBNTgCv!@MhXs+2H-Y8J-7=j~)&Ss8jW zkx!EHy@dWxlI(nh9Z5?13Y{b+{p4o&R1+HEFAPXh^jkra$1#vq+4F^t>I{yDM%8zN z#Q#!v`uH!BWlO#un2)(?dhSCL3 zlI(^-5|x1u>b6Dbc|_vKlhJ<~ka2?ezme2Hktj!!9GCz})I`CPq>fAxI!Q{>LuPbL zr5%i>(o8}8FVYS6Yhh1Cz7XZgOKNbV;9n=JB7c|2FQf#ecMIa5B+c<}gdIsr9u)fP zBt@2<+oMSRAnZs|@<)8Y(L(8z$RJ6PI0s48Z-Or`i8_xDvbzjPV|pEu_!~mr6!I34 zI7rga+!g#iA@4&fw1YoHf{yZ%s6Pcyk}7y2^zxE=Mo%!(L6Rb*5cyvu6{$dWwEonD zko6$zBZCYYh>A#3vXRisOB!nr@Z?}~kx!D!)1#JjyiQV<4?YShlmVhZpeR6+Lfca4 zBq`ZS=>H_iBtqDcBu65J{u)_Gzn4o1!hj^r&i+FGe<|rAElZ*JtBEp6q0EH?WT7_dj64UfF5fl zeV))4h=S!MMPjkYUk2$7{UjvKt)C#N{Le!E0!ar+@L2zXAW5E`g`|QPAgSWZLS7O1 zHz4u9bW2EjNR{+^_@IXFlOiNd;wR8)SyE-{MLHz4V+X0O|C-baj(?I=u|4caItW=y zNJk-^AgN*(NIFQ8y(=U+&=8V3<^hTSrRF4Q{`nw51zQLiEM!X|BZQ2BqzcKz>J)l zJN3VJWOQdnC%@O-iP20U{@*(?S~maQk^Or|_U|3pzjtK+cqc|%Cf%vghVkzm+5dw( zvV{Nt@5nZu{jz63;gW?Hx;9UnynjxcoHOs*x%_$c&e;_impncQ>Yeu9i>?F)5E7t zZXGab%H&G@b7r`WzBb#?z;%M%hVj>b&l~w?$+miLpZL@{r>#rOM;41mto>-4bDb+y zDq8%(fKeX3ZjENgt~wPyuwCz1<=5KPPp`sBf_mG9Q6`$Ot|uK|7c2G0IzYGT_YNBYWK_in!K8l=_8btlrl?RWHX zwVocWYG0pPB`s>@^vsD%L$=v1UiQ+99eJ#cGnlPgIl9`3$*;N|ga?Cf=?PM^=I8C_ui@z`OG z)&09HPv|yg-?{ytZd>gVk~=HgYA-M$K0VADTfV(qUc zeO;+>L4Ilz&rkpOY|wS9&Qa&jUH*J;hnQh&dLCpg(*t%b@qJmY+GXWC{o#<}3o|Fy zjmSN-df3Cd&dJ9Qd~s{WjotJ76ATq{@=YmQUSuBrezBkP*d{aoSnwb`%C6=YKOJlr z{LzsO`^HcH@K4iH*6OBHVXGpqNkL!DH+ML2{k!Vf$E)1k@TAM2@8zb19`st?1&Kqt-hpvm?w)ngGr#<%F-@2j4 z{6@dHM}AwUBD;ChiLJRAC$C^nZYHvow`|z{J8^jG-tKN9>v`Lz@Q(Y0o$FG54rzLN zN$VwES3{c530nN#7Y%Bq3_egO1>}EoWqH!#1IM@B{rN%cvFnx}zKM8yP*{x{@)w&f zC#ET@@Q-oI7SQVQp7+bzJ#=^VNz>6w+Ev{BL+!Q?_ii2A+GEq4o9y{*Cl+`$PF};tUrl72@7S=* z(7s@P*AiLkT^lz0S{(jOK?yXcdp4}?^*DJWn{_>r9fbA}+9notBaw}~Z^M?~h{L}< zxeKl70~^)@(b&S`ZzZx{pl!MpC*uz#+)iXu9@;ST?KnLBzaCo1pEk?^k=n(Q?j*7+ z(DLub$$Quy#BI(a8`kJv9R9u0p?is}-D4Z(bw5tt&kF7*vd7TQLi>h!JV3mk*svK7 z;_xqwPC@JW)P}Ws7$+ZLQy(TW>%VN+O=!nh=%45xv;}|0;ooXpg_iovhIM)rC!b_< zAEAHGZP;^Y-?R3Q(LZQw9>>W)vM10+zOZ5ap2Xqbpsad={=KweHc#W^Q>@oh^bgt& zXumS!Ux~A)$TlqFuQ>TE+wxZ;50Rm|K8uroV`K>P;RhL*ERYFOWgp=2wYGtpuWz0^%;8tAKE_2JxJT`@DSx5C@4^Qvt+7{)C8; zMiBk1Ks@5BtUxra48q0$;tB6%0PzbEJBav;8!LjCQUydtMG(*V79v8bf^e+_;w4Y3 z1mX%2$3Q5u%xhcAZ}B-cV8&X5sUY*i#I&mh#>)uCAoEd1Fpr5jOH3u1dsYUsvO1U< zmBAQgewvt`wqROS0aHch)2e{6wgYpM7#o>~RRyz|m<3hgPIZ2@D%?q}0iu%)2s=L4 z282^h5YLII$=g>0agc~L)j&A#Cq#_22hp!O2uHrEI*6tYAZ%!Qg?o@g?VpCn>rJ@TO$h+sax84N@CDH5UFuQ@~*KU5c@ zC3mj}<0A@xDrYz`uVhkAjyLc|(p5K;UIc@fRyYeU5FRV3PQxei2I z-V1_1ZV2NYbzp4bMi=nycoK>Bd<%&V+|Csuj;E36$oG)w#GT;~-`oU6$JRyBF8m~1 zle=;cH;8yXoaL|@*%Aw)mEghYS-gv0W7L9Fxzk>3<40GAPBP`h!K1}5lw?YIQWAo z;7R@?X3lLL+LFBgpF_!NkA|wPvqW}CI&;uj+J6EUCLwFWUI8bp3;5DWMoB0^$7;9vg5$%}YS1c)m{ z{7A$S?j8wZP8$%DB0((UCy8j+7DQkah~<2I6o|(}Tqa@#_lpLxG8V+_Xb`J-2@yR_ zAlk-&Sj}g}fUs@{;vo@hcvKq@n~7N72E-TqE)l8iLG)+~Vm)8d7KBp=5QbO~8+m*z zh=W9IB4QJl@h=bg$T$#Y6NqBIo`|L$K{&Jnv4tnK1Mv$H`-#}b?b>68Z|7+wcJMtU zc5>$q5W9E|iQW7Vi9OsM%T3~A+9lN2HZyoSxlbVBvYMZ}O^Jso2Q#KSI>MsAR$m@%jB+6M;s4=Yq z8~smmd>W5dsE%e-uQDltk4llRkzsSSCDo8F^B(oalSyi09GQdE+N+AcHc^yRzB}DD z7jF!Kiz9{!7aghk%K9H|xHgPeOOuByE3^i8zA;67IE@C!Bll>XM(H^a-bkr z3$6~h0fPHna4z5|>=fQLf^$WhB0>i}xNKH$|#X2^__OYk+hEq-h8@3$7v3^u!$v;TFL)Li!d!$5z2L zM%qG8$kEtr6J!&l-$I%~xm|EgksgXP9XkZ)fi(U2L!sO$I8UULk)}xQ5?nK+Q;?=( zx8Ry1Jp^fr?w&$HdO@M@fGCuE1*bk}BDj5!G=qHrs)$0lU)cE~O}%o1JRmqfr2UYl zsOb;0`6Erw3{r>afj0av`M1EwV5G>?LxK!I`a7h_0qu(^Bz_@tJlF9XuX{qS&qEH$ zzM(We^t8ee$Zvt8z+vDR@C|SPI0PI7s-t`jpc-JummHE?;L`cWLvoWs3LFK6Vn9t& zAE{5|?|Z0y1~3z#=OyS#pm6{_SyTj!2POct#n6`W4lo&@za}^xco&!nya(6h;MXiEwPXd4Ox`~lj2 zS^|MU3xKvB2cQ<<1ULh=X$sO-Lt9HX^wJ+##o=HMd= zXa&Rqw1;&7;sDyaIsu&l+Oq}%{Q%mQdIG(H#Q<$Zv^RbP(Dp^AGTO{(Q>IfEZ98

4rAt4*=RYKLhBLF%O`Pna*z$fp>t(fH%+_ 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 RIsRS#RHY-|wO3jBe*o{|IfMWJ diff --git a/package.json b/package.json index 0ba440b..c67530a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "license": "MIT", "dependencies": { "@slack/bolt": "^4.1.1", - "socket.io": "^4.8.0", "typescript": "^5.0.4", "typescript-eslint": "^8.17.0" }, diff --git a/src/app.ts b/src/app.ts index e496351..f5ec10a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,15 +6,16 @@ const sonos = new Sonos(); const app = new App({ token: process.env.SLACK_BOT_TOKEN, - signingSecret: process.env.SLACK_SIGNING_SECRET, + appToken: process.env.SLACK_APP_TOKEN, + socketMode: true, }); attachResponses(app, sonos); (async () => { - const port = Number(process.env.PORT) || 3000; - const server = await app.start(port); - sonos.initialize(server, app); - - console.log(`⚡️ Bolt app is running on ${port}!`); + await app.start(); + const relay = sonos.initialize(app); + console.log( + `⚡️ Bolt app running (Socket Mode); relay server on ${relay.port}!` + ); })(); diff --git a/src/relay-auth.ts b/src/relay-auth.ts new file mode 100644 index 0000000..347a433 --- /dev/null +++ b/src/relay-auth.ts @@ -0,0 +1,19 @@ +/** + * Validates a relay's offered WebSocket subprotocol(s) against the configured + * shared secret. The client offers the token as the WebSocket subprotocol + * (Sec-WebSocket-Protocol), which may arrive as a comma-separated list. + * + * Returns false unless a non-empty secret is configured AND one of the offered + * values matches it exactly. Never throws. + */ +export function isValidRelayToken( + offered: string | undefined | null, + secret: string | undefined | null +): boolean { + if (!secret) return false; + if (!offered) return false; + return offered + .split(',') + .map((p) => p.trim()) + .includes(secret); +} diff --git a/src/relay-registry.ts b/src/relay-registry.ts new file mode 100644 index 0000000..41bd8d5 --- /dev/null +++ b/src/relay-registry.ts @@ -0,0 +1,52 @@ +import { randomFromArray } from './utils'; + +/** Minimal shape we need from a relay socket (real: Bun ServerWebSocket). */ +export interface RelaySocket { + send(data: string): void; +} + +interface Entry { + lastPongAt: number; +} + +export class RelayRegistry { + private entries = new Map(); + + add(ws: T, now: number): void { + this.entries.set(ws, { lastPongAt: now }); + } + + remove(ws: T): void { + this.entries.delete(ws); + } + + markPong(ws: T, now: number): void { + const entry = this.entries.get(ws); + if (entry) entry.lastPongAt = now; + } + + count(): number { + return this.entries.size; + } + + all(): T[] { + return [...this.entries.keys()]; + } + + pickRandom(): T | undefined { + const all = this.all(); + return all.length ? randomFromArray(all) : undefined; + } + + /** + * Returns relays that have not ponged within two intervals (the grace + * window). Caller is responsible for terminating + removing them. + */ + sweep(now: number, intervalMs: number): T[] { + const grace = intervalMs * 2; + return this.all().filter((ws) => { + const entry = this.entries.get(ws); + return !!entry && now - entry.lastPongAt > grace; + }); + } +} diff --git a/src/sonos.ts b/src/sonos.ts index e1b4667..5fbeba2 100644 --- a/src/sonos.ts +++ b/src/sonos.ts @@ -1,46 +1,143 @@ import { App, SayFn } from '@slack/bolt'; -import { Server as HttpServer } from 'http'; -import { Server, Socket } from 'socket.io'; +import type { ServerWebSocket, Server } from 'bun'; import { getHelpText } from './responses'; -import { randomFromArray } from './utils'; +import { RelayRegistry } from './relay-registry'; +import { isValidRelayToken } from './relay-auth'; interface PlayUrl { + type: 'play_url'; url: string; } - interface PlayText { + type: 'play_text'; text: string; volume: number; } +interface CloseCmd { + type: 'close'; +} +export type RelayCommand = PlayUrl | PlayText | CloseCmd; -interface ServerToClientEvents { - play_url: (data: PlayUrl) => void; - play_text: (data: PlayText) => void; - close: () => void; +const HEARTBEAT_MS = 30_000; +const IDLE_TIMEOUT_S = 120; + +interface RelayData { + authedAt: number; } -interface ClientToServerEvents {} +export interface RelayServer { + port: number; + relayCount(): number; + /** test seam: push a command to a random relay */ + broadcastTest(cmd: RelayCommand): void; + stop(): void; +} + +export interface StartOptions { + port: number; + token: string | undefined; +} export default class Sonos { - sockets: Socket[] = []; + private registry = new RelayRegistry>(); + private server?: Server; + private heartbeat?: ReturnType; + private token: string | undefined; + + initialize(app: App): RelayServer { + this.token = process.env.RELAY_TOKEN; + const port = Number(process.env.PORT) || 3000; + const handle = this.startServer({ port, token: this.token }); + this.registerSlackHandlers(app); + return handle; + } - initialize(server: HttpServer, app: App): void { - const io = new Server(server); + startServer(opts: StartOptions): RelayServer { + this.token = opts.token; + const registry = this.registry; + const token = this.token; + + this.server = Bun.serve({ + port: opts.port, + fetch(req, server) { + // Non-WebSocket requests are Azure's health/warmup probes. + if (req.headers.get('upgrade')?.toLowerCase() !== 'websocket') { + return new Response('OK'); + } + const offered = req.headers.get('sec-websocket-protocol'); + if (!isValidRelayToken(offered, token)) { + return new Response('Unauthorized', { status: 401 }); + } + const matched = (offered ?? '').split(',')[0].trim(); + const ok = server.upgrade(req, { + data: { authedAt: Date.now() }, + // Echo the selected subprotocol back per the WS spec. + headers: { 'Sec-WebSocket-Protocol': matched }, + }); + return ok ? undefined : new Response('Upgrade failed', { status: 500 }); + }, + websocket: { + idleTimeout: IDLE_TIMEOUT_S, + sendPings: false, // we ping explicitly in the heartbeat + open: (ws) => { + registry.add(ws, Date.now()); + this.logClientStats('New relay connected to Sonos!'); + }, + message: (_ws, message) => { + console.log( + 'Ignoring unexpected relay message:', + String(message).slice(0, 200) + ); + }, + pong: (ws) => registry.markPong(ws, Date.now()), + close: (ws) => { + registry.remove(ws); + this.logClientStats('Relay disconnected from Sonos.'); + }, + }, + }); + + this.heartbeat = setInterval(() => this.runHeartbeat(), HEARTBEAT_MS); + + return { + port: this.server.port, + relayCount: () => registry.count(), + broadcastTest: (cmd) => this.sendToRandom(cmd), + stop: () => this.stop(), + }; + } - io.on( - 'connection', - (socket: Socket) => { - this.onConnection(socket); + private runHeartbeat(): void { + const now = Date.now(); + for (const ws of this.registry.sweep(now, HEARTBEAT_MS)) { + try { + ws.terminate(); + } catch { + /* already gone */ } - ); + this.registry.remove(ws); + } + for (const ws of this.registry.all()) { + try { + ws.ping(); + } catch { + /* will be swept next round */ + } + } + } - io.listen(server); + stop(): void { + if (this.heartbeat) clearInterval(this.heartbeat); + this.heartbeat = undefined; + this.server?.stop(true); + this.server = undefined; + } + private registerSlackHandlers(app: App): void { app.message(/sonos (.+)/, async ({ context, say }) => { - const match = context.matches[0]; - this.playOnSonos(match, say); + // matches[1] = the captured URL (matches[0] would include the "sonos " prefix) + this.playOnSonos(context.matches[1], say); }); - app.message(/health/, async ({ say }) => { const count = this.clientCount(); if (count > 0) { @@ -49,15 +146,12 @@ export default class Sonos { Sonos.alertNoClients(say); } }); - app.message(/help/, async ({ say }) => { say(getHelpText()); }); - app.message(/say (.+)/, async ({ context, say }) => { this.textToSpeech(context.matches[1], say); }); - app.message(/^Reminder: announcement (.+)/, async ({ context, say }) => { this.textToSpeech(context.matches[1], say); }); @@ -67,7 +161,7 @@ export default class Sonos { if (this.clientCount() < 1) { Sonos.alertNoClients(say); } else { - this.getSocket().emit('play_url', { url }); + this.sendToRandom({ type: 'play_url', url }); } } @@ -75,24 +169,21 @@ export default class Sonos { if (this.clientCount() < 1) { Sonos.alertNoClients(say); } else { - this.getSocket().emit('play_text', { text, volume: 60 }); + this.sendToRandom({ type: 'play_text', text, volume: 60 }); } } - private getSocket(): Socket { - console.log('Sonosing a message to a random client...'); - let socket = null; - try { - socket = randomFromArray(this.sockets); - } catch (error) { - console.error(error); - [socket] = this.sockets; + private sendToRandom(cmd: RelayCommand): void { + const ws = this.registry.pickRandom(); + if (!ws) { + console.error('No relay available to receive command', cmd.type); + return; } - return socket; + ws.send(JSON.stringify(cmd)); } private clientCount(): number { - return this.sockets.length; + return this.registry.count(); } private logClientStats(message: string): void { @@ -105,25 +196,15 @@ export default class Sonos { private static alertNoClients(say: SayFn) { const RELAY_CLIENT_DOWNLOAD_URL = 'https://github.com/clearfunction/sonos_proxy_nodejs'; - say( `Sorry, I don't have any Sonos relay clients connected right now. You can download one here... ${RELAY_CLIENT_DOWNLOAD_URL}` ); - setTimeout(() => { say('... Burn! :fire: http://i.imgur.com/4lhFLpO.gif'); }, 3 * 1000); } +} - private onConnection( - socket: Socket - ): void { - this.sockets.unshift(socket); - this.logClientStats('New clients connected to Sonos relay!'); - - socket.on('disconnect', () => { - this.sockets = this.sockets.filter((x) => x !== socket); - this.logClientStats('Client disconnected from Sonos relay.'); - }); - } +export function startRelayServer(opts: StartOptions): RelayServer { + return new Sonos().startServer(opts); } diff --git a/test/relay-auth.test.ts b/test/relay-auth.test.ts new file mode 100644 index 0000000..5d96b51 --- /dev/null +++ b/test/relay-auth.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from 'vitest'; +import { isValidRelayToken } from '../src/relay-auth'; + +const SECRET = 'sekret'; + +test('accepts an exact token match', () => { + expect(isValidRelayToken('sekret', SECRET)).toBe(true); +}); + +test('accepts a token offered among comma-separated subprotocols', () => { + expect(isValidRelayToken('sekret, foo', SECRET)).toBe(true); +}); + +test('rejects a wrong token', () => { + expect(isValidRelayToken('nope', SECRET)).toBe(false); +}); + +test('rejects a missing/undefined token', () => { + expect(isValidRelayToken(undefined, SECRET)).toBe(false); + expect(isValidRelayToken('', SECRET)).toBe(false); +}); + +test('rejects everything when no secret is configured', () => { + expect(isValidRelayToken('anything', '')).toBe(false); + expect(isValidRelayToken('anything', undefined)).toBe(false); +}); diff --git a/test/relay-registry.test.ts b/test/relay-registry.test.ts new file mode 100644 index 0000000..49b6dd7 --- /dev/null +++ b/test/relay-registry.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from 'vitest'; +import { RelayRegistry } from '../src/relay-registry'; + +interface FakeWs { + sent: string[]; + send: (s: string) => void; +} +const fakeWs = (): FakeWs => { + const sent: string[] = []; + return { sent, send: (s) => sent.push(s) }; +}; + +test('add increases count, remove decreases it', () => { + const r = new RelayRegistry(); + const a = fakeWs(); + r.add(a, 0); + expect(r.count()).toBe(1); + r.remove(a); + expect(r.count()).toBe(0); +}); + +test('pickRandom returns a connected relay or undefined when empty', () => { + const r = new RelayRegistry(); + expect(r.pickRandom()).toBeUndefined(); + const a = fakeWs(); + r.add(a, 0); + expect(r.pickRandom()).toBe(a); +}); + +test('sweep returns relays that missed two intervals and keeps fresh ones', () => { + const r = new RelayRegistry(); + const stale = fakeWs(); + const fresh = fakeWs(); + r.add(stale, 0); // last pong at t=0 + r.add(fresh, 0); + r.markPong(fresh, 50_000); // fresh ponged recently + + // interval 30s; stale at t=70s has missed > 2 intervals (60s) + const dropped = r.sweep(70_000, 30_000); + expect(dropped).toContain(stale); + expect(dropped).not.toContain(fresh); +}); + +test('sweep does not drop a relay within the grace window', () => { + const r = new RelayRegistry(); + const ws = fakeWs(); + r.add(ws, 0); + r.markPong(ws, 0); + expect(r.sweep(59_000, 30_000)).toHaveLength(0); // < 60s +}); diff --git a/test/relay-server.test.ts b/test/relay-server.test.ts new file mode 100644 index 0000000..97355a6 --- /dev/null +++ b/test/relay-server.test.ts @@ -0,0 +1,37 @@ +import { expect, test, afterEach } from 'vitest'; +import { startRelayServer, type RelayServer } from '../src/sonos'; + +const RUN = typeof Bun !== 'undefined'; +const TOKEN = 'test-token'; + +let server: RelayServer | undefined; +afterEach(() => server?.stop()); + +test.skipIf(!RUN)('valid token upgrades and receives a play_url command', async () => { + server = startRelayServer({ port: 0, token: TOKEN }); + const url = `ws://localhost:${server.port}`; + + const ws = new WebSocket(url, TOKEN); // token as subprotocol + const opened = new Promise((res) => (ws.onopen = () => res())); + await opened; + + const got = new Promise((res) => (ws.onmessage = (e) => res(String(e.data)))); + // server should now see exactly one relay + expect(server.relayCount()).toBe(1); + server.broadcastTest({ type: 'play_url', url: 'https://x/clip.mp3' }); + + const msg = JSON.parse(await got); + expect(msg).toEqual({ type: 'play_url', url: 'https://x/clip.mp3' }); + ws.close(); +}); + +test.skipIf(!RUN)('rejects an upgrade with a bad token', async () => { + server = startRelayServer({ port: 0, token: TOKEN }); + const ws = new WebSocket(`ws://localhost:${server.port}`, 'wrong-token'); + const closed = new Promise((res) => { + ws.onclose = (e) => res(e.code); + ws.onerror = () => res(-1); + }); + await closed; // never opens + expect(server.relayCount()).toBe(0); +}); From 87e644d3b369bea55c93ae7f73dc80bafe374094 Mon Sep 17 00:00:00 2001 From: David Mohundro Date: Fri, 5 Jun 2026 08:41:50 -0500 Subject: [PATCH 2/3] chore: bump deps --- bun.lockb | Bin 181095 -> 219173 bytes package.json | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bun.lockb b/bun.lockb index 1e36e2faa1b735e711500dd2a84839409b79e5e5..d9740487433e6d5603a5fcc2198f3852aba862df 100755 GIT binary patch delta 63272 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 delta 42277 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 diff --git a/package.json b/package.json index c67530a..79e964f 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "author": "David Mohundro ", "license": "MIT", "dependencies": { - "@slack/bolt": "^4.1.1", - "typescript": "^5.0.4", - "typescript-eslint": "^8.17.0" + "@slack/bolt": "^4.7.3", + "typescript": "^5.9.3", + "typescript-eslint": "^8.60.1" }, "scripts": { "lint": "eslint '**/*.ts'", @@ -19,14 +19,14 @@ "test": "vitest" }, "devDependencies": { - "@types/bun": "latest", - "@typescript-eslint/eslint-plugin": "^8.17.0", - "@typescript-eslint/parser": "^8.17.0", - "eslint": "^9.16.0", + "@types/bun": "^1.3.14", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^9.39.4", "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.26.0", - "prettier": "3.4.1", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-import": "^2.32.0", + "prettier": "^3.8.3", "vitest": "^4.1.8" } } From 86e90513d7d4a44f2c3f465b135a34ece8a440de Mon Sep 17 00:00:00 2001 From: David Mohundro Date: Fri, 5 Jun 2026 09:35:39 -0500 Subject: [PATCH 3/3] docs: update README for Socket Mode + native WebSockets --- README.md | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5e6ff16..287cae2 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,26 @@ This code is a Slack application (bot) written using [Slack Bolt](https://api.slack.com/bolt). It allows users to play mp3 clips on a Sonos -speaker. It communicates with [Sonos Proxy](https://github.com/clearfunction/sonos_proxy_nodejs) -which then communicates with [node-sonos-http-api](https://github.com/jishi/node-sonos-http-api). +speaker. It connects to Slack using **Socket Mode** (an outbound WebSocket — no +public endpoint required) and relays commands over a native WebSocket to [Sonos +Proxy](https://github.com/clearfunction/sonos_proxy_nodejs), which in turn +communicates with [node-sonos-http-api](https://github.com/jishi/node-sonos-http-api). We affectionately refer to this as "Burn Bot." ## Architecture +Both connections are established _outbound_ — ClearBot dials Slack, and each +Sonos Proxy dials ClearBot — so neither ClearBot nor the proxies need a public +inbound endpoint. Messages then flow back over those sockets: + ```mermaid sequenceDiagram - Slack-->>ClearBot: POST /slack/events { some message } - ClearBot-->>Sonos Proxy: websocket play_url { some message } - Sonos Proxy-->>node-sonos-http-api: GET http://localhost:5001/Office/clip/burn.mp3 + Note over Slack,ClearBot: ClearBot connects out to Slack (Socket Mode) + Slack-->>ClearBot: message event (e.g. "burn") + Note over ClearBot,Sonos Proxy: Sonos Proxy connects out to ClearBot (WebSocket, token auth) + ClearBot-->>Sonos Proxy: { type: play_url, url: burn.mp3 } + Sonos Proxy-->>node-sonos-http-api: GET http://localhost:5005/Office/clip/burn.mp3/20 ``` ## Requirements @@ -22,21 +30,33 @@ sequenceDiagram ## Running Locally -The easiest way to test is to set this up in a standalone Slack instance and -then use a local proxy like [ngrok](https://ngrok.com/). +Because ClearBot uses Socket Mode, it connects _out_ to Slack — there is no +public endpoint to expose. -- Create a Slack application (see Slack Bolt API link below) -- Run `bun install` to install dependencies -- Run `bun run dev` (it defaults to port 3000) -- Run ngrok to create a proxy to your Bolt app (`ngrok serve 3000`) -- Point your Slack's event subscription to your ngrok URL +- Create a Slack app and **enable Socket Mode** (Settings → Socket Mode). +- Generate an **app-level token** (Basic Information → App-Level Tokens) with the + `connections:write` scope — this is the `xapp-…` token. +- Under **Event Subscriptions**, subscribe to the bot message events (e.g. + `message.channels`). With Socket Mode on there is no Request URL to set. +- Copy `.env.example` to `.env` and fill in: + - `SLACK_BOT_TOKEN` — the bot token (`xoxb-…`) + - `SLACK_APP_TOKEN` — the app-level token (`xapp-…`) + - `RELAY_TOKEN` — a shared secret the Sonos Proxy must also use (generate with + `openssl rand -hex 32`) +- Run `bun install`, then `bun run dev` (defaults to port 3000). - Set up [Sonos Proxy](https://github.com/clearfunction/sonos_proxy_nodejs) + pointed at `ws://localhost:3000` with the same `RELAY_TOKEN`. - Enjoy! ## Deployment See the `Makefile`... make sure you are in the expected subscription by running `az account set --subscription YOUR_SUBSCRIPTION_ID`. +The relay uses WebSockets, which are **disabled by default** on Azure App +Service — run `make websockets` to enable them, and `make appsettings` to set +`SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, and `RELAY_TOKEN` on the web app. +(`SLACK_SIGNING_SECRET` is no longer used now that the bot runs in Socket Mode.) + ## Resources - [Slack Bolt API](https://slack.dev/bolt/)