From 526a8da8f509c7320b9b8bc3084fb4d53e88aaf1 Mon Sep 17 00:00:00 2001 From: dank074 Date: Sat, 21 Jun 2025 21:41:13 -0500 Subject: [PATCH] add webrtc support (#1284) Co-authored-by: MaddyUnderStars <46743919+MaddyUnderStars@users.noreply.github.com> --- assets/schemas.json | Bin 26419350 -> 26923306 bytes package-lock.json | Bin 397427 -> 240779 bytes package.json | 6 +- .../#guild_id/voice-states/#user_id/index.ts | 2 +- src/bundle/Server.ts | 15 +- src/gateway/Server.ts | 3 + src/gateway/events/Close.ts | 35 ++ src/gateway/opcodes/Identify.ts | 15 +- src/gateway/opcodes/StreamCreate.ts | 131 ++++++ src/gateway/opcodes/StreamDelete.ts | 76 ++++ src/gateway/opcodes/StreamWatch.ts | 98 ++++ src/gateway/opcodes/VoiceStateUpdate.ts | 66 ++- src/gateway/opcodes/index.ts | 6 + src/gateway/util/Constants.ts | 4 +- src/gateway/util/Utils.ts | 63 +++ src/gateway/util/WebSocket.ts | 2 - src/gateway/util/index.ts | 1 + src/util/entities/Stream.ts | 42 ++ src/util/entities/StreamSession.ts | 48 ++ src/util/entities/VoiceState.ts | 32 ++ src/util/entities/index.ts | 2 + src/util/index.ts | 2 +- src/util/interfaces/Event.ts | 38 +- .../migration/postgres/1745625724865-voice.ts | 43 ++ src/util/schemas/SelectProtocolSchema.ts | 2 +- src/util/schemas/StreamCreateSchema.ts | 13 + src/util/schemas/StreamDeleteSchema.ts | 7 + src/util/schemas/StreamWatchSchema.ts | 7 + src/util/schemas/VoiceIdentifySchema.ts | 3 +- src/util/schemas/VoiceVideoSchema.ts | 2 +- src/util/schemas/index.ts | 3 + src/util/util/Constants.ts | 33 -- src/util/util/Event.ts | 6 +- src/webrtc/Server.ts | 21 +- src/webrtc/events/Close.ts | 2 - src/webrtc/events/Connection.ts | 6 +- src/webrtc/events/Message.ts | 11 +- src/webrtc/opcodes/BackendVersion.ts | 8 +- src/webrtc/opcodes/Heartbeat.ts | 12 +- src/webrtc/opcodes/Identify.ts | 130 ++++-- src/webrtc/opcodes/SelectProtocol.ts | 54 +-- src/webrtc/opcodes/Speaking.ts | 44 +- src/webrtc/opcodes/Video.ts | 336 +++++++++----- src/webrtc/opcodes/index.ts | 7 +- src/webrtc/opcodes/sdp.json | 420 ------------------ src/webrtc/util/Constants.ts | 10 +- src/webrtc/util/MediaServer.ts | 102 ++--- src/webrtc/util/Send.ts | 27 ++ src/webrtc/util/WebRtcWebSocket.ts | 7 + src/webrtc/util/index.ts | 2 + tsconfig.json | 4 +- 51 files changed, 1227 insertions(+), 782 deletions(-) create mode 100644 src/gateway/opcodes/StreamCreate.ts create mode 100644 src/gateway/opcodes/StreamDelete.ts create mode 100644 src/gateway/opcodes/StreamWatch.ts create mode 100644 src/gateway/util/Utils.ts create mode 100644 src/util/entities/Stream.ts create mode 100644 src/util/entities/StreamSession.ts create mode 100644 src/util/migration/postgres/1745625724865-voice.ts create mode 100644 src/util/schemas/StreamCreateSchema.ts create mode 100644 src/util/schemas/StreamDeleteSchema.ts create mode 100644 src/util/schemas/StreamWatchSchema.ts delete mode 100644 src/webrtc/opcodes/sdp.json create mode 100644 src/webrtc/util/Send.ts create mode 100644 src/webrtc/util/WebRtcWebSocket.ts diff --git a/assets/schemas.json b/assets/schemas.json index e95b0ba737ed348285d8c08dbbdf95a950c77c94..a30c3f4691f0c2ba68f6e606d1a8e6ce6a38846f 100755 GIT binary patch delta 1873 zcmd7Q_gfcK6vy%Bfbazc1Vm8;RNNcIy+Uy>aNsnKtJ{L!+kx^y+54uIp?`~h39+@?YrfZiQtAimGHn5 z2VQtnnJW0;iyu|-CxAez5kz%r5KK*K5khT32_u|3)TJH~)TaRrX+&e1(3EC0rv)u( zMQb8yLtCO~M|(QZkxq1`3tj0(cY5#;(e$JjG4!Sned$Mk1~8C83}y&Ji6xF<3}*x* z8O3Pg8N*n{F`fxbWD*HXCXp#5F_mPdF`XIAWEQiTLke@5$9xu$N*d{8u#iP8W(iAK z#&TA$l2v4~nl-Ei*0G)}Hn5RRY{syKtvJbM8#&~%ogL(1*~u<;vxkRyghz3)mwfi| z82dTE<2=EW9OMv(d5Wibh9ey17|-$?&vTp?IKhj&#LK+GNltN^0?ts#tGq@Luk!|P z@)mFN4re*XdEVtcin%}u@ACm4@(~~N38j3>XI$hGm$^b2SNWW4e8F|TQsVc@_On{g`G1bHbiK#B8hL~V6HO15t6C$Rzm{2icV#3AL5mQ%8Juwkt z>WgV0rlFWdVj7ESBBrUBW@4I)X(6Vim{ww1i-{D|Moe2VQDWMOX)mUOn2usPiRmn+ zi=6B#y)L_oOM@jY?_Bq`uK8~DB*JE|^6{{dd*bUjQ?GQD3c5)_>Xq&?-03+YJvBKo z#a?jh8fRBs2zTUpcm-PLDMz?_L{g&bYNNFhV#|+K_q6NFyaIB(LUSYA1=+nj-2Cn2 zUdQU%;+>wMu56dDjI(=hT&(Q!NSY#@UFD&DCEgZaz36Gr#2l-pg*|MUx0`p?;ZmQ9 z<9p(8so(AzC*rnr$sL;;zaYeBuel!Vo{>Jo4!abPXXnp5=&aOJuldW|rrDcmood;* zZg|_eQyPu5#k=l#+ss<6qHOdoM`Ifu<5*}jibe*x{Hvx(6_ z$Uh^xPM;kjakj^<>JFQ;D6nR3v`4rt&yOAb;K_Y<@{+9lig5WA;qpuU_WwuWs{FnG z_{8+28Lrh)t7YK-Euph$ZV##8AthSrsq|7}l-^1orLWRY>8}h>1}cM;!O9S2s1mEh zDZ`ZE$_QnoGD;b(#4BTzvC24QyfQ(Vs7#WAqPYojJke&Kax{GK+g(HNOqHg#(Az)E zmY;9#Ya`cGu9G8`tbg#3+OD{DUeYx0$Vm^Id7xhHf>Xr_QXn>RXKdAgl*GjeX~{_$ zsmTe`QWH~>(`@1P*f8s#wkh1+&X0|EmXst&1(TITWr~s{1tld@j50l?kLVkx5Ku3h7K`8q=A< zOlC2gIm{)4Oy)741uSF{Su7@-CFGFHQkId&a#paCRjg(WYgxy7Hn5RRY-S5v*+xDE z6jB6iX9qheW*58JgR++rJnUmX2RO(f4pWLl8AmwEF>c^Sj#EwrH&MyWoZuu?+`_Hg z#wl*+4({YG?xvc1xR?95pVK_RgFM8;Ji?=#;V~Y^%URCx1W)o5PxB1V@*L0e0yUiH zMK16XFY^ko@*1!625<5fZ}SfCa*_9VpG(wonGg7okNB8R_>|B1oG)L%3}w7F;t z(UzitqOC-OL|cot5p65lPPDyf2honA!J;9ep`x8c-J)TlokhEdhKojsb`|X=8Y$Xc zG)lCGXtZcg(HPNQqP<09MdL*Kh{lWd6-^NBC)!_hfapNcL861@Ori|CBdxJnlas&VazmU8MBQ! z##|%A$Ta2|^Nj_@LSvD5gWOqiI@1{vQxoM>6kG`P*5qf2SDfmyx~V@Mq`t)+1O%iHMSY~MuAai6d4%XjUC2LquAJG>^AloYV0*i43DwT*l!## Y4jPAy!$zs$7-hy0@zzxxmDuQi08-c(=l}o! diff --git a/package-lock.json b/package-lock.json index a4dccca94a93f64d0c31b290eea24aa5da3c5457..b2d2f39bf5f48290a45ffbd1aafdb25c0c3910bc 100644 GIT binary patch delta 18971 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

cVEy&efv9Sy z?$70Dr2iZZE`z=BWfgw<{SYq+2$JFXVGH@gRLhnSh*b#sXfh1JKBWUAi`qtz+s3Nx zF*30tG&^f2)+BJI4c_>q_=hMD0xa zCTZsj4>6~UbN%6(Z?}4pukD1ub|xrC)PebfA#34x2HJKd z?qtDSs5NZT0_VUwEGo`fYEN|g&Gt%R`+nG5tykeP>1HAs_~0n{)o8tqkDX|S%prTR zAl6E2!u6ZaR)+6{dqI%p3qvf#JS$_q0@U=jJ&eUv>B5IiBMg?>zG485cM> zr=D+0&AS8^{=m}T^Mjy{5L!P2p*h_fLi1k_>5_Juk=+A781KSk0uJ<~CM(Ko>_Gi< z#Of`3W6F^_v_4rKmJmloH01fnayTDsaai$&R6rwaj}p&)$YRq^v7W*voK!43>cHNkmuJlhzf8y(NK%>{w` z5V{#SvoM}05!@U$IMPYyOQO3%eATQWRIP^0R>)*f)1yugj}-}Ix-i+HH$i^k{P+tR z!KQr{PBEe?LYq&@y@m*r~1p3blnM6$4v*RB?&Bt?y^OW&%m*P(W%|)R-4=Dvj0FUC|hcH2cBm6^S=;0vGfuOI@_zku|mELEn z)??r`Vccpq-*pdG+-cf_QEkZ{mcuAr4Zu>>8eRkq>C-rIO^yS13?8o2Jan_S`y z*RltK>;L}u`8b41XX>4d$$aC>Ij{k^P$b5FdvO{-xgyG`yh~nICO=H5@~mzc)G7J& zj;$N#UbO(m_th03ANfYN_s3XE(A`{M`6a5FGg_O3fzg`pnKd^e_Hpk3VcYR+-8gor z&3F_pQN+>PZBZ4c#BkRenaS2~@2F$e>1z8>Q){zyCX7kuZlvwwaeG}!=8f*iPbzAj zId}%thh-p2h&6)_Y8xG^UBliAQw;LKtvQI6YGKe#}wfvSwQCJ-I$m<4nT0y}?NADx|H& zMq@=X@V*6i2AS38nKV!&g%>&mw1sBNO3t+SP93ecZ2=T6K4Y;u4ks&7UXQ~z#9RgB z)i_Gq28$Xvb#VIOA*+K)bP+WB29A<1J>m$KQkRW9-Q*j{FU=#X=ZvXhcvR(5v;zVA z^2pmgKM%mYq6Lm^vAt9Ej~5$Y1H%b~e6LL2!u1Foum@goPKhr087}B|{6akvPM?#X zNdkCXvPd8X1jrO3dn)h*I(LD}H*oU>Pfsu2(Fl3(z@RnM@Ovl;poCCm!cBhE#`&`z z4k*5*46t@H3BG;{ENmvfNGupOY;rjwcynJmKYLZ5eB%SSrHrVq#a z(Y)5_$AQzBOcx$B1jit6`%6;s{o&3;$Ju@xk6S8%+#I@hPFuPKgRAlOd%1gHQk`=( z$J~{9piFMQQ&j*Jmu{iBM|WQR>;260w{5@C-lcE(A&!AAo!JHc0=xn}`+!$-OP|er zq5v31ZQa=am9Ii+LtsKFh{pDjVv zE_Ahmc}3VKTda0-03YJjJ5I&pLz;q9aW2wTE_asuQJ+0hw8W{L>}g2zzks3z)IJze zk7?DG;Gl-Jq;VElNu?O6%cS5ujYTg)d+f9+fcnxcM_|$o>$Qfv8nehCe+E;ys4yBi zvJJg4;rsjw+0Y?uyGPfP_~*m_syBcHT=m3n-}15vr*P$IJ))$)zQ8Bn0cU>!JyOjf zpeWB~;!&3cDC-l!f;WII`TyQ`<)|Tp&pQyanqisTJ=D{(4=qu@w< zfivdvKpetNR@h6+ycc=+yqJ|Y=B8lGJ^OGP9>)VymweOmyPi;O% zoty!NpL}mUZDvJBB{IlhMMc%)D zze4E@!OD5hCm88iz#N};f97u4wXn_`KjJaT`oQsg!;xHfT?@xtzvYdkDSg_bmgYn( zXL->>!0-IL^!J4>S?yOlE>qF<=z#FL4DAQaL!9h2IvdVLw6SLw-DtmtJa%awJ#?nb zIx?ZIn?~=5LW)XmL%-n}>%FLlM}=9hkp;mVKbWId^6G`}bsd(%>jE>B?_h`EQ`nK$ zYm5|Iaq`1)8-iaMHGuO>r<2n=8=#?e}_&MaK12NU)&*Pf61m?Rr*$+lHW;Zx8aTjCAXz3I- z=5^cmVwPME_tuUa2g~rdaD>e7w_+?HC!TtAd6%23gIc^)IuX|~iIh@$jD`9OUpQ zY%A*wn6#&$XgVLdqOk3FRQE7nwOCe8Sti)~hkiVaqMPsg9_QYX3;4y(^82-2UyaR4 z*L(vT&VfFF{`G$Yhv=ht-0Q)uE^+u2zTL`BJj9vlv^_@J4US#T^}#qE3W_yD7xUJk zH9Rnl#d=LcixDy)+|4rTxN$p@qzr40mV+L3nDsW}O${22r9;ned(^loF|1eLzp z8;$jlOaW&sPl%8}^wLd$#s~hz`79++ap1E;c8X#BT1FS4hO3+K3QBi+SK&hxy*+*Z zqn`Uq&{Z`BM7ruSkmxN zmGAxpd-tjpFzBzYJZAk`a*NS9VE9;bhj=WzA$mxILq|iW(d%@E1|dn?q10L^hOHC) z1Qdl$l+JWyk!Z`}2PQWfSj1kzwt7wPr|@#7w>xslp?^<@J|wBRk977LK-Kk|mjI%^ zr)&z+{gV}t)roqg=DJENQ zHSs5eG4&Tqsx5?l6yui-D_jknsn|2B0{}l}R4!=;1;DN_e5IRTjZNX%6}Y}a?bQoa zs`Ts+BH%!jPdbD@W~;u8H(+L6uuyOZZ#kB~VuW6eX>Qc>)++FJzLVoEUkiAD?#toi zfbsjp0+X$6-)lz0&2*xTz}za-;azu)kDGL3nht|qFYEfF{aQ11X@}};yh=K@E@B8_ zbb7Gb?15cNtA=CXfv-GfiXKvpU&GryfPRFxf>BrPdt>Z36a*7c;_rb+S%?JN6Xfb` za$x51`IARp`bDoHd26r&mA9J!n8g8_?*o&fYWsG7*M^YTr)KG}Z$V3LI2!F4W;s9d zsNUmRyEZvX*Lohdz}78t5U#jSy8!6O06*pvV|LBRUFq~$a5c5bZ#Z|6HX-tq&x-pAYATPb5^C{3bh zqHe~#O{_6B+B=HG%Dv1(bw^IN40?}h0j%OpeEwmX{nmWW%H&!02qkYIeool z%@+vMXl*AeSYX(i*fIw(rVf^!2HqG55!7Ap1VoL3(F)ymsUwB;48v@sLlp7GJBm_x zs>5Q6G-7tTzYqUH0Q+00CXlwH1Rzf4dtRPT@g{@>RK>Hs_i~dJ@G0NUHPy#{ybysE zd}R?$4jp)+_J#}{r_mndu6t2q;Y%aEwhb8tlXaEUk_7Ir8@siacs2uZN};JwtGi2XPKRi9D_o|+QeA6pmOGrN$qanuOXV5MNl~Q zEpcKW-Rg}DKNd{*yMsbM8LKLV0qX2L)uG9os(?d&Imbd?*5`d!X4MCiAauK@hYMvk zQKhELND1bV9rE4RAQ#XR;n9~pwq<CCxHWx zTH%Ve*FA1WKDLbisd!R(!SIa_`icSh-2R8cm#4*`lO6y`YhI8Ffvm`K<(wv$B@cmv%YjxVpHd6E+KZUf@e% z%-&tVTA^J2S@Zi1?9KW)7`_wve*@&Q>i)q*psJ<-nyW7VpPDvMH@rXr$0Y(Gc*6nU z)joS#b#ARfrJV0PHm$2IJK2vE%Tmct4jfE7i@ zIiK4C_ML$uxEIIeRfMhzz-LSK_gSp91l1CXk{cDkyN=WkO8zSb)X$Ao;O=|};10C; zfonZ#)zE`6IZ6tU>>p5YJ^j^<}8{zXp(8<#0!RfunSu8UwIEQ&kuOmvhCH zpWO3)41l*6P)!`s&`3j9k~aa-BWp;9r%7y~G};LkBqGwqc@J;cdd;)2#++tnancss zVuSBhB3w4C^{FvNhWqirYWvwt!8x`y@D|hl z!I22ZY}!ZS*m)xSURnLZB|5uZe~U;CaQ-w;zLPJBgN*&B!sFap1v1We04VZCKQL($ zsD;BH+8a#h9l$XtCb|X%sXPwaE`xdEss~|8BA~uCau~(S&H%`DF)H>czZhI_uTwXnBxCnqo^fe+%}WF zVP1nY*x$fQV7ZT&34GP4n5Oy5=LS+(tgEhUX_};MR+d`uF<~5tMF*Y;BP`aX)ZUCH z#e|j0NE~F!a&Eg|>RE4ZPBK=`vE>*oB1};dimPYZ(t`D9wVM2(f%JcOxDR)@u+Bd` zp8a}{p4X1b8zFvb*Sf`Q%{?@ncE4v&vDwsh=_bOeJ?aq7Wx$A`{I-aPX4X7)Y3 z_42DzaPx&)%?SvoH24?EsBP~W`@mTLElh@e#AVoHY?ktQP{GWiB1tF1GXE&=MBqbup+hQat z<)8*`Kei9-5ALZy&Vv6E#(r{h2sbdf&yA~oDE65&^PS(|_$hG!?=(x@vBy>ge*oh< z)NtUk`937&NuJTZ3y;}>MQ=@Fz=sU!VzYyn31e%|bI4li%d%-srwk}%(>oIA)5)&= zbm<#1L{9bu%i%xh@?F_u*x9IkB%R>%c>G6!QE(`A4hg+m<+hG{z3sMC|APd5#-0z| z0hH5*3@GxP{@C`K`WM-d0du`-CGyxw#XBW>jn{9o{VkFK#=rG(bJ%x3+MiQejV`kR zG#BPph|ER3&JHmBN<>l3UPl(Qajh-Cz?0v$6k zxsSLiIB)7k;$!IvzbnDubj(D2U_YJZ=bIFGt7D#5yPX$_F>RZb0@<6Xil!P!p}POwA|w5 z%p1WQlAQ`asDj=+_I*SBf8b#sXtm_Us~sz@)2+Uhfl`7-m&VPG zO5hoO+PFF`c7xMSUo?z%-^i*$XpMC`!f4-jG$~)u-Qfa>M={BFIs@At800iMr1`8g zKxcD`MtM?Vg=4-!mSgw8v1#2OavF$3C?3~g#@kZuWa{7tHQbevbr#ik*3Tzu@Gr8* z8_TR+>LIpyqlMp@B-a@j-_%RjO8^$RehV0*N$|L%fox6Lq5|S1yleSzH>#7a?kxQy zHQh#?(VUqnN>6bOPqK&cWJT=?UM@tuYhr}RYy=90HxycTQ?ZPO7BY9o%iTKY|FtGa z)9|Itj7z=Q>#m!>-0bNLzeVu#TEM`2ps`a&27Mho%Hfp}>q`W1ZNpADSjPm8myjtf zWKiq6*@iqt6Z=886VHb6R^t}vl+*h3L{Vne81yzJ2C5+yBb)~Z_PDY6!O+Z$*nOHQ zeapUWOpxdFxxYg6zTZshLcAYxBF^gRn-Y5qn}D%yef&QrV=y;&Pp1a&&(jItsmf~x z{Vw8PEz+R;)d!CWw7*_B1AUA-!y4oD-WH+OBDtk($3YT~h^l~uC!R=kqr*I3R1$<7 zXPm}LITjRK3*ov$(oB6LO6qhX&>Se%L7;N}n7Vw(V0%cQ&oXh6;){ZOx8vU8%6}Qr ze@)+?3L^Yp`9HU&`r`o${UJl?12(&@1%AzuyS3EE>KBN{Jr|Bx2O}rOu^kvGhV6}^ zf>(@;krXmVacy3VkNCF0U`+0?l-p-)m+lr_I^{cRx2B@yFe>yFOUr~}>q&i-g0Zlv zjm2Y@^^c+d`sCyt(m@0TxER0WJzv6>4XV$pHNhDG`i_MfNpuj6rSv$&lWhd=RYqO& zB~^0hOqp#LgMCq9DWwlJoFsaZHl7qF)02*J3Uh^yOx4guk2a+X9!^m+|5&V6gzWqV zl?%MeCV%FyFo&*}`ar-E%P0d+%U~^!(5KvRL8C~Zx!+M21Bs33S7$Ta0uCs9wG?Ri z-@v^?T5(~~q)d_8L0mG4v+T$#$}X`m6~nOKi{(^v=|V6EhVA1iC~V#ySyS9Vnqb2P zjcwLjmSo+~E)KDeSYYG=1UXymrtI9C$I#q*n<;*pt#Ka1kW@wfG9K$xrga{NTI8q3 zbIq`jrVn3B_IYH#UARwqS#OC*{pJI_>p*wlOv8BpN73cx=Ac?bQ< zz?j*oAnhYuDt(!&z~O*YcZxvKm9PuCAN8eaye-Lrwlb%q<=o?FXR*Xr+e9^~sci3p zV~7V!E#Vj{D7=|i3>@{Qp1UUu|8ahrD~WE*KEUbt%M1%(E1oaAVzu_NYc~GdAMa)8 zEMC`yzA8q!AnwJU1sbmh#z{TK2p}E?nEehb>>NLb+sVjU_E9fyFv;kv)<*IgF5rUm z8cn3`ezGHku6MpkBjSKx#O5gDWGmIOF}bEDl{lv-`rhqtlSN0Wy&SZqk3}cvrVD|M z)079tLeN@-i?x)yvt>7k7qf_zA#-tu4Q@87|Gsqr27c$E_Vz&dmZ*WWpo=xxxFul3 z-Q6i+x4{EUzlje%ouJalBa zgq3~WrMAlzmpGw_6C}Qm*X}OUM3rTGhQ7=aeGjw552h|$N`#+>$GmgW-aK|Ifxxor z|NTGz^Y6_WH1hHSeR%1w3GnmzO{e8?blyf*ezsKRrN8jhc`pGjTv&jIK@=yPyWF!TSqoQ0AoTFT;xW zy0pa}ow;GEy(t*2r%!z{1jdQEMcmq^>XBU=M}*edo4eunl8!HYao+V#ry)Nrx1Un) zJ@5SvBEDJWhNwS8QhK&`06~cT(e6!qIL8%1zZ1e1qQ_FaamPq6v#W8X9KqH}JYsxx|NLLg&O)C4fUrIC z=l}Y2$2i}|3AeA6&C4y^&?(@Kz8I5efi>Ge^ITvLXdv7nqP^G~TY{+~X&dB`*#?=Vugoy-nVc8vY%IY zNMvv-rw+D&kLZE1ja9ftL6;#)pB`6*taczZ8_&ayi1qqZsj*vG7`lCZ5iJv;gAlq? zS*tYOKqxvnE*yMF+1Ppc+f^VLw0Yc49h%ClsX(lz7p1_r1;D zp#^bq$h53nr(;ldxblFTIvsiGW;hM8F!EhuI}&iaV)<12IZFJ;8Cz$C|3-IRuDG!H z%O#J{>^o|#nX~omuD+8d^3TrKO9#MHzQAnu9?(VO=>+GF*6KPvKhZ7QA1gVDp(?}- zI5H#D9>40iB%(;0C8yQFNFt(5XVX!!>GqhIQO#;uh4fRdDsQ;i?ZUBIYjgw=8+jQml8!WWT{}B2mZGtpW_!zU?w+4jz515NC248UsRmgf-ujUTn+mhN2j#sqI~1=y(P-gNK4zWKq`q>iRh z$)!B-?hmgz^Ml`A8^x~?^mf5ZGy3Lc!SkP!Pwy^y>6;FIcj-`b_ji~6*Uj(F{jTr- zuHCx9EuQ-p-~GvT_t?|(eE~I>MyFqUzUN+;&5zE#FYn&zb7w>Fqc3pFrDxFB?(ov` z1^n*Hn0=Ep{+3sot{cx-JXhM>eXvtchp*k=bw&J_7rYd2H^@B4Chy+pRi$oow~x!g zxq`1H__P0Xf4QbYvZ9j+YZBGJhLfwD zRUokEY=FCucg`6F%UyCr-~H@WZbp-+3aW~r?i zlt8vDv~{2rXwDWHd9t4oKD&VVan|dq<8Y$DHNHk?NI2t#9mHAP$w)sQY7AdWt9W+P ziF`EL`uu=xEVa=@trLIVFW#O-KFI0!Po1BaHQX}VfyTgk5bGdj^U=(vrmeu2Bb6u0 z-BImL-T|!uBvd8QccDIs8%5j-TI8(CVctf;jbvGW3 z8JVNYj9>I+Tkn`!p247!AY2Huth?9xn(d1NX!I-e`zH|SDFTx(tD9bFBLY81r{{$} zAlKrhw-GSdf#1)@|gw+St+QIv-nSlTlKvD}bu)`cPoy&1OoBIVlP$@3E> zSzLLiX>+HNtxE&Z&$a7+Jb&G?!AHQ({P61P)5KHIR{bUNdfwXp_Q!A62_WhQwu*OV zB7Par`gnN!SxW3or)zm%ZxOv(#go^lKHyno(O zLD|vSVR^n1a7_^#aqu$b;E#Vavn<|G`-6@76*K6uSw96J+*r6zdKT}&%I9_80c@G= za+~K1-!W}_AsBXSY2oul=d_f@lQ|Kty{If~@(ky^;BRL`;H@^(y|v)!MFO9^&}_m+ z?^yE}Oh4m;Wg2$<;H!sBK^zO#p40V*+Ga406ll(fU8}o}icUq3HQuX1p3`2LZTiI` zop(vnU5r-BQVSIhWs>5+46%&bbJ=(kF5>BO5D?x<`?VQ0&m{etY6GYTN2Nu5roovJ z-Z9iYR?Yw+wd|UC&mev`FIml;_!F-KC&ILhA7-L=@B#O{`|KgMNgx~B@zS)XX4|!$ z?_}8Sn8#fXrrYe*9hq?aqGFb~#*P<7uR8=SLg;=aXoY0+$0O*SFd$fA-tR7_#6sGF z%zJdTvSz=e`LsRg($gl60)a5_Hpm5nBHvSGE@3Q4k(v8eHK(dRsf3cki!SKksq#X1$wi@#dO`>}hl{fi?@Zv)XJkP(Lu7#M4@?4W*70&QbkR zj6rH_p16@U2hF#ftSFRPH&aGpmzJ2B8xG>9MOR={lVVw*HgOL z{ce-@Y+?U8d7DLU(zM-s9|`6E!+^p(B6b0=<7b8{vzN(R*J0SCaGVd;Pr% z|Gd&oJ~)5-kkXe5((QBUv5NFT2V)Ck=ZzNvA;U(rHMDFNdE4c7HI0yr(21>;tMidZ z)w1cM+E!1~2=xpp9R$atoDYL;v=|^L8U3`Zar^0hyGO_Cu9KpEz3~ra9bp86jog?qe#!RxnlGD{R#pQm8`ySFdg%qf z95eZ6h0a_p2cC8lv%a|7KdCO>!3R9^?z4wf1MZ+Csp53)7Ds!qJS3_wlgs`hnTw^8 z>q1mZg#oK;3F=4dEoyhSmN3`)lZ`0wm7s5nX=f+?G{e)_`5rJ>* zz^hRICCYH);!70zjgRmWnfNDJ6fK#4gAs9t`@#wjn+oe_l_(%5RAzeh+TFyUUNkru z$}Xzmzw=b$SK6v@4r94#xG7>0teFqR!HgHut0c%x3K% zs*jN=i=^KpRa5$_c;KHCUi6|$3q14boC#3>uv_qc0YKgNA3j9ikyZ={FoI^4IGcs3 z)leA8ySNM&9ioJ1^bCwPB8Vy5cPAz3vq-7gY9aE5X-}rRi6ILm3)_R(Pk3gmN^qyk zr3-8O6PCdTGXA^$Ixm0HvP`RinbGKvk0|#XSo*3>_Xc`Hw%6;mG`n{ynQMy6lBz1i z9KEWz0kd(efnf|CsIC%pxP>ee#p?M|#sx3nB*Qkkj%~PDyw(ZPIjGr4KH#tlJU7UI zO-`@NEJkXg&ZByaI(ODZA9v`9sPl{ zyxI}}Ioj}YVALkrfN1&ifzwB#^-Xu_!*v=8dnRzunlsNg_wW$WUCL&*A$pV{hObKF zkczztDdoq#NF1ZhOc$!|f}cgEK!PGjfk46R0I82tliI}u$l;V)zg+*si?v0v5&WWJv6+ zvRsXT`=ZwSU0$wM?m?Rjknvtm@-?q?a>LRL!a`RnyqPL#X^Xrj*N`Ji+(o(C&+y6+HiGDelcOMkkCe)TVJSAi|@{jTf6_GWP$U6*^A1^T_7&zlNWhUwij5S zl=eBt>T6+`7jWe#XLRRlnxvM?^PHdLd~bgt%VriRqPeZ2z8$Tb9WY$)w^%5hhu0tyvJeTR;yd@N9A45Q`o zaKxuIg87vy?sX5YLYnFxiTRKCWnbg42h|tg>dG^$Co9Y1?8ykUJojwR0I6ipqj#@t zfQwk1_C;{{a}9IDwck8hypZiZN4{d+7oK?|A#b_%IitQTy|y{WKR{~kOT(l4JqI36 zki6XWIiKWapy^pneASYD3;s1PpU%EoEp#UzC$k-TojE+So`tgr8BEAIM9$Y)x7r^4 zb#JWtonC;5hdgkPdbPH_ovN?x0hq<=^W)UhiNS^-MAw)&TvA#37_{se_J2a_eqd3( zCFW1(wO-@3(2U(}74+?(@>}q4$kwd*kWFMsi?va5+-R+2tWshwlada(*_fPG;klVj zh!``vM>z8%g*NfSxbOFwo@uG#@W@7lSdA8O$w$r@H$_NXuMSm5pZ^QHjUQ;fn-cd; z1NYVvfPA+;eu#t{XSkd2OV#G~96@V+OzReSd=%iLQ7JVyl2@TU(j5tAcS9o{`ay35 zX2EDfT0m>btoX_bBhiYn4r}h}nY5DjWsv`h7x|(~zmOR8Lq^K)7V4HpP37KaM@5ac zB)-~Id6kcDMc4GxVxf`E8 zByGZKo+Ipjyhkf948<8?=`OTq#cICaF*dvt`K~U_P(C(x(4ZnFWPgCWBPEuZ#V)Lr z#g3lVNk=xfoKixIcqQU1{6~`5KQNkKcJ|Y8Tjo7kxuGSO6!*U@P4gPz=ZxXsMd#B3 zZ^(T5@F6N!-95RFLe*GreS6LOhdr6Hz4@Mz=>9%R`~;y>5JUidEfKf_=9E%+dKhNV zz}4sD;2;4<3hP^ZvcfgZIc!J!NzCke|H3x*oj|}(lJT>p06{_1Aqo1x2wZa2Sph+WuJUy;kB1tnTwYg=Le*Jfzs}_pO;0vy7UqoI^Wqj^8V_=@{=9>^@abG@~*d` z-y5ps90~ra9rp@$!fN)bU>G zW74{Vt6**+O+faqxOBgJ1A29!cmwy}4v}8~0|H#$JY*#dLNdaAOfu4F1?@nvG?3QA zndM1VH0}Cp>ZlF{Yo9BdQZ1r^5hJJHo_S*^is-?kAiheWe>XZ-KHcMzodReEX={21JB9Lgc^s66beey5;TYe!@SdT$c5zJ#Xi(I*f& zAkb;}E0Xo1J5LYt-#pFrbdxvB%zvK5@0U5#`~6oB5kCimj%Xf%cU=PJ=QxFdFgFz9 zL)sj5*S*bjGd3x2Fqp{#DdFxE*JcZMA1%BcM!_037X+wp#!6p4XuR#D`DE#I`{J*} zHUAnZyuwys&piTuq))z zI+kj5IzZ%29b!Ws&Am0QWkm>;D9tJ>-tvH*;B!ZtUiiNvqkj#VUdjH? zQ|JQzOq|O{4~b8IH5Yq563x}hG@Usa9ZGBJ;ZQxQb_(9v#RY{Z-N9z5`BX*ve&{(O znh*9zMBWm;M4TyGZlSr-l$k25Q$X3)G=8M0_^vDVmk{uMGS-h1@y!x1gnaYyL&W6K z zeDZ|Lm6~+~&h$6S(E>K~f%X$7DRBKB?f>!0%YN_Sy%rt|7HC9m$2zeQ0Hb7q-MU;H?x*n)piE{qR!4AfSz3d^c@w} z1zp(+xue!uBIuFXL66H7-*e*F4~(ds4?wp)w!a?GkWX^SQt2R*krw8DP#am8MC3x; zgI%R5y~-*_AVOxYjF|4TrsKO-$yPm`19pGwdQ1qaoOV_;cCQ}1943; zrAXAY2o)ZzOu*dO852${Cu1z9N1FpU4@`2fMT4~HFPG)Spayd=;b%p45PhjHj!b49 zds09b^iK%iKU17uGQDrg(XWu}r7oc5jF*al`x;WrY6B8SP;W3yDnE!>Ud+NkGLdDz zUXCq)xix#FFU)Z$UNWUM^!FolIAq3XWtMwb-OW(Bzd9EC$#gvd?io1)RfD5n@qb@- z>(5TgKi*pZnWQ}*Bix|v`BdRs2L1{1ix{5X0TH}^WVH6tRk7bh8%` zqipZ;)xqlx{ycX$_!fU&puh0w<&%f@?_RIA^`S1#3a41r!MyJk3}q&ZtrR=BdYD;A zpJLn)F=a&HS6Z;{A&yI{>=F`e4;^+9KjLB+URngwM_{^CT?{bqFHD5|OH8Y0e)xHE zfWz}MA;9a0NT5SUOSh*(dualXo!&*`VXWix9-CN-D0J0PSK*n%%+PXroDa7_z8x~9 zw@@uU;`n5*b;4ZYd827ej(WD zT}v|jj(5Wqnu4>3kcJ8ino1LlwTOnHmnA0tc;;q5~mEA@dWS_Twq{A1LhKN6|DW;yB1*JI+F__<*8d;K`rQ zv(X>dhwmT*ik?>ilji}*JM@%m1lqZwn#!3x3Oi;vOV>eEmA1N+rU(oAAFiDQk!LwL ze_%V%27~>GVZ_?#S23zCVWy)_-~qbv{dkoQ(mls_!4$MxTCf>9^1*P6BHmdMm?4ci-H0~X+>{RGVaZ2b7bIX>5~fLd?h7I;NdLHh^Dy6d3W)4cLo z=-5GdJg&eTI7&v$@|Y`SHAm3JxOX6mF3lYvr_@|$6%EEamorDbJQl5$#PS;fx9n(K zd%8-fWPmZ{X3q)uO+Nn=FL9v-0Z3i5;5agFo@{}cpI?r40Cs!@7ht9V4;ZFlna>fk z)Q?;h2*+@lb_{%u^kpxx%54-A1%y$Z5p)W4(@A^+%DA*& zCA$#b?Y$+^)6{L0PZx&|8Rk@CdULq#Cu0Zy-u-mu;PvCDrDyE?{K*P)BlD8Nif(fs z_8s=SVEO$r0O#*NY(~@G=gnbfW^^+%$~BJm=eV4VHt$_Qv%6QS3ym1ld|9A z4lx{?LvM^sRuR3GxW#IoG1DDf8<3VT zy`Tnr3^hh-+2t~KQ7qZiqkIyB;Q>@XDA^JZmxmz*&6jFL5xatyj+1gI7n}8AG%2^9 z!|dlBxZZErnTmIgm3vz2z!-u+CYy+X~+T-*jVUo~xBGY#>A8KT9^J(H%+ zS7hj#-eEKCj`z5St})75rfO$nZyj_*;9(jo!3>LxE>4nc+VPETopr)xo~}Lq0FOAu zjCn-2w=@}rETl&8G znwTbmk2qHy!^HyITLQU?3Q-T{3Pd4WeMQpT4u#|4qMU;+4KSzmI=yU!44tvD8Ip5* z_GLX&Csqz?VVVS;iqK%aFcE&& z4@4~(^HX={OxG%&fo9%OxRIx}KJ(BKWA*p-ShR&p*yt}tYLk-mamg02v-;m;*>(oX zxmN4y9gye-=pELz2e?}HvC_~=#1j#2He^Mnr^9AN&I_a`Oz^3uPIePn*rp`5Bdh_H z%35S??Z6r-eQuNQEU8~ZB@%6Cb)Fcdm<@lDb%9Y4fqC{P?rm8eMB2X2)fX`DwqE>C z;!nK$Eq(C}6)^VmD{vs#0K0?qMHg31n3G2-v@o+78dS)p>PQ2{qA+MUUsigy)g$16 zC<@{qd3j@tGf_OuU4JexM|dBM=hR_tD!rl7?PQKaq+Trr_!+>xgV_M@aVeY4xqdC$OK}(YI)MhrVV$@}Z|8KlmG5Uz0{TWE zlUgPEYhoahmCh1!fekSW+KdS;zz;O8?v?#Swj+H-d{t>AmX*;~0uG76K#x}ka?gBw zHBtHzg={sk_77Y=FxU87jC+C!P<(pP?*6?yxZPvqB$nhMmv|-(uSS)p?)Nj!*a?ip zsav+}2?MpGa74(+k&tO})L(?;SW-jSr&%s-03!PLX)ShK68{J zbFs$8$wt{6m%3(U0R{?cy(|m+1hYtzEY-+qlJ6EPgzP{UL-{9dm)BeBZ7OD3x|s z*{rBdoue!V49Oh(g*RRBK^29#w^C~m{#0W=#lodan;~ycHuIsfutknAO3ccpjw?fu zSowzRCveB@xr~%NOtDXun+JwcScl@NP1mt=FgMcx(a{Fs@1y}F2!7Ly{9qA)$q&8( zSOsg{!D`BbUZT<@prU%em5##+?ZK;6h|`-RKTSa&=FXg)!gFbhjy=1Z$E&h426JK6 zJ{MWBg*eN!8)tiM0ql1ZF^$loSpN7hRzFjO&rj9pBru`6cM41aGW_lT`;-=b9!F2Q z=X}r&v|ZcOUrb?B|mA&gg4EOuf#h-FZ{r`#=ogc z8;Ahe%^x7W0LmRC!$~~=xq>{6ML@}o8Qe|DoK!(i6cEA(bG0Fy>222qGI6k|%Gcu7 zblmaUV0M#@U-v0wKebm;zo)?qiB@8FItii$d~pYVMS4NiKnc3%f`I_q4{z7kkQ-!O zE%eZa-GN51xShjvHCFdsg!L<(KF$d)3}`qSbwUiT)}sRJtOeAv7Wf|1hw&~)H(GC9 zCdF(qLstjRJam`-yoU&Vc8^5r;v&AkhP2GlZXO$TY!TJG0DJ`?-Qg>5 zsYvv6tqr^EZX_kldOfMK$R@_z;Gv&8`gGt)mUhULo~iuGb%J zBxSZ5kMiEao^*6vAwg2=PYUms#PkFH16%I5G-H@I$jn z!K4SMXAb1iHZyuhOPb>Q{bD@PmMa&`69U=0lkGA+=7ibZq0(9=ojyuu!g6!i$RjuD zOt+$rt$&EuKylCQjF2W=f_^ob^W@8gEzkx(?F+zc4X8WZmWY@er`6Wu3XEO!H{zOK zEawmf8pwpOs_e9C5lqDBfRR{9sM*>)76&BRxw!8qVu4JLkTO9OR~lhmqDDxWo5r&E zunqE=t@pjKOIT*$|8~yJ?coiLvG+aC^A+e8%~wkSYy-G=kiEc`%l$I3DrP!I*lgN4 z2>xME?;T{2VMM`HBQVKGn{TEA(UX(drpPhugpN4|3|vW0f|c9h zntE8YKQ(5;I!~L?rnj6EFdp}t7;Atwm}>quGTT79gCknvhx4twt8A&W5rljS8CGR5iGIrT)R@>WbjKiFJ@Bom6h+-2?|LIL>-dS~EVjHnaXDzCg9*;-oHTw? zs=tKP(=ph4&IAkv>a@SLk2LEyCuoIj+d^ru106LPygpWrMzJiY;TT@&vcbUa1 zoDY2x#vl^6C82BjW*4-g?v?X$G#PiR6klY%(ix1rLeklCCZME`d%LLAZ%<*lcRE-` zre}ChtAyyPo`EhzVWG_$-~%>^QR_r^QKwUcpcx1L$vsR z0sTUPm-nqVb1z9?FqD-{H{&_M%KD)@8}zk;7uaZbFawX(4yL!_W|PIjvyAP8N{r6Y zCWJ}4?u<}>Q-SnpwF1MJ$9zs8%iX{qZ=tH*Jxqj8x2ERdLJf=qxj7iC+gLf@J5IR( zen#18m8TQPJ6t>I4~N<$>C&sfWSm}A%PZ9*q5e8XTn~j;q*LKTvS07eOroe`JQc|? zx$|Iupl}i!kKuurJN$wycIqq;`{;0zmivd@zb|p^BZ{F&ZmWdv=43>1k+fI^{l}X~ z?mcf>%~c6mD6-a78Dfgp^O2PeD(x^#2y08BgX-_FB=3;{kr?_vMInv$dkwy*{m$ilvb?}p93XQw^` zJt4gT-?HpIOLe7DGSkUYY%MAWP4BGo-bQt&LRbntQtU1~HjY-bndk>nL&wMrt=t;V zTyN$FkcD~UFm~$MzUQkf!DdX?;^ew>bzb;&%(ra#P__`Dhi*N2p#~0!a*(lo3!UKn z^_!8jx6m8BzF80id+#T;_XkebghxW;hwf~4&_E}EVxLJtT9zemBfeNJ@}n+yBV3g2 zTK8;x4QDltbC}onG;i491~4nbBtxJPNlG(V9zd%fwPIh){R>lnBgv&by?bZfc0r&B z{a!*w+5}0`9PDEah&%dYH|?3uN}=mfDgm|>32oZn;xw2VhZ7)tf(|Mqg?nmevR~3M zMGG>C9=6;RR@ODI^DA{2Pxf+5$F|K{ER?|3WEW6Bv+=(VMJO4FgfUE1b zz*+)O?;y>c0t=AScVLRWIC77kcIYf71ud^;)U01J*mf(1GrVWkCgHAUP)Az_Ab;X8 zTg%>BjIekJI<&$3bf}t@G(!@0nd(w_*{yyhjQM=3`wm$kC9?NT>#_j}u+;o%GAjX? zJLoxJ%&fcO=Cy7aJ)?sSwuqW1plA3j7iF`fP z`8wa^&uh>v=C3k3PGIj)U_&Y)xSLDWiGP1(4Ug%~R{ z?vT}w>rHmp%7=0~C0CT>Z}K|l6eP++VK{P!^GTjvCD7kt&<7}Rb<%&r_p`C^PJh1s z{3|&NP2=sGRxLR_7$Vjl~ zu?IL-(eF9+&rktco?o?%!0wo@<^|hYKNx%*-RYs3u~#m zOHMWCL`NhPzhZY4;m!od9d1@t-OJ1=$d()AVR0m^ax>gegDzaGlLpsTt^;dR2!Eo5SF zYjO_HIw(5o_o)p=c-b`Dbh?{@AnH*c@_drj(s(f1m4oetrG8y&enC{*GB_~vBmhY- zAYZik^OOW7$bq@FSCM~Wz*hW@O8j^ofcRJI0va@n+@XQ4Az4Oj?8?FLidp8kHGmd) zVEY&YVGbU+8)LXGcXH_#hAAI9fiz*&V8k+m@ic?*qj#h=cvY#Zq%ei?THR1PY49^N zczzlq4#P46b*6A;IUpUe*<;#vVCPNyJQE|@_2_y7FQzyAXRDK)2; z|Nd_v=zR`*pJ3QcXgoRxV?Zw@fWhVg0p=qk4gye;;8oU%&HwfX=(F8?*_a~v{{zFX zFZvTa%c&LmH5IS#_+G~N_3z+w70Dm}Xs4;RPt(2vX&Q;EU2uHjvgXFXl^u{2)n3}9 z{hZ0rhD}=V&1L`oZ%u|h@%R6?)8OWQ4CVHHy>l3@+4F0KLpK%xy5NL?$CSX)IA-HD zlQPpuu4?LAoZ!X^=}m z8?&@&V?T?c8MC=+>ha5p_|+4BPq1$Y-pTfT1^Ys2pvs>M3f_q8jbD8(-=?LPjf^xp zt2uCJcjf={gz&GGqqj`(yTs|$IzXyky?;!&u*h+?qiqLNFJ$>hK*(fIY?B#3N0&|! zMFe$RSK_B{HsHlwoU!kNRQWcoEk9uTe^KuK%-?v2p!2@^ySVORi2#+gkmAD@!|`B0Dq#%8J9W4M3`Z>DsmCx(C7=$gIK=AWko>GN z|DMAA!1Q{nXL$t%r$!+L zifuV?NY3Mt0a38-&XayVnoVu518Pc3JKw_jc*qWHVRgVj{~D^B4h(ofgOL#YlK0Jm zd^}kEueY&(N%fuDy0K3 z{o2UEi#&aM|9#eZAE-iG%Zn!`h?|xWfBd6KZF|mr{o5a4TEh8@CW>}0T@1YloLm-$ z+y16;jGp6bfBPf1LQs{~e%=G8?-z&Zf0V|(P_ezM}n zO@SDQ7Cn~&zLUR`qkqyu&;8Tes>2tr8^lmN;MvrFMoH-Be}aVhw@`tky;-L{p1&7J z+D6o*5=mL9vyS9Z{nN7agDRdW;LSFKHJ66-QLu5%3!BkXDW|X zZ~_8_O4kFETgI!yhGBjcCoeXD>)`8b$1k2FuWl9Ia65ub${YtFUIFPB44{qzYxJ` z;RB+#e;?c8L~6$}dR<7`pqVB;fM`2#<;Af(mR&{68tWixn2f{&h^{JBxpI}-Og~S$&8I)mAB{^=-Ne% z*~qrV+^EM^Z@i$6qr?tvJ767jFCVc?IhfE~P4|gaCrYJ~@~o)= zB+E4)NtP=VU5sU2Y45ASIJDwjvYdAMnQ_GWKE$H1MmzdWU#@n&{lw^Rk7lgpGg)5> zQ+kG^VgW+~ju>PKYf%F$d?;{3B^szbZ_K>s?fbVVyf+*2jq{%q=04qz>vHGqv?G|( z6@zouEC6@D16=-&nvI3`)dSXBup4@wR%uJl?peW$A%_K{p-s}cSY#j*5?v^eu*&0L zN{|6DiG&E<8G{({2ZcyvX8A2yufIO!cfI9bcgFxj=9|pZ1VD~t2m2P6v zkv=GnJ=$NarkR2u3;0+qq_C6B*SvUE5I3XRAkLrY}U?Ux~GI(G7w2H z&s$ZZC0+^Q%JFm`D(3Wf(t)7>=~5-O0K8YhqtGf4OY=c+7&HV!^Lc+CjcHHJ86WMZ zh?yPDjcHNiVt>`8@d`!v>aMKH0yz|gmN{1O)Z{F+OxCnleD>V#gxdc7-@1TZTPyAZ zN*IQf6gM+qn$n;zCIf3HXbs*^uK-5^Aa`{BLZJ$GrC}kqo*v3dqH%gLgh4Kh4CQkQ z1?@3Gm(^XkEUP^vP(w+xwu}e~V`I(svbhj+Q$stfak&JVUfb`W9wc%6MdyD3-EAX= zv*r9>P2(E;9O#KGCoVhS8NUV%vL&)5Ka6O$nVB_uK zFz?7AsCM;d8l=*>r`etThTkg5ax)7D6GR+eqJuXHpkI=L{`N7f#5VIf- z_aX@&@i)K*s5(d*H2^y3XP#abm(f?(4W*h!315s)oN)XO`WgH0z5()Zztd)fVC#BU zbxI1i#-kamVy2iGbf>%H$yH73Sb{>xg2WfYU=|Hw5YN{lU0k}8`6iN5Vrs$Die6+2 zKJ2@6F@zg1(M=6~Q9mCimYiwx5ArS#Kl%9qknx=^ZxVOD*kIs!rn5NmTktYFSv^(I!e%jpgnTp$KdynE8aZ(z-LK&1_c= zJ4;k8Wi|@29%HfGC}zf-=tMZ^8ndjz)mGyid#e}39ldeeUb|w_rqlbQo8x`;VVX@h z$8#ptJI?-wi46?K3(N-T*Y6)Pur+bpqbAaPHtUU92JuuJQX(4Z#S@E5H=}jLLF#0x z4OD7};C2zNm35krv>eX#?R1Hu5kbT5bSRDMq%!LH&dlM%@ebbK7|$PQTcS4-L zf#_$rKdSiyL`-+P4k^!jG$xu3I}wL*RZ6AGNRicDj=I}BYV&jr4m9Ka7mIkZP-Z zK-wa-XRuI`F`Dbs#C95Uodvs@n9G^gm3T=&ZJP+^8Q~9gdnx6IC9$(6UM+E8epY|g zaj$y6-n1p&@aD-TzD)|Z1lh2l`ta%04h*E40weE1W8?4lwX@@Zvn4Qd<*Dskd5bI{ zgZ0a9&1#g@P{to^ zvhMjT)oVH=0|yA0Z*8jWs|E;nU7*kH7Qbnk|4y$1ySeve%iFIS+W1vtb%S5uA;e5QB^jm}Gg}ACxfau9d?8_M%`Q6;dZUbPwYRi1xTsVCc=q{#3*?s>T&_7^c4Tz%HSsY$dv55y_*V6ay0IPAvnpICHS1wu z+7>e(^_jUh-{Y(H@M&B~L%P!|P;mzad)zaH>6uDcL&m4p<`$hyuob|5!nyX8D&wI= zPhwc9nU!vX-bn6v1^j1FxIdqpU{cWbkP*hzJ)l^P;l~{b$9bP*4}m)Vco^DeJKTS0 zMhVW!L2VeUdqrMHh1U3FfBXYH$BX&(w?FjL#8A*I5Y*&HZ2;(x|7tdov(C*d&D3!p%JcZcboNxX!}e+n)dW<_VSb0 z7DT*edoOY=FwpPx%_%RV1=b{oy)YjL{+*c(B!IP6$^SMj15l%l4gMGS7Wwn=>ArJ| zg6|-`BVY>{|6)Y&HCRBv*B_h|9H=V(obRm)6OYnyvrxjiKPppuvi3mFD`IXMBi>?F zF-tpj-dz?R=tH{{MNi#i{T3jkNGlyn*Jb=+bDX!f)SH>R_@B_XFDPK=* z1>7wa(1gXmh?*vc>;$k;_~$q6*r9s}NlQ-L1}jM(@fbhuOY$}**ZW){@>q*pC+_ls z6p{pk@&kh*b1?95pP1hASTFlWSXH@MFxymkFP+czeqwULOlE-9eQu?F)1dxDoqlfR zHjkDUb#OcM*%ak`^{nLr@db4!_`pkStftSI>8KC-*^r}FXr}w#8pJf+bjpqeg+DmO zjuhnOLB%KPp2pd(P#pBFDh{Ja@=$(vfF^Y8nu~x$HwNH4_A599T*nJOD7;FdF7fwlkZSdaDh zqci%L{~y??;Bfo_1yNZ!`W7K_AyF9eIoV3P)k3!;n(gd{-Jlwm`(4Jz;rh za!2OjgF*bFn?4LhAw~Exx(aYP#)^lz=6i*+JuIyEp!)=R|CP-K%miOt!DP#%N#k23 zb9{l~b#{ZrQdS<2>Uv8~xpjNViW{&A(>}9MT$kNyvl_qD`ABDKLooT%7p(r%hW_t} zMe~I9*#z zwCa;FrZuz!v*i!1fjaFahO_qLK^NwSD6{H1g}T1oa>g{BPJ3-&xIOo>H4GVXGJ$Yk zgejh|*c*{JA1!eD(oEI9yJ_RfsD9gpyaHJu@qC9oKKzR1wzjW#`PFb$-gLrxc(61H z@*lg^y0qp=)9KeM%3Q~$qOmxz_4Tf?E=|g%#kLO1>V^cIR*Ccf*8oS1RLwMQp|zPw;ZLZiHxK^-2U|4rENy zduA;Zbo$}ILhK46*#8{=^c%u&9o3)r{y}&2RX)sye1i&P|8@l=EC79>#OXaAx@HUV z!)^cV=Z#@(^HZV2|RQA!jhp+?8v;8{r*YR~^ z{pa>TVU9hO>Q4=a;5^RZx;_BaQ5?VE+kzZAOVHWW!6-(RFwdrO;V>x zULCiX!$Z~gKx_wkp6q-isXq0uj^GL`RQKLUne2!t#hF&gdB~s+O%cdlEc0CQB2yq!gaT{z`!KF@9ueJ~SU{JjX?)oc@_=e3iN6pv7<}1R z1}0-lyN6ch1I&ct-PvbI!+kg;RRmB75AY}FmsfL9n>@49+kC?ugk;I9%+$K?AN*>l zE*XKJaRebNd^o4O+NiTwY=-0BYS{|Sl{lNMxbntzF=w|ak2Tl_oWaU5*th4`jM>AZ zQ^=S1jO@@Y-(L!WoaB$Of`H|CK@akT!Yj-y#;x9DE!m~$P{&FoYPSt>u>O{`XoYl5 zsQ9IBLrVFInYLzuGvsHU-4+?e#}0UFsJj^>SG8pmYsG7G;izWKRl2f%7c))1{Z;*H z+2S5jKn05Ki+%qPg_qVBBo~i(`0KZSxPIX*Z&CkxvBMiB1|hrlrksaWB*-AhmBJ&Q zh4MBk8Q9;+o=Kx^k_=Uf43ed>^=c?6+T8p-wq-kr&=^|H61ijQM4Of+9 zr|qYdUn2chrymOYbc%`Pt+yV`_f0OFjbbO2w4r`r+niDg%EP%!V4c$3q*|4$iL-ZY zo;T$K{DiG}*E1-plL|V$TLF!*4B$@4frr3*sFnFSe#WXvj~ODG8Kw1ZQ3dB(sL=|C zJMmIKNE=qqJSZZ(&nIWw{%y5-frt=td6rAgS2)SohB%?KWG)}(%*<9TabMkx*Tk~M z&wR6)YE@iG63FoklEe$jbmTk!vND~8^MSdQx3M8koS>pC{UqUnngvThm;P1H{5w2T z(Ef=b`?^jjF}=I!ETj2dM=|{_b0Js=k?1d2B>>wRvm78Sb=1D5f~;*E<3}l_%QHu4T5dsaZs(KeVF@=KRL7kC8(W4`gGWcmM1# zBlP6(jtI&h1BxJl|KdkI<%Vie?DBsXtnp1rJY)^nx-RbTNAZyf{Grzlym16rFvs!9Tb}!dy; zqqvLp((dgIF_EMlrX1KPiid7*jjheZ7HqCV4*V&(s8|CLt&cZ<);kr78gxkJbuJ)Mej~=dsWp3|R-s|%X*%@^K{q8Bx3~Nu+l#MI zQ1(rodj%0_arF#R)Tnm#xmnj!DvM2%ZEpp8UF$*Xjut|VihCGr7oA19SFOwC&>T_I z2_@2#In6mtv)*(4Q05(u<-KlwIPM2geXrp0B=W2@BzhF~P?|mCPVN4(AC$8Hu;-@0 zJ-|EjCOpm)_OE7w5KUPxPwI)W*zaW1#QD;Oj!mW?OqUZ*4IHivDMO4C$e!A3*EF$R zg|(m4MY}ijY&ssx8G&1pj;~m^xAWL8H8h(_^bi{bnV&Ejl!&}=P9-@=^ZwSmob>6T z&VeDMui13TeWxCa$O&%hqck|WX&yQ_1Dr2 zc3|4++-wN_C0n1QO>rQV0>9?9BTrFaaT~LO{nK$AV8W(e?v?|8bhQKTJ=F!%o%aqD z>i+G%(?^4d`w%tK6-dPGS5IMb5YmRvWhU#%ev2QQNtTjAmo+Oe=)cc7OT0&$^&xDp zus4Ued7zVtqT9}f0}HSXk!MQ^v<05+hli%tt~C!8b)96iiH(`LzEv+g{|zlKq{M%H z7Q2FGNi=%;fqMt6{h>v81eYVNfSlWduMp-}c6fw4#$i9U@P#_GmlR1+y*94zz1R*) z29vbxerZKJHEy-u4$Iw=G4!xfLrp@O;U}$VR_29iY2L1K+#nD(q6OCDSxDSP*f*28 zz~cRAqXt|2zMys;MHY^g4!;X`0-2W&YDc8I+rCf|=ToGjU9^&&=YL)yvb-wQWRBK( zsx1$FHP~pdh!T@l!$_R;_%WQeiP_6wLeY|yW<#Y{uY?RfaO<6DRbK8^%LYj_23o}+ zw`5at`%>Jk8q^EU`v8#_Q|jMIa1)M*fV#-5ipe~ScMK~$%D}$p*p=*C`S~rydCxoi z|5YmW1B(5i%>NwuRmt9~k{R5smZ`!XtWU!qM{xN;Dl-iuDTG=+~3C-R-S`GHRa408>B_?J`!>L=!YBMTP)A@9ZRYnz|Nn5k_ z_8>V^N1nYh2fq>TfyM0jUynvIz%GIhrFsqw;U<5O2SMl;n33llW?Qj_!6Y@Ya)qlZ zhToA~cQOve&3;#F5oBsh(zFkI!`t1>j#wiKrU->}K_T$K#EM2unFk&=G_w8ruL3sGF?Qppb=I?gPCc#G?sMfcd1 zzJFnG<-2!7R_ru*-40#bf^YJ4pOEMdYA_ zh7;zYtylKh-1n){gz7KY_Iy9y;pC<+E8#X{_m&p#wDEi!#QoT+^=EzFcekuNr}&g5 zCasCvzOi)ye+IgO3Rs)MO<|?8@8lPt%O(Qd1D0XajeX0X+_7_ zl3G~_X`Gf*o}lW{8tVu9UFRw=aK%ZFiz_8BhMk4?5CJXSpBw5qv>XozEHDIu@!~)= zY*}QRChqTcY_mVDFKABoW$+Uly9BL&g&{5qmcS~?64hLj`}(Nr^oEQ`4t3ubYeSsT zVm%(}8)~!;VH+Q{UpVaJvXciK@p>L|UXk=^xabxvLd@+I;1l@gRRqaw?I%pTs|Sow zV=9MEA14z(E7dZdX_iWJf3NSLjyqa9+?XQEb*HtTao%{{g$3tLyQG)vwY}zLipc1z zfxPZYRC(7kAD)?Ke&B6V42kvIa#7G#P^${_k@G=sC^q-B6(h&SUDq2G_Iy|;SO7cW zxG3y-WqG7iNzljmnBOehm40Xr?ZCroj7Ck%br{n=;5(t>aoIqf3W6mS8_MIU z($9K(KOnmn)@iGC0AVL`xtVF4pIlW7J}HrV6^;Xs+y2V;+vZqCo$&7#>^zUUpJMqx z1V8LNJy{R$YR|%(MJO*iVRH~Kj(dlZ)BdD6C7OKCK3L5%wIv67gRRtB{g7GX6}IWL zW;4|z>M0#etDLyVRzh{Kw3{NasBx)KsxZpZDh+C_>Pl=@t$ppP~HSV^W%QZZyrxNSjERg+;3A=)f$QN%*U$Y;~cjK^0nZPd@=&+6N3>IXe3{;&G!c< zBAxN6qc6L$L=_W&`#Y#R z`z@ax;=Bgqf~&kD11YCh0e}uBOk1;sJ-;$;Iaa(h=2Zr|-O@?VWwnQa%cnTG?!&)>n9uFHf~U@4 zFW5O>ddtr1z@&zKubYwlFr1gBb63dvBZC-(0p@vxi7k3_p#yU%d#&lp4Rt=OZ49=q zWp+HpVEQu`mN?kc#RX*}aV6U)MdBboo(%X1MWK{Co_7#~x;Y9d@Mb<$+9O~&I?w|^ z!t0Snb-p`=ykE(f(n(rfjONYKy5okUrnJ%wtu~9?%pca~+om~h&a_}kcN5voYD+e> zRn|{^Dp(E=g1H~hc9zYUTLVsu&!Xjv$VHzjXy1)YH2kVD%U- z_<_``@WI>15Wzw?MSmgo_WJ1IBds=1vw1@C)pW*=rEz0fqe&{L%RvPjTlGeZgq1@v zKdCmS4Orbk)<+(bwifkCJ?m{p0v}m(-lVi_Om$0sQ<2xwtN!07so0nKe)Pj~g4|D2 zAxHL8sec9Xgu3G$-m0@$lsC91)yhj;60~Kbc^C!wVDD(;GF7LuX2+rCiBy_QDZaKO zdw!Fv#zafnWq2}aoAQi|MkS~#HZ@nTw=iMy$yjI5|>Bl`ZFol6JX2iD8KLgY!d zO~STb^G>@jciEaKb?45*2i|P}6xC*?mrY_$LPsR&JcNYF^9f zm{27Mzt%Q&1C?VJ?&0}c2c{EVDF0hGdwtO=g0Xt##bkf^(cic5iN(e z_L&>AJYV$}>dH}Yrle_edn*WfyfHC#S6;N7-860)m^uvCV`zL^({$OabVY+O%FPbf zN{HRu=t<%INWdSRxD@8e!&0c6Y6WY6-aM8I{(d6w{(vTD-uyuj#O24s^)5jQU_g!H zL2lK(D$71BcgxKb3fNb}q{16YX~)tqczdTe(!pfdFn9fVC)%w_UVmX_UUi~Q+6twu zXtSGDOVD<>4t?A+S3AiFGqtx`RpwML`z*gx=#iTt&}X#dQ=nx>K{X-&93ntG0u@UD zaqpu1!zt%7uu2XuS31YXIpSYUI`;-P-{oX3_7{3ZRotyHB}gLHR^|fV+tt>VRq+W# z8Rmn%DNr+CQjLY#<%E5?%7``$E!7rrYnp9(HKMi_nr_*2==)*QS23FNZ-C7OzMXy< zstpwfceD){bcza@8ubh6-Qg`!o`Jd5NN6}c3=xCjj8He4pCDubp0z{>FGFkt_Mk)3 z$A+_%7pfxpiUjt16b1|JW_ao)R+3ZnM{(`FI$+x}#6Ws3LQmBEj+MK7bCm4w9@Pgp zzu}^r`{q3L%8TXwGK^_sOXi`yuIEcWpoCYp?S)2a_>8jpF+>+I)a$=P#v4Z&AD1+0>fJw0*J*_MMgiWB*d$FK;z zd`|rPzD$8wZ~yp&e<)Nh0MEoh(BBfSSPH`sY73fz%JxBmH=2I4wKE!YXP>a*bk&~f zwF*r#gwgbk1I#$mOOn?d?Blf`5o^JxxDDBGHdA*2`(C-ZJ@Z9gwf-n0{D9w%2lUD? zxp#gM@&0E!njvJ85p(@GjL5$H*l)1y9Nv+_`QzMoePzf@>P@Q=3Wp7kE&J_7zg#w$ z*0gh|5$icsTQ5wKWxFu2W;d)<$*}2I0)g!kn%f`jkTehJs56qIgrJh?a9S1T`%Wzk z#mj+vEAalrAbFaYmL(W=r6w?`RYs=H4@81M{uq&0AQ3LFo`NHbJdEd_SYIz^97Q#) zp*Dw-9DU#)n(Aa%sg;|P@nK)*W+m1x)k|rw1_2c;Zfy20GzXY*1EYg|=HRQ2(qEF( zE-qQ^&3uP5R@;=uyD4 zvOQ7x%JByCQgTT72jPAA`Q`H~-;AQuE!!%ZLZrH~c{w4l7x0=Ix1>=-z#;4{3Y@&- z3JN#Qlz-=Q$!DJeCvBggn<3%z?yb%r`jdNo_yqwM)ymh+hjQunK;a-Jzq{!IHBfz6 zp_S7Jt+^;3fRX_aFAmMP(X6-n?Tdv(r7d%*fP=^Lgo;4ncmym zAUq_YVOd+65cqM=H`2yH9rcD2t&t>IC(@@>&{_KHiwwHTzh9L8b5^_Nv|Pg!RDOm; zGz0IR5JF|PpPE+rJUu`m`kNQqzA~GE4j> zIHkAi8opXi0`$vynm+EGZTb{W}WJysMi}-f7`(ePA>f0}Av1zc-o?LV7Sa)w@Se^H3%Gm9%6Rpqy0 z=+Ha6?a2+UW{vh@0(Fj)MNKBQ>MRs@K_yBrtt3s1v&0IWMu<$C6p$|aAG zpS5?-5{lqn@z@7^b6%&dN#xUod*{&n9S->eF^S^*IcJ~;dE)4mkd*qK6SceKpyw-| zz8xy`GV~AKY)cO`KTUf!Utf2Z8@o2|O`6P*4Bh_NP1MkUQ6Q5@ozfD~NfjIOmr_6N zl-u&5wYpl5b*1<&ao8vB%1J{#FtlLGk>e?_BMJgXNiJ)C;0=}IepqiMe*=p!`0WxX z95l%Ix$i<}FyUJ4ab;f%SUUNaJa03eZq9lePr9*BenDcl~);__fZM3awNiESPKIJ z<^@YNd3EHb`>;l~2pk_xX?+@%aTzu;rXF7BB}~+YjcwNOqwXGtsVg+o^|rmyUfK92 z?oF4qs^KmNSUAJT$#TXiSUafQx9l&+^S)hL%wYVAP30J`y;=@kOuhHNknMVu$XD5~ zIDoNj=a&l8;7Y7MAx!xg5`4hw-)43Hpb)a5KFq`4%>PD|y1qv)P+w7OHtVz|p;a^Y zcp{PBU@eDThWGvM$c0`QvNB%IX9J1ICZy*I@v>XZmX0&`M)FwewXoK#?P~*kLDmV4 z)2sCj(-J%O;GqyGEcyEsE57?FxA-5ZV&o=h{si$?M0$q%BkUiDFK$(Qw>K+sE?@SH zbp##cE|R>@<*qVLv1((#s*06(ByE`fp4<3yS+{!oA!JgTLwqmEn=U;Tq_$Uw?PBA4 z3d5J08f35d`o80>J#t5MlbW)o$NID<=#6mtP{uq%$Z2xN9dUtv$ftTKXTBrMGg^+% zbIiQbNISD>ZGDLwj&Wv4)l}pRzy}T|RxzDzS2JuC_sg5C&7|RB z$MpEBGohT!oyc2XaN@Gtr)AbkXLqB|7x8fu&??A!K?_ReonuyyFzLXXROR9++~F@Y zk>f-Dx`CQw^X5wKQohRAb*CeOn`wJiWwe^yl$spd6^sE74fB<4qY*T1XVyH(hi-># zHp|RrAVhq(!M6>?&gg8sjuTiW-B#QK6u7H6M#_rUQSRdN5@@-9xM9+ElU<7afLeAbbEdInT2qFE91Y=nP#MsHRVjmVbP19zFk6{*$bX0|0ZY8`GK!~k57O2cIG+xcJ+@^O)pz%K^5Z$$H!v?~DeeKh_tW3GyI+k_sCed^K9Qm*t?irm@M$3w5@ z@Qt<|v@R7C7e)PV=yVs08({U;`4B%VlD{uLj%^Od;_ITg>0B4~xKaFP_dSYa!9S;1 z5AwMomr!iwLQBS8>^esk^d7i#@E7_?Z6xy_&#*Xq`ET3X_n{XwzIWeTslIZVqZHTX z>elz$6|Eo74(*^&_J;a$mtlwKV4VKFQvh-S!X=-IW<-h{Li1z(=a z?%UxPhauBnNNmS)N(T=U*W`2w&1KzasBf!ASY{{dE9F`CAG5 zC5FGu=oh5?eo3n8{({JX8lY{j%CFdrj9;M&OMXN<-#hS?j{}8QG!udwzhF~)5krD4 z1iv^Ds##Q5bme?g!O@t3_e$xnt{22&p#17MqG|sU;cijnhyOc{!G3VLhhX4@xjflwMFNpvC3LvtDem4Z-VO4_d9f!&N zzKXJ3?1sC_>VY)4j9@?uJ;7AHj(c6iPp*Lq@q^sFTmqpnNX5G!a%$dku9y%MgS0wS z{oLnR@>C-9!Rh`ExnSJySAl^6_zV8+9@NczwC}|wLQd?4xso!I%I1wBZX2p0uB3US z+k4g^@D5G3cU(M&n0$4x8I{Gxs!^A_eS^uk4RE=7*v8~+x5XmB#=0Ra zLv#Bl^mrc3xb6S{VNsxi^WgwCB)Lx`1+LJK6hUrMzZ%2l&aIs$H@h9vA1FfWK&;!- zRZDYV)x^N5mu&?K`Wv1?C*R%wf<3*(63aJK)&Cw30?E&YX`cUgaM)r zV$$zx%yhKbZd;Jw1*;_MtvZ|Ks#ap!oo0FGj(VIaPb`({M3LRsGmk8<`4}4IULf)Q zVJ}z!o%5XdCp)UR{1w8uGBMf*d388CoJZb#hm9e;_*^dMNW8woU2*W?F!t|X{6e1d zopQ18YDD|A(avmCa`{oUdcb1EZQ(~rz*l&~U?ghVovmYLz2o@jZ>v!_)sXwrUc=qF{*9BybsLRuw zb6&-~Hq++8ghZ6j5%X+!>Sprp?#s=1SVi!9hv3Aq!^OqTSwzjzEK| z+4{5?S8S)*9_o;m=roA-YQ)mr`yq3r$RD5u9HmHwQR4Zc^LzFV3I~2_px*#RsJXe2 z`!27nbVVL?g{tL3BW7O|EHm_G2Q}%l%g!JZceSh%YzB#jaeP3}w~E6!;evn}hw{*~ z0t@dlcHP;M%Y<{*4NQw>+X-Iv9!IAiAqVC!e}%pn-?ao$?Du}P23d9RbU=O)XhNok*w<*A^g zD=BI)P%J_SY_=H4Vs)Gi7E1MMbi!SYdS4l!frMXMwWz7TspWvdND|om;3qo{cmhaY z+_r1QhI#A^EwUbf`gg*BPbVzgB;bD>?W=tJ+ZOvdqmRgRe)5%rdO;;O_GU0t^e~&b zZfmz&*T69v&)VUBH^_Etwo=Dnq^!ttoIV!!MpZk+3@nkZbvAx~vK!QULrkZgLDDTB zCiupJw3^jf-tRsBCzUQdEKm4eesB(+d@JQS2lv3^0=|F>6d4uB6@(EcO@`LZ$r z$aDM&KS+O7aPbN05x67a(L?AHL+asqSb>=@0)58T0*E|r1`DcZIleXs^ESR$b(1yYpw;l+s;<_lU{&&?eau(K zaQrSfPNt9$ zZq#)(e{=LqneKCH~VojR8Tc!2?GHQ(U08F zUtYRZ&?0*s8S|k!uzgi(592Y;J8th3>*j|S!uLNd>tgcoqw&LviqdhAFABk;%uCDe z1|a^RfAG0*+p;I-ynd!xg z%67$w?4Y~ln{Br}!(@kMF?wnU&a9L6wL{6bOj@;C4}G?RSl@72Tzo)p(}C3$R~Rf} zoi`MHB3n+SX~ix6MB=}j^;1;UM{POK!g0@a0Q30sV|s>|)A#%gLQaTnp6lS@m`VC$ zak5qzo1I0isn+fr+=gCy|M!oE#saB+1Jdv)7+w z+t8Wo1kMGfDXx2C%-ag>c`b`vJg#>9=m2Xv`M$y_1Mp2td!nVSM_qQ3Vgx_nAnehL zD{=&d5WJl<63u?&O9AcS5@D@dh3>CVGs)0uhSW~kHckJ3OI8a zOZFY+G8HtF21y6TnwVZedW@HzA4 z9oRRZb1eV7oCgJ7MVEpmcj#xf->5?{7~|QE=V2JrSntANu+?iEuk}rrn{*9l3*nNQ zfGJ~P$xcITI;UuuzmsvP)^_{csIoGZGB%Qi)%h!g-xtyEw3k2WE7rYYRB_&qe&ZoM zksSXyz2u6jt1vVLLi2Y^X;4eY#|P{TF(>}{8MxC(7(JKikTvkR86K55GTgNU&8m|o zA&8AEadQ-q=5##I)pE|o)VIf${jVb3|q8&D=4xG=HQjV$t& zuD(xm@w6f}_if>upO>sc3rh$sEC_S?Yd1YJU$;d@=xodb(b zeqjZm8Xo&l%&b0Tegjp9$ss}dHyGMuh516S+hVPpTG!QEd06w665gbIyEfA3vN&v( zX33Bk^nwA?6zp0@3Crz<(@|t^DaL|3Y}cUT40>myeZOB38*qfh+LR~CRx;r?<(f|{ zm7dEn+v=;v{wJpf1~8m#;Ya$!R&|uQzNb%+-~Txd3wQl{DvC|#SG=`d#j#$SpE!yI?vnG6w6Gx?CN_r07 zEHC>wmV7$c4_N=MEqB%C^|t#CpmH4e%3B>Cdd+3138kcM(_=lo(dhX>&u>M`zQaO; z+ZGSJR;EZ*t#43eJjC5esLd$PO}Tj6YRvGl-KwU|R>|)0hlMxoG{UeS`~xofi)Ct; z$?|tj-nn<}ePqE=%qRRE;R44XoIe+im5}YRb5ZE`#aTHfOqL&cqmtorur|$?akt(k zc6^JCA@$r{2~Y{TZ|H7^Nt%AFic5K-9$FQ&Ag_Mw+vh>|B~EwA&+ z-vyDtKb%3Ys1dXt!t{OV?%HL9lTX7R7l=7Kp5f`1#c7G_tw>Vp5wNZ%Nm73t zD!kI$4P#-_hx!>g*5_*|9+Z@Y*=!_3oo;orUa1~t6>*ns-PnW~(p$SEO*TJXT<43x@n3w%6HvtVGez7a!Sw>ocF4cR=0=ag_82Q`B9zY??!XI!s zP|xf015eg%CmHY!qB%;38OAW&BD2RGSa(2omza8>Hk?{Lu%@9aNv6y3roQw#or%*J zwl+O_=zSwX@WXTUy0EBJoyI=q34VY6gEM{TCqq4tV|?BfDWg` z+6OV9WBV}F_ub<1y?){!Kj99Q&*PQeJ|j1cvIUi|I%k2(XtLq+b2A=x=1q%=ORx$^ zZ^nw?PLxqeb~&v}d;W-`Wf(%g2_$XO3lx8{95okKsS^6IR-`@e`{l09HpvSGu0lsQm?>RWz#mXH$LG6rh4A^|o;d-dus^OiR6GPNCn|dIRls;bQ=+z; z$i}RdZa1w`wQFqG4I?ifw)~L!OZO z65G75%AlJdg6-}iI8#Mi_YWC}Z08Tn$6KI4x!VhnDM2>rb9vaW*XQfaN~Q&K&CI3* zY>grV%hL3%Wen3*#oqV&hT7}Qq|uZbuWRW%h59JoZ^ORWdRl`*M|$nF?W;8@vwBYHy|W&>tK;L{D;??^k(XbgEh9>C<;aKEl@$%dl% zq^;M(rMHytN}3ZD#@p}Y^ka~Owm_+|P zXS)>_JM_?)Ej(l@!#@}sTN)m4cKdAvi6gLZ#Zi->xwp~gHT z$W*Itt6IB07-#``SW{&wnA+Z5HT;C_P|FY!qz4lu>fP`u1j0Xd>heyc3s{7i+Z%zw zkD$K5P}1LSH#=^N%hRTQ=y=`KR$)*OCq(4RX>FQQn-E5va_FXt&Xat@!zl|yOf#-T z)mc;5))TxV3?*`6^p+0344T-g-M)J;F9C=r0`B?ZMb+^v6Egm~dUrY>8o=?XY`Z|r zF?fcbPxE+%JdU^4nYqPsyE z@AV?gl&dB?&!%;l@;6twGQmUj>~i7sI@Dd{Jz+026-?a+cUM{?!Tc#kPVf*uPLJOT z5ll-i&CFm<9>sz*UzXLb?Ht%p@XN6m3Gd-bJz1=?5rut?n zRm%sX2TdHBGZ~EpCvnHySu?mB%X+VN>_uSuG*0cFO$pNNnd%?#9|nSB?QOXjCW-lSA?u;2bFN<)@v@CR!XzIzvGQ^ zx8hV8X2Sb*-sn%JY`|6@a#3L=eh>q^UGoYpz^EZRxBDO1Ac$$ukGDi$bk+OIERhHG zCyaRw_ZrcsdjX@qHby#ob`{&qWK$kWjrM*pZ`lVXiY2k@62Z8QDYJ&OO2b`eEX?G} zVsDmUUx+(mgHT~$qwm;Y8-yFQnhL*N5$!g)P8#0hWBUZ*PXr+hxkE?y9Xwyg0sc_6 ze`4*O$ae&LjpgH|w=6zbez)3{w|a@_SDDCM_s78^P&B1|*rsDTBn7TMZCJQ8qRMLz zUrNM5XOomXn8Z5Smdg5ykpfTiCy}3Ub!nih0c{yCq``gTM`68Gju+@Uk|X}aeE5GZ_k0<;BCFC-SBwYD`n z4h>GpIKL7wC2F?{*bZTO_HJmj77R8v_kDeVVY6llqluB-9@fiZG_LgolH4x_d(|sN z2{VcgP#j3LYb)idH~h*IzDWA}v_*z#0P~qo^Dv=>8C4Zje*K@nP_*+Vy!+&|ft)_S z`MFg4UsQntjSE#i&?Eneq=H%9+S74EP}uxg#RrsbUSQ@T@#;CaBB)aOxhsP0cD#3yeSn*tzHy@T;))H$RPq6^FLPWMM@?|YVik$#EuW^1as+(mKAGBWDWCx`QTgC$7@BXk-k4ib};`y`?9C1Eey zL!-?!g2}XHa#lOB#9>{VB*tCW?oOcmxhT0=eDS&RD3I{3(l|gbc4RzoD?t{1%I8OT zIXRAxbEE2&tBo0DdpjH$8*aX~JiWZ^`0NOJ`Zw%K-piGCdyDVF9Y3GVT4TW+TI#f{ zl<~b1EPBR{OBQ;=jt8SnN!BWR=<+0c!#!X56D#%!ZJtg7faNC8j;GwMmqj+^hvgI} za0rn{$hjBwO1D-d2eyKY2F+xSw`R^{Y^QyG>W?gJ$E)juoI3b^USgAq#EtF2Fyhnl z#u@IH3EvqEQZK+qWNI9Id}H>?UZ>jbtT`B2_V>RZ`QWZ@{U*g9Xux}`f__KdrJ>TWp8`M&MVf6@8%#C2BfKQ#C6Q1I6kFU2Hg?d1Ju|#(HMFYXXdl9Ia#hpK?|B7URa$^817}f_9{Mn2bJgS%e$G!bG*60 zV6m?U5cfLsyU|FdGDxCIV$BZ{dze%)WZH5{cZ@%ZI_zP3rV2 zE%hWG+iIj7x0c;*rnI_7Mc`ZA(PGF`a(GvqUewb+hB!~MPz4lJ;O31#chHB-_`>^o zvrq!icMY{b;`Nocf{xvu&Qy$gHW zd%q&D81NN|`n@m;GQ~eN2hfc08^8q>zFh=S`nuQDq7JJ}3wlc}6%t*=#-)S|4N5+M zzpj@=oR}#}Wl*KE7Pll-7Q?Y<+;Ab^UYEyhZnTn?Gi7Nzz0Gu3S))z1b*aDjrx7Bj z!C$}cZD*>S8s=f7zZibQaxvWPT+#w>fZ^adfsvNFu|57F0z;AuW^vskD@uTWOx6Ni zPR!zAo=1OW<*gQ0WoIYiG#V2+HQAAJh3?JeRj-{Ybu${)v$<{hdZ(3DSv72!=6K*m zN}XL$Uev5k8ZEs?C0lj@y@!&=@fgSq+$d^ZKRe+X>YHwE$SF?;2m!dTbOJH3!D(l8pk;;}+S#Kv9(W6v-(QTxS zs!JZCPJg3nlP$RtiG?5y_B^Q#w(LsLFRb>DF8=z@YgfE(()dBBKN9K3rjLR;MFYrN z0=zP^`|WwWSh1s$GlSM@C7L!f2`1zyy>(?$UJ87=s8^@eWRP&R6(dxpc3m0lYW2#L zp6k=a2nyP=&U#Nv*>Gf)clCPzA1sFY*`{)o<7k7p2m=W7csJT^ah8#&4B%9gR1AB# z5z;sFm9KH1FG@XMKvR~_w>tI)mn!~S@AJtAXkYa|pH3?DK70E> zR!`fN{{saHpXYx=SnqXr2Oa&l3JNe3U+-|PS&qzL%g1hcGS3%4tC23Q@fS5vBZqT zT2hOFaJ2*ct@C`}Ec;hA$4)-{1-3@R{L$lu3zyssRHkQIQM>A-lTI)Da==6GQcu2;C_;H&E z%=`3$)AFJ#m-NT`0~I>EfXxwD!1?Kf{VN)G7y?<)CZzmj1mtql;$j2}Hyto`3^$_P?ZsNOqRH)yNcl4HDF4va&?Kn5gSCOkzDAz`+w1ft zK{OzH<3`gR-*rXlBWpq9onQQtI`RfDF!BR+FkzA>EWLwSMfq~VV{P!0aM^F^qsEC4KVIgS0DVd{iK57{B>|MR_?`ZuN z(Bs#C`)|Mg9V7}}Jp7lxS5P(1O}^z-ob)w!6)LcMb^jdxgs62Tc zuJIS6#H^9sI>+KYMIA)TAup|UV$-h@EY&KtYN0eF)?>e-CcRj7YnyQ2gvnY9Lx6qo z+_Xwn(#{-KMC>Q=K-KY5t0G*r`#w=_ccS6RC#~0hmhntpVYgy(iQO^dSL%V z?gMcMnQj{`bTbd?-{52&wQ#{!PdVY6xctEo(DD4atCusxHRKClTwevZ8R7DIawPU; zY$&K^YMNm(u{+yEwxGg8%9pjEKdy3mVwE)|txcTzTF-RYjaV5~yCUAkMZLV*Nn<}y z`Y@4!in)bd$HhtyGsR07^a*?XC@6eFOQ_WV<&aB~GFUO7^U&je`8()$B-XDO;*}`* z2Z(u_&@Ghq4}#`8{KFxJ017Aw|3`DtAfoevWL8yM4)w#_BP%u0P1h+q?#Coj>V-!)9Zr9*rYhyEx>TUhmfTROCtI!tKE#qQam$uSuzOBW5sL{?8 zPYk(*G#d@tTxmCraIDD^&A{)fji%~c!un6R>JvfmPcte0X>#BJoBkm171zE?;CrBf z!uKviF%BGx7bH$3sl|{=YrP=n^`YD`jJ;7~*m|82V1?E$>{r*NY`p0Xv`q&ZQdTCj zz}Y0*QWd%xgrbByoXJchla9FwRwu~WM6E90lJ!V%Jkpa#kKqTxpw8xO2)v6i6mwD0 zSoPw;R{`S%T1wM(t1eXtm@f_f{mgC8ht$kuw6@e(xm0y05kt-@ZwRSWqZ-8CRqJIe zD^&-2&8|7H?xj9i%y-_wH?`&B=9=YL7lbLa(@K|{#oGK}T2 z#N&UwiF}&lbBBK5T^IWKlcNt#@$u}#qdb)VN+hbieYYFe7O-js(+MA&G6#26nx%6A z=a(wmY;(BYinzvXT#|?Td4-WX19v_-7!c(Xd+H8qd+|^jd46dSW=_cp#npnigz~Ry zDDIr~Z?1N}#Q>erB2Jq>y@}->U`sfR{>n|5;ytG;@A~Bb^7mtl?IU4)iqia`2wH}9 zG|k?pNDeKhNd5?NQLjf1$|CK@wB|Woc`!M2hl0@@ECzIGbLg3p*&82b>`1a~Yh4R# z`yEcM$3%0S$?J&SbrL0^>7YxA4cVO~j3X}?Iitd9__j2zzO%Uchg143obm9){mb8h zt)G`_U%A3RXmI5jzfXgsYn@Z!_!v&c>$t(d(FVh+MD?(laG2Cb=6H zb34a~XXSEpHpS)U1oMZ{TH)ueR^|HRI`j{l{ZU5}s?FJgjjR=sI> zh@0=qs3*Mq5{IYA?hgbaKkwrNcMdJbUjFE<61nX%ly}0G7`J??&TmS}R__d!z>0O# zs+LNF=4#IFs+9?@tku<=j|W6~mCh5<9+lOZ(;zqWVu23@x?St8V2K9JH`WPt5hl;Y z`xDe5G4O%|9~8=z{{}|chnf*UA-usKkihWq7xu8&hc4)(=7{l91SE-erRjE{Vt;h- z%Dc`y83r(=$t#h=T1M$!O>4zvmg-C$X+U%xw$k8hTYcHZ2c6AouQfHLWpnc6F48XX zgeSOy%DCsn6bA_7e?=k;Fpa)K!*oD}{@^$i{{Iwh-{B%0e!uqC#6=UY2$IWZ_&%An z78Ev8maINlwdWjBsbSo1Ep4Y+yILu?_pK?r*p2t>px-F zB%wm^!Vp`%DCmBI(AU_!TO*8;b)QN-gf28HH^*BD0m;|W&Y8woMcYoL#xR@KmL+ao z>hBa+7>(6=G$DeLJ)*N(qansELEoujO=_EFrPNpl{btZra3(2nl`*J&zFaQ1#O`(> zOQkmzxH)rR{>2G@14u8hoTX|(bW`cM$#ojC*J zI8r=fs(0@m;7h0Otu1@?jgdhs+veFqeV}mBCvuDT?vPYCybu}vQ0!emo{(7F0AeKw z+6yTtSEQ*n97$X@oH*HZKMv|zT9U)GPOO~R?ab=sRlVNM=04q8k0vaYW@D?;*)$B= zPvf<}W8BbK4w)%iUuCw^NVPTAzr@2%MHGM@G_Ls(R-i9Z`HECnN?F+bKLp#KsM6#H zYL1+nD{rY*VXf^4+`%HoRZ_$Ku`nDo!RN!rs0@Ixs^qQm&=Uw#feZyLm zWz~=JsQQ1PTHxcx^}l}SMIv7sFe41ZK_?rfa*xvOuiuiVq?bbsuR+%LoUT7@i@ee)QY>kmZxGB6Afa#il40b}Ha1PBA? z^gT*KTn&P``3A)tb*NpD1Jj56eNG>Qq4U>pR46M@PINo-E#$FX7_#A-uuJOro3An` zxllajKyqrI!R1S-BJWa+35dVQ;lF;vq>3c{5)}o>>;hfqYMfpEEg+wDHX8o=9iZK{ zR}2DoyQ)X}HaP^zddJx^B~lsG6WA`$t!6)&YNvD!_V_>$@v3ekZjon)3ycl1uJk>gF% zFKuC`_-L?ah@i2?czl6Z`c>+n3>%9sZ%tWj-gU-p_pa8tm^P0KOpd<}@fH;5paoZ zFTK5^ZoSD0oFx1Y99~KMcX4>T*C+?@0)tYk*0BSH6N8eu%H zHHQPML^CqUtVbCw_xc!?*nYd!*W02QEyGf^F`w0mhtBX5{2YrL?_dUe`a&6fgc$_y zgp<4)6NLs|VWY*mMFz>)+RD)Y#@MMS75Vw+kLN~5B?pVF$g>Fqd#wWRQVI$oSuob~ zqyN$)s^=MCX2Rrny!!sW%}=br_=rM%JtzHbXU4m2%jk4Of!KGL7>vZ;FFzhH0?v zWrWDno2%xfCtPtc5$C?tF^2ruJu`oQ*=1p%JN2;r0KL_omINYD=5w z=lw5M?E9^(xy26UM!gkBK*bgn1o4e~Zh(M*^ofLU0bOJSbDoHm=VSbzh;X3Z%UD^v>{#9c@I#4`4`M~4-n zQaEm9Z$Vkhepd)jp3+;~`%L-Fz|U6;v3uq=h?VhZTUVXo#Gd;$U1geM!X_r$F4NWL z@f;_mPBRfE>jmCBY;*Wv*Jw!fyfzdRo5zo;)N0PxlECy16-@Ml&d|g2{f~A=sd~Re zs2<23)G*$YITR~@vy!w}4lFw#zEBFdw^(hfyW_#0i`X>YwB~tx6~SayCsg1d z3^3QI$Q0WW2zTF_O+-hI07l5+$Cf!C&Bg9atL(d*PII*Fc;0BAZ#o_Bs#N~<3qFHr zO(@$#H`Z+1gdRo!A$qjATE5b~zwXa0LC}M4f>MxW z^L^{q82gkX(IE~>8(L2i#dR-h#&r~W+ zdD3ns@1}J!hRIm|+K16%%VTqr2O~`yV4i``^lE!6dbnou>wSMPO$jPoQj)Y9%02+5 zc)2w7GJ2`oN5a|hjc$5>jL((jSFtLNm_PnJ7j#3UGSocxhp5=;(_(MOrUQuXR_K{~ zuUwm-H*au{$q=I=erL}HCk_XY!b23EXyidU;j`nOT3c)77z_Pa(8qVQ#XkE$pd-I0MwJFt4+zfTfB87ys z85#<6iFU`fdP)yLd>mn*BV}10!#~%XLc7kx+P6Cmo!>0UoBqz1Ros_8(L!=Qw~oF9 z;k;h+tCWIn3%TXV+u2|_Cpdi;OXREzV>y=dl8B zKfk2!-dYbt@VpR3!lk72WDi%fJ~PJTfz|Kr=#kYM&oN)5d^?)==SG_8sdj)=v~V1muElPk(VG1T#vndqu54}pt@!>;i~2q%q{mAvfJL>OQojyb~1iR4Ba+0 zL02@&-h@UHfet;b?rR`|#n+Kj0gsHbgYZb-W#aniZ;8YW*|TKaT!K>*IQm3BuuWWv z8k4r5*Az1qH+4epE=@t29NXq};PzHFacneng{)c)3ko=rDqEi@!MH*?HHzkD^jL^T zRmn=FHosC^++Le3CQsz!H8;<+?T;e;d5>d|%s#8uH#l}8Sn}%h=A$6>giBI1@U@1> zOvas!WHw`w-qlB0LsOP{JzRS!eROTL2{maXhHbai+9oBS5r)D{B9G}2^i&_>#X%kt zV|kK|!)?VNMJ)T5gi+7A<_oJ(PYX)2zIMDkoZ$%h@7rYj9kIQfZ?7-Hm1MCD*S8?9 zC~5H6B*DoU5r<4$@f*Rx4VPISk$Gj;us273w&4|lt@nf)#bM%bODl3J3957$==QW% zB^GnczO#dHraEOybJ|@i`}Z=EgS}x#7;MGV*^wI`MXAeP-nU zxjDrP^2j$Cx&b12RfbMDCZ&o&h$cnV@w3Aa%01_6zLwgP3BBWUI%-E&Kp1(B9oBRu zcPs5Q^LtuTUoVc7qrxQ9LDZWsj*dxji(x~y`T|i?1TH$v;&8MyMcf_SluA7CAfFje zwzq|9#>{_bGxNL*v#@bRo#_JVQNLSF$K=&PgiA z=D8O!wB~OH-B~Z`Q`1QZ1s`TFBc|JdIBq)i(x#{n%Ow=*0=iGk zpq-XB2hrNhXFJtfW`UFGQzq*=>u5voafjJ+$4#HgMFIw%DlscIn-m>O@?l!ZD(!ke zSB0Q%w_4bK+HciqzN52~QE)YbuCCF4A~W(^ngDjZ2?Mtf##AAJe|(D;-#SlcMX^j2 z%@wo|f-O6v&=fz-mAz`iFRsE^Ct%8{l2N_JwRAC7R#%OD?$08#2= zjr*w6&*GmC0oY(>AUFXZaDJ6FAmvF=u>I+B*FNdNtJoIH@RCqMY)aln56`Wy%kfDD zKL7e}%a6hnr=Z5GO5%nhto+S8IM|Re|JXrN@@!MnI-QhYYvydEj9u0~NPH;HX}gKD zHD!R&jrGtyRET2BxjW35)a*|+Ka-WiKKw>HHoeb}uvxMUEpGBNhTyZ-!j(I1^Z z|DFYMKA_24zPJC7G@atSuPy)@U|G;BH(vR=uqvL_)ffZasYZ5!ZDhrr#Z6CMZt!H; z%XG@Z=S`ijx_Gw+nAw#LCa8n8rL%^x^7k$t2n%+`42J#GIcS4*2Bv4L;)ZyC+PTa8 ztA@NE-z=(xub7WWEtZ-ZS^^wMs%h$N=naY0C6W%f=3nr@fv+f-eUW5@rstFq&U2s} zPOXrzpEo`3ZlySAiPE_3-B z`qoek=84KomIF-p$#}_dJjHf74L9g|s}uJUrSB~#N^>*o;$kI(U+}%}vpfaD8bAx(1JzS9u|JQAkTdJR!Sls8O z4I|rpf3sQ8<0(MIl1#hZY2p5Qmm9rYUJ1^GqW2YTH`xt$OcXbG-&OOxGj+#WWsomN zOZ_0IE>19pFSfVNIzPmd!>IeC0t8XLXO*40gzgB{rEBPAp+d9}FVMdrOsK==u`uPM zy0eg`wNBXUvx%w+O+b$02g5~sspQ?Z2LRi|u(~FFr@e>PwJhB^nLSO{%yBbv_JSwv zU71kpzOWY7b5-K!gl2WFmifP2m?D5Zbs>%jO~>~n=|+9L%(EMkh;=t_U&=^iINMD< zZm75WnKd0Ah-OkzTA^xao1T>~@y3A9fLE=+Xp&~7F746@wH;RVc^~r9tvP2N*%;r< z#!e-zFuGqQw`TfmJAP5RKL{4glQ{wm#XUYDh=4ckJYY;guH;ei5|XNj;g2OOGwj8# zi#skmT8-Igx#~x|k-DG``?0ajJF&ePv>0-sFEweCfytLnwo+wd%9sNuTY{EAo{JvJWv3lF7TC~q}Au|M>4g6^Wq0bUlsdr?9RSoj5_8KAlmzB|D6oUnZ% z9Q64#dZ04#U;m?BqFX)=Uq>GZ2H2PH_pRPoIB&@MnPRz&=XZs+e72H8b+ltAeTu6r zabX)^kxeDCCic`B)$kJ99!+P%NZvAaA?{Ahgm8rBWFA7Z!j`r>`<9&IqmGsCMA0_^ z*>Wiv&M?2<4}7)N?^_b*pIxj=nTWly10Hbx*As^S1?gS;UzzrdE))xfU*5f>T38nI znYum3Wz5*PT~$t6!#HQ=L)>54hhf{A!sw;dVi~7RFYVXUeaB`wS&0WU#m}3AUAU`H zm4Pd=#JV%C#m$O7n9_HTrw6g6`0k7jgxm+Q*wWlXxn}!NHlCg_HX#520p2kC%k>>A zDuA8!*hfr&_Sf>SpC}Gp?hDgNO(u`^l}B^2Gf5A1(U0TiA#EM8#__nWwd=0YZAqX& zB13GAQY~22#pZF+1Y2{#4?3nhiV|t-&lzUbShraIdNh5}p^psLODVuL{SOWVfJU4w z(qjq1kEF3op}i)I2zcUgxePGullKnZe;CRLO#F_u@OD7@L=D#D^a|Dmsj?)$r z%6)Skwhqke{(n^sC7jD{a`+T`jJ$$0#-|PNjmozbEr>SW3}JpI3oH8h0_+O}Gx}_2 zXovbD=W=l_ITBZdfdZ!58gXgr^DrJ%kJp#C`Qw`?svmF)Ux>aG+f%VcAw<>KIMlM zPhhcryV*2XyJe^Dj;8vc&OPbacorhKmj|#dLk^Wkv4NeO?C!>YZmKq4HPH%Mz|Z2} zqO;=}A1kd9KIN*d^**AKbx^4!{bl5?)HZuaYHQ6^v}BmfmZPI2TDyfZPx=O5YuepI zBBE<#f!`qup(fB zXvjH?L(fLRLeMs)MStTHmLv4Qc1KF6pz=v~ou@lOG-y|1@s{O!r(^ymwq5+~*9t(m zBaHn}i<&v)F5Gl}qTl0x$Zk|jt0>ap7mW#jqJ4lPak^!>7hkg=&e`z$+a95%!FW9z zhN7RGm~Yw=J^rcO_+usM`!(cgPc|y62mW8YBi~*7`tJXnC*|X zK?vr;mJzqXtf#tm9Q4z*pc_4MK{U0Y*JyV2h3{eEU~e|J+0fKu3B!&0GXSd}{Q~^2 z|Ar*4Q~m>E<3Zb>CW`#bS?DJL8H);Bl5c#y>EL~buNRspu3p}QLPgT{*rFhG&f;Vb zNxraHdW#yg0h+Zw_(4ppo zKsJS4%h4@`=-kW!x!KRp!ud$*io@nZ_I*41xE(2lw&SIe9fJ3&2E{K>DiT7QRv6zI zlyh%z>1jv8N9R3zn*53s2>j=4nSVN!zN-x+xdnpE>7yHaW#^UPK|l{S$(bISz!_zZ z;@M~2g1r9IulY88^K>1Ww~dd>Qr`aIYr7mGtbaQ#^}-=`!;3red|}Ny@?7xtB+JF? z!tH%#M@2lRla>cT#||+%497!F4*p<!YE_-rB!He?W3BP$vj z2hShtqS!O>rauvQQPd2fGqxvMxY_G7zDu+sUaPa}XzbM3{E1C_Kjs6iH0JgLJc44c zq8hu47D35W^lYeRT}gDk^e>82_WGSqbJITUrytJ+bP}At`NImCdK$_<1W4czdD9Kq zmE_P8m9I+2%)$##FLt~JH^%1!5ynKHaYf zk32hsxs1sgeC<%5t=4XxU?WjII%9m9GIZszw;G3z=61R)xv8b(qH0jxMN8=}cp9tk z#imHaE5F)U7!WB9iQd7D&5Uwb_PxdUK~~G*b>CD;`)a4YtlCRxmjcG4`12=9#O*J! zeB!U_5+yH>sYi_t6MS`mZmCf}>kGp}%1@%9Uaxf%#-2v|Zp|=wbzhm-(kNzaDJBlM zdWdqmP7amzHQpFNZT{J38zHjgs8zhM4=7ygR9UkBnB0I%HhSKfD500IU= zKIb@b>9bs{0MpgMmN&}cAY|-Z(%iQ}lZ#$Lq3kQV3+gDi7%w;JZ!rGORz^`h3%Db) z0v=CNjtVH!>8~9-pC;w~jyntio+lMZH}Ed^wkSGwPLKbNz@Z*{0M0>M6IS^2t~kS< zImqi{I_&hIbY-yRR_eDM z4+Wd>-*>enFdgWf*blEhPGp|!{dSki_(tkveq)H&xTZe9JM{@BbnD9jO?dG>7>tJL zY&GrIIB^BVKEvU@BI`A%TcM@h)WJG+nUPYu;^d>lffyzoHQHT`xd+U@9#)Syc}fVC z2iWiIhga*#PY%WR)wuc>FK-9%P0&KXKVVQJAO5jpxLJ*t243B-`d-)MyD=}#H=~A; z66x%CtdK*JH7(a35OX&iMbwtFqFS0|G=3QQvJ~&!9KdI!B^T%omc^I4Of-(s^123h z4eAFx|2O$FpAx&@*p9+8(A8Y7`-=&zn_Mws|Mdn?ZHK5mS%#%DYE>y=5MmFTRc))SL~V?+KoNlFskE_skp`n zTi-PN-z>vb?|iY-e$W+Q!@}U=`v4{|0{3lQai&*tsiX~(ii#T_OKLA3ryh&%kJN+^ z+1AnYO@`mtTuwS|al+&EHXl&cL|CdAGj@W`Y@KOPx+ooKk2Kq9+|hV3T!rH`#R_si zAda>D?zNF5NL$?J4)Xb-Lja-w-jSlW&CN48;LZ7EQLOotHQ9c|R~NOh#f#h6_jkkY zPUl!G9s}6j$})D-um&|ceBw-PysIA?y&-37t9Gzi<=&d^^Ie9gspgFJ2Gz}R)#x4U z>HKO$-AK<%it}M29X%L0%|$>50z9{H#@|?Y6AyY-|NTV%#L|0rE&>nF424!}1W>X= zv2qBSW35J7EN@Ng(l%ZU4;0@X^m8V4^v!(WwNf|S?M)Sv7O~s4wLTXl&cQlY^voxU zr}aC%D>5(7tdP!9C!|heMlQ9fd&GAAyxSQKokf?5VoDNMzODJROaKUTn#Rtqw^{Wu zqiVK}ToV(2|7fbS5R@i#Ui zV6=DHc%_JVd42a$5PKqmtz6$hkLk*jwXjOG`J+4VkB+iz=c9O@4yFEdQ)ODombd2{ zV|<7c2Ww>}G+R%1ve}R^IunkEb|3{_s>waI?uOFhCWUb|!TuA86_gl!pE9m1t=8@D zzJcN?c>S$owTwxhLVhnb2BKROa{atX_5Mk!dX8p;6X?cg$h`s(?6g#W{0X1m3_2-L zTP|=Q@L4fA1J?Ub=mNhiFaj8f0Zyiz!2u-^DD3oT^P$-&TX|~$(vs#tk-rJOj(23} zrYz%S3-k`@LYTha5fmG;N*^mWEvyqQdOQ~*p;Nb7`?NN&;!2l>W^aj!WXtZ$@k&one_@_UjLt3_Y&&o+W9^fZf?ydw>N8(_DKa(LMB- z`ljaFq`KZInRRG1eTL^0BKI_An$cOKQK7}GCB}s4Rru91W+m<}$DWkx5^35f{p8~Q^HV0qZ?YpKV(V}#02rbgdk~Xu|w}(mFS%ti4PVtP@<;HyI8-h8i z?)q~D8uS;HgBEZsvo)*pBe|tzMO)3N+-F&Ov@xHYVfVFK!JQ8T!&vAh1fF%(X#W){ z;bOeg4Zl9uuYUSD-?k+Bb#J0&UJ1flUEdTaVbAa~t>vhs67i!gqm3+27`@z zoZ=xlNc(n!++orX+6heOU=8B#LEY?|^8}~tX3`w)qBLsCHC?RfeYhEQ^isa=6N)xqSQhN~6RuvMv!8k>Fw(8CGs`btZS+FtzGP5? z6$a|Wo4rVZZMk#o8xlZ(8!A`F}PGp`e8it2e-i*U(r!(EP`p~}L z9Zv{c9ZmCHFzL0_LnF+{ZYVd$R7w-hsOhRKY>(C^$(-T9>s9JTuB-ACYP{C9T2+hM z8}WMHUfEZ=I3*LW*xb+YbpQ_x6b0(NKlv#4-JW*{omuo3@(2GWRft{wb>et|4`2YA zQtZJzr{MoOF9fQ%NTJWo(f;~CA8%#%!#o$Hf5hOiz@cve`qJsAAgTF2Q{b6p`VR$c z9=jSQU^hBvlLchp1Xgk1&_Dk~=+IT5GO8S;_|3RL$cAs)``-!QNapo!VEsQ21T{9c zG4uLFc66s5U+yTP->TKIDy!ge(-!QSU5%BAvv=&OW;PPFGZZt9V&b|DNLfd5X+xC; zO$>#4wkDa;oh58N4zI+1R9jJQh z@u3#CyO~;1yM3&^t;n;w*013`E`kUxFZXM^PO@I|k}ryo^lb9f$k9!{7LIt!BAbTX%O8wXW`1NWj57?_u z>;Y5LVAy5z-XZfFCf11>VtYyVCDTgXW`RK;)-|e$+w09%2q=GC>BrOMLQsd(8D6FJ zmKD`=FIh#wpgqK+J^*A5!O|T|<2o@`*X)E*6C$59qrU423pbFjEPr;u{6;uT3q5F? zM_TxVTK_G^28DDt6D7Xc1n5jSeWf@PV5Of?1UZfpJBB`>pCWf9k)j*=+{;w{MD+jP zkBb*Z&_LS|bZXuc4m2>~T@_XkL$Xo40Ivr@IzCoVMnv+nRRATd>0XStsx+*d7%o-a z71@hsFx6{piK@Aq9JF25+0)6W%XN~a?eF*7g`f4-6KqrUEjoi9Zf9yOsZo6wRIawi zO9ta3ukv95_q8_pn_5G~D({|`Wu-IXU-{~#!j8E<8VI|gK3S_~z=p=5Xpg$Q0{soF+c)0^61)S6N1QEF29R5|qb-nN%2c9ZOPmiFk!ld&|2 zzXTA=i}!uWf1q%1RD>}=%KikS7W|N6VR}&B8l`tR|<4Voc!}e8RznrLd`TdA-5g5w%w)5}k`a%j3!HY?C@^c{(PC6r?NqmPgUV*dR&0PfSW#n#92#?GkLA1ND%ms8bFg0~8b4Zlk`!%i zQXX%)DJdoch3&7Q^LXOtywcK=CsKJ|0E0wJ3R4&vnV(SGKKY5I*qykCoW;{7L|5MM z8}Y{->8694T%j$X9PR#1kbeU|^Oj_>PS-oUgqz7P8)H4>H@q~Nleo3A^dmm)6XI43 znF$dkEuK}z&COogk*YYJvwe^A7DKg{@Co!y`%Zjh>X9!F2m{yRR1a(ZguDS)eMte| zU%9ek`@`|nuH}CJr+o{yafb(icFQ;>uNs~3-awOFYSlo7>~~IuCLbZ zo|eaiDIVbDm0rXB{Rt)6Z^pas(Y}hnr(43NMf9KOwtZcIqF#p}0i-z5Y?^1RA)&U$ z^KH}a4w#xVsy*M;rOkRa9c`KA+?VN$B9&PtEd8sU zcB9+6(1Zj0fN0jqOS&%tGT=y*t<-=ObtxqML^7Hu(UqS<^~XTJ{&Rkazb~F3JEYNaWJus^f zc1?|VXa=B8u=v!8e{aM5j)8Sc6EXMp{Y%DHPo?Qi((KmRY&dRD!fHJqRMM8^D9OG? z3#8jim%YV-%p;#1?Wvvuq2a-CRkL@7w3P=V%3X@4V$o|mXt#-Vt(vutH}Ksz8{``5 zxod1a;O)cAAG8;N%L#Q(KUHgAGM6vQ>y=``(hGf1ImjP>EU!b#wkqqKXjas=88WS4 zndu|XWmTQe6BtUa_wdbJ)Q{K>jzidt3N534+@c^3Z7wthqT>J}WvivgRzkWfT(RTmK zOX6R4n3Q!GzfApC<|V&UD8~3D|Jr$;TF?Pq4TvCcIYZ5LTTiy3%3J$Y;Q0OuHL%%W zy2CFegTH}60KX2hooN64S8AAB1N|~5fq&sQXz-^FJb?j|nE_C*Anik)O$dm5zkwri zzhe%y==ue)z+=DVifQysU{KQL-4NG_Tp=(eT2fIa@oYowtA~NSZdN5*Zism{r49g) zv6i$Ua|pmMbOTaNa-Q*Xf3eBC(SAHNHuZ6(=aMEjCGr914g1>EUyqM+QocJz@t(#;AQUIu<=2%9>Np2_uvy@bpD2@&P|cE^P)~BPWloozx#?zfyihCd zFeIqyH3Zu z%H(d3H>wgk&AWJ+9|!^upkRtWeZq}xwmcr#ogC4Q%E+W>;H zsKl+E;ZfCZ7JEu30utXTPj}M@ni5L06vo>o!-SEsJ2<1wTAFEByZ5(!H zJ0Tr=?yUuJ6G(?-?8HONt_mC83I@(dB95KTV8F^7qB00E3q zEAk$8Y&ETQHujohpwZ2gR&%!TWCugm}}ZPYt}#W0Ww!fqZTDoi+u z6}SbK%)2wUsdjUxWACN84EixNtx3>6)*E*<;C0X6m*z{2ep#~cDe&@``Jbvwe#bR` z-c$jsg}ggz1iyDqp&ZQni+3VeF5Kx2TC}HY#wlZ^JKN82tRa|9tG78ciHRnRrfxGE zq>++uQZMXzS z6?q}U1YUDu+XZ$R>#W*dj)jjGZpYMHApx!;JCe1U)X;qR+ zL`9sK(L^wlHA`sF{ewY`xAs^yb4ebp5~kZ}EGL%Ic40np^vg zVViAVY&4CTGP66Zb!d$RXFsBw2C34G*y!;3o;XO*zcsBmYmwa;{rzsEAB_p;hUqeF z0zK}^ua|`MzM}cGm&?H&ua?QaB-*y@DC%7lbmRuUuO~`+CHu?Qg(LV(v*&GU+8@lq z-YjUDeUcyp4{xsbxI{#|islD3CmU{6Mjp7zl$Y4!A&+{oDg}#GW|Qs7QHLV(Ftp~4 zI*&`~)-*jB`+n6DH;(dEvww*^{9s2!A8z6(_u~nu+V}naUeYUBRK70ChM(=}W{ooS z{SMbUd}edL8uPw35vIX`BYQ&5$1!v0E$Mc%R`K0Ur#m}f7}lIFC(XuU4D*7<#CC9) z`S@%jZdRnN4A#=|`0hNz$BGn%JAsQoP1w4E-+r?wkgUDU$O7%TU}N#Bkh5n@6ec@p zFqlp8-ei$`?L2k1J8b6gLMDzS>oC-&oknK)Sh}QnXXS=dyxq|k%63e8xLc=2y*?J@ z&?8>W;$4Lw+j58KPJWcLpF|2mwfByXviR|>G@ls(t%3*eZ{a~dnng`d|mT{gPa zrCO6ZluB^JUKAYG`-$LjX?43x4cF*TX(d|pwTaSJ)v^Xhq>4VB7O5I%%6yOj_HUK@S=vk4vAn z=W}u}srOi)98XfVnGMO&b}cecliJo^?TMW5VDHzPyKo)7}ev?o94 zl%cz~#HZ(W}S_3WfDFZ+EVXzkhC9UePD@&3J)^$hU{bB3ekb!9S>>^7)Y1+F`X=SB5G* z73NlR-`TUr9RPJ~$E{i?vy*8u*rb6SHk*B}=KCA6+8T&PGu#Y1gckZN$&Rd5l_evW z68gIAN6Q>{d)G|KUETIOr;IPV2l?Uo0LrM0_kCj|%BQ`VM?k6#s3BICuZkksXITVs zlWR2Fi+PqDH>WDh4Z(&%Z`F#XVdKCF^<}W{4)aw{sFRGl(#HcBY7EG^{dC;wX|zW) z#*MybaEvnPS{4@7#2ku{ukPLrGe6ZRem-?UaN{u1evq~3Ba@+W|A+S8H*IA&AP_ag zrSetL#QV%rO#R~~B4*xkBaeFAXfWLH8Qqg5-^Ti5u3G^&O&*z<2Cxs)bbnmz0DG&+ z*T$pWY5+I{!JM8gw+;-RO?KhZ8rftl9+p>;`x{O%m?MQ*g7$vVGW5wM{#5vMdSLWT zRP}UvdntMPiTg#Y`kR2>#I9dWX`rxo!FR<48wU9~kE6v+MG}OD+?~cW&nL~wF`^|I zk6>g*Ho}Rj>_iC>N&R{rSZ2@LbX&)f*&suG-AJTLb*Sx!Nqd^AtfDHk=zANLJT$6T zrr(Gra)f_+{9+mgobGp{T)*M0p;2J~d5$O+%(%RJ$!X)v`Jvho{7oV%hY2@@8qih8 z=;Izk*>0y+laL1$ue*b);rel^GW)skv!f?*E4&h|oFgpRDLJ`9^gDfR43yK$B7VnD4;IlwWQ-O|SeOY$` zeZh~MiqMMZb5*Zw{KPPa2R`YNtzBoo4cF|(=K)eJO@*aMX6d@-_{OZ?6}&H`&s_U*`6W@DO*_>&;WpOR z+8A!!nmsZ0eYs0PJKu7#wnRf{lHf_FW7?wS~~O=r0{a>v@z_L^~!Yk`7A zER+$*Y}QbdyC=t!?FmyJVXW?{fT$d^cZXjPpnoq-QwY~r+82g>xhHT8WdI&4w26;1 zibC$ZXyS|%>xlW}MA;pVc&9J-SG7ppJ7lumJBBgpboP^41BS5HV8H#do{h*Brctvg zL!`?fUd(1@H&`%DY3=@~&>s0;m$?hYv4Mo?GA6nF2n~O~e0G8FxQ4CDv*uEKhj?`z zaCq6rFnV4=t!^Js+HoCc@acql5Jvm=z^C0^W*E;B8&KZ@_SE^o?ugXoIbS~kLEC>7 zsjv6?QN+Idrec#m6FE{cMX-Ay`e%8Udv#OK$b&fm~+UpCO^E`Y0`ePcU{48B`6 zP)@@G#-8XF(ZOf+0!PL&=&-S!kzI*GzMb=ncKqX0HB%^A7EcKV0!@Sp+Ck#~e3!7YzCdNQu z_qtlgJY~}2#2%@oEe@(olG*V`Re#qv_x6oY(p64Mj!Gqlmw#@Gw7@!1C z%!OCrfB}&wheEwNHnG9hNdhk1^#S&05fD=VmTJ#A%;}}cFc0;HUeo8nUZ92LVH3_b zvA|84BcLaCx-eb!*kzUKHmu7*&}~dKkrC#1c{@)aK(&QhJ_kjHN{0T9U;8}kj`*5t zO6)<8@jdGWnTl_AoZXT`u=Xy15}w53TdQW5-A#2P zOr>EmH1x6TanuH)aG}%X7GZ~4x3{ZZWjQsWC3cs)q2&{VR}q{3Xu|xM?Ra$dT4{L{ zqK>&sZMyBLP+0z#>27Tpu3dfsh3rq>?H_-hzYr-N%rBG6Qh8)DUk>NXH^8&dV9?rW z3tZ|EZ)jZ7cnE~+9tnu(nQnP@U&vh0K`u8cF-I?7IN4X1`&^1FZ!{HD;g=70Q+n~o zpMc?YibVdi4Ru_f>OvkE2vGJ^F#3-_VRkI~8UOP?9&h*bhwLsWiZ^Y;R7OIW%5VrH zE%Vs!3(U#7dNg{y{&>|L;eC^wDq7S?i7uaK`d4!JK^xrwA6XR*7h?6ygG2#0N6 z=~C8kZAYUl#9CFZ=B&9x?l2&xSWW%vulqsidt<47#;Tp&ZNGLS6*#C<4w0VUzv6#tX7&~ zYw8O-3g!UU_EvOkI~JE%<6!7t?V=|p@2*xH&3e1RDrXfFO;6*GkN7De2-(}iJ+7IJhTaW8_rtIYP7INYsw$`c;<9ian#ZTyn?ssD6p#&S?1fpi0sDo zFxNe~+FFnU(m2xQv8KYr+x=pEH9`Ky#(~8`X2*wp!^QK0q0G_;1f;gbGXF2cr&E-# z+=>s*X9P}o{__2Bmu)Htau5FjwBlm_yu2i5dT3qFuVKd_Rq*(X9_-k)iuF9)H>90` z(WBT})MK`zZZVjVIg2+Vt-G9S=5Q43vjenHcbHlS-p8rzrt-sOzouMh90p8t1#i zGr+npC25cJJ`9rcRHPOx8FFq~3E_xrf{U8EBF7+|0zF+fX7gE0&?kkYoFyN46%@JV z(pG(Rkd$lmC5CZ9s{zX2*6!vvyeiz+hk)g~z5(3>kMl&ln>w{`6KP~j)qLvLLk7bG zF&y=pre_BegIX~a)=Y-GRi&~du?o?g+4Q7SlVi3$ShrfqYMf{u1|8tD_|VISI1T9M z)4S)%kK+Ak1s~z#z}9_L;ecs+`SOnnr&!Z(>v7|cyt*ERe3UyW@Lwtb$3(R+%#s<>(NqTX;i>Q95N=lM4DgXBqot*mX=sZLoO@ai;rH`nJ4&F1si z8_yfi11B&?-mT9V$UO@;lPdqtQTb6mOJSQJ6wy~@^W14tZiC;6ySxfSaQ4ryv4n36 zdJ|fBS=v8GD2he>6te*5;rTHuHpnCHAO~s?+oK`J9t65-D}e=|>hl%NPhCKb>bpW} z60`9LkIn8Lp9p|tvza=zwy-gyz!-0L0QK7vbD}n<;+BH_xC4F?_VRb%gi^jS2tCQ0 zm6vC>pUK{E=zOV2OFmQ8WZrTTKdWJuT3@0lpV)LaF z@ea4BwXIBc)|0kS$Xr!T8MDGzYsQw83Z%SQ{TNVrB1M;A+BXjU9U&^I-ZH4X@{$NG zq#*b8lx-ZEkrs}b{)6vg_}BoO)=7;s@86~X&$yCoaVcR z6-J!21HYi!uT1+a`MctD398=Z)VnU~nNuazTPBrP3jREET(NX?)Q?Op-LZ{9s3h}> z%JciS=U4Dav$mkt%3wI_IdsL%V7$6U1%v#UQIXNEwFs`_>}VSL{zI2c@i|qSbFE6c z8ti^goM86g?ruQ@nlbZkpr&L_LGsF(;w`WR$UU*Cv9WIj>!olAC3xC(ElQWwH9y7< zGA=Ckizz;xdF^>1w%d?v?HXQp+3i53&b&4Q#7%o_btDektf+X&SEW^AWD=cNKR4<; z)Iz6F`yG+F1ZCbl=Mlv#Q_wr$398z9>_&4+v(uJ&mI?_qTnLknKRmLzV8}XaZDz!3 z)oL2%tTSIVlCH@KZTncuq?xbG9PL=2k8zb7c{83HtyWRWhjrZ8URCkyuno}4i$ z?)hC1it4<5vY!behMZp`|P9n#gjO>Otx1;=`KRm(Mw_aGwkxKSMP zg7z*cz_agotQ{0)FMZ=zW$8)~ICp*XQILAVIp3>}V^7aRJ{cR#L~|9#Ht9$Z*nW4G zZgsl98UPG_>Smjfv&q||9cc@=n~w>v-rrZ)ah)?sXT*=|t);T>TCFytR<6;qn+WJ{ zd93o$^v=;Tz;Zjs3{W)dqX=hxnf9H8_J;+{vW4G(&b)_FN=M#nAF7EWuHW|b&DfmR z*P$d=tU~z`>T{hJ>~jd{lGpsUh$v$7=2>16gC#6qfFJ!F=8aso zg#%3q3|k56N3thI`y+?bqh2jox=Ncq=Bv=#DeW31dYo=<$)gmR-GK_{6XrNUb?S!$ zBI}0BeHJc`Jd3ZbY5qk0e5yIVaG)qTF5f+=I4)ROj0?Ia{U}2Brnz_}c$WWydJz_W zW*SDFm0{1c_F+EuoP|Nvg1%-?y?t2awvFy?rbJ%DZ8sfZ-fd0zO}ZzEnZ4GV>$&Vq zj-y`3z;;Hv5l)Hb@sKY0IwcIl#42d>8;w^ixR%Z0C2iGpK@Fx@Y#> zUVCYBS|#`Vl+N1pZ0|bhd;y~&otE9*tic#$bMC;+W?OFTKx!l9_bi!v`?#mCBbk`g z8>8xAxHOK-NIvHKdC!2Lq~ELen}fTj{`Hjm64!d9Jc@BQdrk9Y8?r_x4Ge;GR@yK~ z3}F-&A3*=fP29KuV*fs8tVn8|*G$io-}NatxE zp%#Y)=+>!y;q=gv2aU1cxccegqbR&Kg}xex`*jY)-8UY;ZMM%Ey$}fa<>whd@W|hE zdwIH-0ts1A&pv2HEiL-n=>M)(Lv3e*_m%wxRV!9hV0?ci3p?@arI$Q4K-yJjyFlaz zU0l|NOTj-8?(m=!Ikttcm0 zq!bLZo`_HM8EAjiyl}dsFJz40vOpz}bG!mHI8En!!4P7#DjUN&Kt(?{96t zdrINXDnT;my`%6OU0AJe*I?C9mgup`EaPL57*uew;fk9Ypw122{wSR^cDqSdOZa@; z-BC(Ajukr%)SS?#1Hhi)vqsX-d-GBvvrP*4+!}J=2`T^>`oMHc-x}+M76MIK3xHu z@444g9j;>~?d@9(KhJt8WZ-aPu$R`yDd8#>-XtrgEsR{My2MG$XIg`%&`>a6P$q4f zw3_kWAiKV=ELOIT_wlt&Oqis0*P6L5;R7G$+6M8%iKRg-`va9}+;tZrkMTAeuM`mr zukRq${5-{@29*G+m%qb|z+{E6fKn0*h6Ld$p`)ZA`E(+5KZIrZOE3KE(( z(?Q?p7OaHm<;TsvKpqiGAFGs|JUeu`Hoxf*?P@5<8_b=?-RwBaOejlKETx;S{Qkn9 zwXs3q3OZ}9eQmXxj+dm9uVrr0p;aD$LNO=8>mfjZ8a>>3!qET9{A9tI`h7OPU4&H< zJM*><3`Jqe#|-TRZK1cDj>P7u6K{24o^^H@G21TEjjE*mUSMdDaNc{v+;Xc8en6)& zOG)v1aD*3ZB9}2?ogT6=dBFGHY1k3Zt$0~%a0`XAx0QXRyREL2PuVm#rF=r z6SabW1>HhJK8t*tTf56>vtZ`noxC5r8%(RzCiQGJR1f*glaukPDU#iWaA2y3dEeCf z?Lc4J=?c1H=q@oQl=*td5m;g$y_H8X^+qJr?|Q|rYRJc1+}wh%A%g>bPw0^G@+MfV zb0LTrcX?Bo=+B^Q=j&7_4JpA4lA5O-+QoVmk(QLUtT$9{B8X$N0nl@u=1}c7`K<3z zSQZ`oDmL(rDS*=*ntXMh_eP6jBt<>5PmR6$O|t1F;rpS|GXxlqGNyTxx%}f#G~V#c zJ=FJyB3Cbl-yXN${8ZP5?*8t{52b9yT0lix@#jxefW=X(c2yv2wl@^XjaF;QJ&9RW zE49_M$9OW^4vrKnjM$VI?3i|4=!(97oNcXDy5$Tsk^JjPC8t*?%ja?1Ga%#^+ag^)6TFe-wXoe2sGYo zLoB#eP&~7)cvDo4HJ)`NZfL2Z%$-)w^ZknCQ(<>SxMOU!8&cAo$DsbZ25@%$Lo=#W z_}PBD6J#FS;g#mHo(!8)YhmSC#E`Z8n23`WX5_NAHlL{6`)cB}=*S~DNzZG_;01=8 zTZje~gI7>2=^N9Fl*OCp8x)6JiI#l7x$sgJB*!vwSZVg@FibVBvvJ~P&uTI1$k}JL zR%KGxaB=50D(1MQ1F$yEH*{mx59*bv#xE5NZ*FlhY|g6b0i*K;Z_RRMHh5xx-PIKj zr1|V^mD=LJ)Ov%6+;MB8MS?YSy`H0E3M7wUYgd#w%KS` z$ZQz0BHLOo7TtW)5ZLVmo6j;O^`8!hlyf^h6wZ6wtG?eSzqJPc`_T}@hIYhE81 zYJ=wYhB2N?gFu_DhDIycPMZ>`|CaFnZXoZmmvUmuP4?lA_+ElfubUoay5mgtmg}cH z2Z%F}eZqa5$882Fa<4S z4t)a1SSr$6uP)@L!iBTs(09Q}&)qM@J)o}SuO~%5m-RYj_3jA4+Xx$Hf&I`C(N98v zE);w6nT0!Y7Mvvc^Hs2JjWW09tmS3Os&Xw^beMT-+~unt%}uC0u94ExSgf$BLB|nU zOZ1K2>*do8A$J8QBGXQ#FGkQdP1)jIA^6{NPu?U^@2torr~0<0Jkz~hl=HRX=zmsh zeN2Wydpl%fj9AW>qZ+YO&8AXWnzIAlm$cT1sqA)8GaC#7f3e<%Nn^fUI_xH4$gw!C z@kgDgY!?+~D$JTHo;UHyX5RkaKv%w|D$g7H&YHX*5tJ@+=rURrZTT zo#FvQ!0y&*r`p;|6@nA{y;;Z4b9Qawp*?M_j{znH0B7IYEHWZmwOcaYZuEP^W{hnj zCB&tcImZ*i-N*W_e%Fis_wJIXt8r4J-_`+n;=kc?AabV_Krag?{F&9DbdS-pLkhZu zW3;$tdX1!kc}%X`%|Ux_?73tvR)T}Cwypk>aoXzS$Xcs?t;ck!QPnt3q>c}xJTS?S zn2p!AOuM~!)+74TiF65<{qg7TT651Vhev(a-_``3`R}Gne(vlpmg&d)!>&Q;%Eu9Z zzjEmBdgjdLI*LD1#*HmCvc@*aw53Q|>We`&`2X2^(`IF{EnW0;{)>z}U(O9IccJ?Rf;Gg)7BnHtRCn!7>VQ-)H%zZEng>JGJTGfwAMFj!ZyC$wvRh{A24pz|Rf zIGSDcsoRkgL!8N9Kw=I^ykG)1Dh1S}0Zrxshp(hYGaM0S%(UEi$pj680VILaPRp3m zDBCyJVKXY1-E}7VhbS82xSF=~8J-S!km?T6$FS8lZZoguhyN_TN2eCm6hU73+EKm@ ziJrKY7Z9qhb`Fd_HCv4w}6KRr8 z7-KuaC)@%CC)7GyCYU$kp}mqEjRE!} zK;)l3ggn>D4}}jcUDg0C7I}gH$U`94D)mX^_%%q!aRE?O{eKo)Il@lA{BDJ>p5$Fu zC8q`JoBs>?0a)=uKPb&Q>x5cI?sy}G89`pik5{`b7`C{sIz(!d4Fmq`%tUqIrva(% zqQES#mj`b$>$cV|#?~3lf_XFKAylr;b9 z1OAJA|Na`0?xr#UvO>~1Y~(c3Sy@D$%5?r}$b(!=Sbux0ZI7ro}jY7>u|?q#jF{4mJ9ASQJ{KoWi@gceJky z74U;d=C#vzoJQ8zr@8kKjqY_rG`bhb;B(H#heNbcaD}IOyA{uInA@5KTYMb|DciamGqhWZ}5%*U8!xMFQ z#2);CXFnK7=U#$$h4ZpyQ9sLbhLN0eNMPoa#N(Z+j1A^Z#?e`8eb^HI-fU-bQ>3%i za;Ns9oj;e`oh=p-`=vl)%jJlS5A_KR2%&M?n+~yf8FTnzGIM@Zc0W9L;uQFQ599?f zx0gk9RW+Tbw_NQk0;5$+M9Vdjt2WvF9x&Z z{T1g!SxEr*q3CQt2lDwk-~s_Q3c@Sbj`kQTNDfJFS3tU?88rI{PZcpsr48YqSaRg#j)M>@u zW(gvOSb_e9vkOspu|Z%KTglv1S1{T;sEP);AqE_q0KzR+jy+iS5xPdmn1FK6SOT?*$5ZCj_T3E|ieV}zFt^3BzJW;1=C98nC8IEpds z3GD>uf7~AEysn?b{2Njxaf$u`issfSPz})LLErk|tbASzl&`^!0sQSD?)2i-L%qAw z*Cb)Vuixen$FD>`#)c~x9Z;RNN-MrGR@WY{+En^p}QSH^K;8 z_yQEQ~3lpzxb@O$agLvzue%qe^K>PE2L=~I! zKQ$eVj?U3U;TBh)=RX&$;7~`{VdtOlyi@*OYHokSoy1(JjCUpGiXuqM)jL2>L^khp z1CH0KqSWZLHcMw!=M}(GiC7BrcHNE0iWMHG)JkLz$nl7j>CuetyKU$i@`6a6y8X`7 z=cwLL2xBVIuyhpr>ny7AWa%??{Mf4V1zCVzCt7`K+RqJ~O6z3a_GOzJ$)Th!zo!tj}tmiBYLZo8a+jY9jX@GA- zLWo$UTdt`M9!BS3-E6q-QWAx)FbS!(VR#mvSlHZTFn$f7%CCCS+4V9?R`8_p%7 zp}e*TzEUA4E2g4SQh$1JTwl0XKk!S}wS1=K1L>kq>fznJ=;g!^ck&k?NkHPo4kRbE zvF#9&>KdhFC3B`GwB24ha5Rr-D_yEbWQtc852egmVxc#Tra^^itGn?uY|7lAv>o&- zX^^??ZL99i<%k_R?N=3?kG=U*+zgfVptQ%)>banKBCQ8~(&y=a2pfE#Uqp)L1{wb* zPf~CL{UKaF^5)$LAl&TF{j*rXk>@uC(CJC;))YP1(Oi3XO>ZNBIe`}|+T_-U)rzre zGt02SR6T^;Sl`WN%XaKFw+k?E_7%o5*+`uoNJ;4_hTog?Mu|BaXN}k*4N77sbEp7a zw7C{mUnI5o7vsA(^9-K2OE<0XcTCGMTnlMP7w1 z8arrBOIk1d`k?$=x*R z9lP3g7HwCV;c%A&=_>Wk1PH|g{ix$cdaQ!~t!TmU{5S`9K9=Pj74gm0vr!ydb{J=0 zaq1%lTVz*`Ux=eic^`c6azSJTK~~&}F?hJ28w_X}fZl0E;3bqEBRVgXr%Eil<^FoZ zH^T9VU-8u%-(Xr&qLl|t<>2iH`?9o|mUKm&h3>qvowfF({j^6NSY{+nmQ$rSSK( z30p?8hJ!Ga!79#HIzEHCyjZY@$!s~dB3@1?OARe6okp#nQ3MMvep`$u9MgON=2P_` zF4`LI>Z(FC347)-r)vs4aa0IScX`Uqk6 zA?ZNQ0Lti3=y-fU5dGBuRV?$BZoF*7r#lGZ(63e4kJaN~_nmd`o4_}@Fq|sKFKPWlg|`Cbdu`ru7(4UU_`Kf>{n$*0Q+x=1F1ME>n6Br< zJ+)hlYZa$2O1j*w5G%Jfo@};*GCS@vT(4H$(ss4Nj=JkZPaGLeJgHY-L=PTI$DP7M zY65aT=fKKuf50faV?GEVuY4zi$&9%&IgV$$l8Ya=?|)RFvIH&Qv{60r}M%JURd;b9^(uUdYc;7(&|(Wh1|ijGJ@X^3G~I{(UUx zk>39xQKa-FQK@C;U<^f5%nhc{ZBzrPghFD9xW@7Qxd^mSmOpL^a4n%&40gurQw>Q^ z5eq$OL|6rN1E6FNlbct8?T&Z=Za)6| z&+?rEe&|oaiIO)@<7o642SOJ00wn>e*k|XA*P)C z1a|-N=#88f$U^>a4pj51@5`XwQiB~3>y1guWkXs!x~;7TL}kuamL+$@jjOFrIoVa_ zIPEukO(o)WGFvMRFR1I?`T;BPjPKF3jC<{53sa(FE4vy@{7(}A88Vs5X$B``K68T! zPKe8a2o{hZejOz*mjaZgJV%3|eg{B3VJMyt_;tR63^(xSPV#hhutIIjSK>izzks98 zBTbiNC{*!44oTnJ#o>$gskU%`ll;Xk|{A z7^N9Dt}Yr;j~!$t*mmMJinmdwx)L+OVxqC*05rb5vpIJv?rpI<69Z?F5xck)?z+O& z*z$j#1mB+=r#A898Tht{o>WYMSA3B$9Y7*s(A5S7Do%&=KT8eXtK~a^LU;=w3e-2M zAX(q8z$u9UKcAnHbb>{m-3(WFi&$t8J82)lt3n#--v6WpvV+RbKdDnQHSQte5N z6^wS5X|}sw=^J`T?1xOc=C`FvM}rvbv_7V%>Qy!C)nRvu;GAq0SQymRQ<%VyUC-+} zk6is+Ryfx%*R>enkv^@($UW8@L+zQ_{j{DcH;B+Yi1klq&#yUyN0LZOAV=Z&78dfg zQ|Ol5$ZT|0X38mo)DT8PFXD%BBc}JGGA?f2jhjHCVy5<-5QC}>u`>?mLS1cf6YvZ( zMWLHc1!9n~CGc4vPh)afI$UCje{~_(`DkMs1|1Zq#f{yvEzTY?kKP?M0GwWWm?J9Z z`xiNk=QHB+S%I&e{{=dKUVBMCoM=!~^w1?jrOY2De)DBI(L62N={>}Nf#8b`XzaWt z9ZSljzA&0R(O@z>gV=7fm#ycE7TGspv{m^)a4lm{l9oYlF%V`AO|gwVjOeKnx)N4M zzC5VwlSRjlhAN=H@%WGH_OtbdWbH8kD>%Mq+u{>fv7mTlO7R9xy*GNFrela_G}--H zpUQEM#o7&dIVE;maX8+PeB96kp}V&^&2`HacQ^)={?V|r8RAoMSxH;=N=?OKC!GN( zqsmR2w6UtTDdDHc*Vif8_Cy&J@q;{w^@@Qd=(Ff^-YqGa%TSSp_vM;pCG9B&W zh5_h$Z5T-5hccZC6Gf;QyFF#Ef+<_Ip`5di>!qm|%_m*AM2mXg=#G^QB~%vbP@duS zGE4b#uft-yL509f^GB^xJmR+neYsQ6bHcQyuQ&ux0Qty|whDUK4`B+CPCoMR?iRQF zy!#l^HmH^1xyfX4_0e!6?>V!-Td^7;w|nxqBUFh>#nDRg*csdPPMcKtwY7F6Xgn8` zS%V(gt|NrKgjhE5L!InuI;oSyP)K{UGt_36&GLd^Udyk@=oTgTKl(R54zv;4u2;zy8as z;&nIa8buGgR>QK!l^MLoJ`*MZJo+IatV`WgA;y9!PoCuBZBq#P zO^_oTquKQE^%wm28@7p6lkH|bCYVhlNo(WI3X1ahYIm{ONzQx6`-;F9fMmcHb%-5l|GPg`kqdio5RVtf@$=+!x6`@itaqR}* zUq+;CkK3Hq6h&v-rs=-ePkP2-(HgX3V}D7h{k@UIAEf$G(C}N>`^~uaC1_X#4RV=> zLiHy>!ynWQ3}=CoO?7iRT>sbsgvAj-7uk}ZF71gP68zKa+$epO?Q*v3aP7)4Th&__ zeYY{{m6ii9^CYWUiqgHk#1|r4j%pd(7D%_mq+u7DdWNMLuMq&+tq-)%e;QTuRpZfL>^-MojC6cBu|4Z{x9?|U23YgZyJrLq}0pNjaQg+~iF zAtT8WB7H^8mnD7zD1;y}~1yB;$z%P=M$PdQPX&h~V*yPR;k*>8(;bGl_q>CR#lySCV_d6QNFS$;OJ zvVo#Ml=zGF^7A2pS^#dETRxEQb9v=!@aH4vHhT6$1oKyg8d1C~1bT;7;iW(i5{8|M zL$M!1m%z|ycMmO62}TJXuVQ?^TMx8}FfMWWtn3VwT9;Z(>gA@~+FQxMOs3T(U*gxB zte*}K(SVRPvEUk3@8SzzshtlO;RkVcP18oTKM%whZH5oUIH!iVmcIaoWy}ZhHV%Xe#}4ayX-IRUnz@Z=M)ex=N8kirkVxxr_;V-WOieXK7p6cLjy@8CpxozU>-&S7KX z?0~VFo9aQ^)rC%~VPmmXJ5(q8Ng8Za9snt$=3H&Aq`|h9uDtnz>9zYMC5)5KS|2U! z6^At)H)(}b(;`Zn7rOHCLH7R^f(>xw0(y1Fa3oD1rUWkp3w|G|=Yshv5g14eTjDxB zKNDtItZiX52N9JxAy^N}oe@4fa5k1ulYNPu6WF5EnJxGwPwlybz5_6-b;D?F+dGWf z^{DMQ60EkjoovFtw3DBr{~U*JKH%ENZ77IT&Mz2&khi_QICz|a+%bhxbxep*5wuZN zw%eYKha-8ZiF;3ySZ6$wR^>h?GKtJ?^$J@WWIIzkV6u=6Y*M28hasWuDs9RmD2!)& z0bY7|@Vs*L(hvY#a)6fSwou;va2#TL$FVZ>pMH9Np9vzqo!>y`9*}y$JrW`+b+;?- zC#p2)2}5N9vs~)VK9Ym!R9Fq9PP!cNI#+gQ^DW=ygyp)@8moPOR;^C)3CzOCR#!Y z=k{0lu(*mEU(W$pC=hcJNaN<&#&3Cx9PQ@hIus4F=RjcUhuRlf+oR=NJ;gj;`sS~( z@B*k?bn}XxJ3;t`=`=@bAO*))kZnSG>-iBlTMcCoQp`Mnu}0FMs?KMt5fi30qPF*z zYhvpzxQfF1j9M$HBIY}Fes0wv3b|Vc6>iYhlD3L>rM}8(0Q^EMRWj*JE}r^d5P{5B zWB+TL=t<$VATjtfU-Yqib2k4X+e4vdKeUqQSu`Ga?H>Uq?iTSE0$nF^#VY2M-wJs3 zQ{Sw=7v0R1t9VlmhmWz`Sw8`{!M-M6C3`N^82q?LO0*uck6eRrg=9W z6hNK?MauGj^UBCq8xT|0!>wzwv&O1HG<;U)=ekU3?yjv=U7CkMzbw6|;hCNpBXSG@ za-JK=rR7A`J8aTwkcVwpGP{AmxK2-3`a!lyvHoQPzGH(vQ^c}qLOd3dT3>n?9w)C~ zRd>A9g8zlfD68Klyx%P-WPTn7<)1{Jp^o^PDq7mxD_=R$S6Y^BQ0#P@*uB15cIMPh z)rhf!b(k*WXNNWigH_{ny*UKKph>KDr9e3JjP`Cln`#EW3*@TOx5aU2(l%MGlbJ`6 zBq_fz=pG-eH&fH$2dSHN^rgE+#!o*ToK;xj!vBJ#+U=BQRQz+vpXo|c)bye5)T8C{ z+aD$b?~)u6@(*a$0L6Wxg&2L!&Ri>?=DhRW@}+qAC+7nmEx<9jQFMbK0_R)YP1?@y zfwHRam9C;LkEghe-`4*&>vqGe2l98eWZqRM7lI&qmp34FsG#Zj*}k4kh*ZlQ0F2rU zb<)!93eEG9x9^i-gHJgJZFy@0@9;Oir?c);GX8Tc-H@8|-T@2&x}QC8tOvv*y3L}0dPXbw#twIo9rS2umub|<=7F2(dln%k|p zMB!UwE;{P3#VRx%R3{Wp0_1$Y5?A&hB>vAZBoF%5{?dY=HZwrcL@^$iRQ#p=IPmYR zHQ(JWurv>d7mM`ZgCYp`V&90YBusHk$!fzUL@e-873Pai*ckvz_Jr!d7Koh?uH_a+ zh;g{#z+xRJDIAOwhHqk}dXw?1fuJ_ziMepqMx(7pa`Q39W&_GVhVmx=RkUu#@pIm6{DKp&B)4Yu8q_GO9VAsJAU&kf%Yz?m)AUH+Rcd<<*fW z)+XUR61k?M3&Tbv@GHo9uRVGs@1PuhxmN(QJMIrz!xJ=s*BwNrZY`}!YcFc^QP1CN zgu7GOc;|MbYTq@={XnirFRb2oh4}J3P<0`PC~)_1)BhFr6_mg8uekaa1NprhO?0`O zYP`0J0rp7L7qk9iv~{Z`j?rqoK<%14%j`s)=kwMq>{-iJD+$t>Uk{ga#*}*k@6X#U zX-Wqeu~ep8{ciy+ypfW<`w*WPC9M7m_kiqo*1;v<8v>+$Zi|&w*0JY(iM2wmmG-UL zbT4G{POx41!$qq@k0_dmxnx>eCWC`Ohz+-~Aw*(W>n0s-=1IY{A=G*tQ!Pn$N48UA znmVsWI(!qnXFfH0jDmyt0eyiPzQu8XKdT;#LeZF^r$sF}IpUEJBlq;xN2Mp*d}# zHI$*YC#^=ak8yy8Mpb@FAK)K?NZ`Mk2Pm{QxOAa1)R)0q)$ zmlV`SaN|8!9!#-Idf;Jb>hYQanv*4afctqdq|eP~1usDK{{V!qa|y+N>1r1%eF5RW zJ4vpBChyzYXcHEC0{Vw@eE-Y3-rO-?%vU+ZPNt7l+D^HiN6eJnZp$Ol#9- zqXQ#y%tNIWIOTM^cB~f5^Nmr3_abr5_R45Tz0X$+b&VVC%v;*@U^T3Hi(#*{eW5L% z`IIlly96@Szd>_(l!JWVG45C5T}fxDz)sM-iWH=JOaJ2jucLjQfWYOy};*n6@E}Db8~nwLV|#EwcJmf!$l? z{u6dSO_t}pv?IV;gueeIM<%~xXzp!(>O%nwugIRbdUYu`IbWUQ%v?!vu~XG)WlPp} zQ+Y}=ZW)F+R7NrtDExLdi`=Cl3wA&7LZaogETc0_dTS{0rwM5^Z!sc(jT!8QqWU9u zq}Rpf_=V1Y!b%8HoK^bcQkk5eP$Tuo_CX2Ik8K~|Bal1s4_@YZ)K`4-*RDP&Z?#fZ zUPlKu_E$A3ERXapp{0FxBRI`rLXHUoFv5&YlrEEQyb3MTp9MWH6V$d_t%=rlUm|8; z+zZ|`?O81O>pS|j2KyF8V9a&s#O>LgY0VG$ zwOf}CsTiiRtMcQdJw3Ft*rFA`+mbVNrL|P4m26kc49*isESd6lF;n9O1|=>JUFX}V z*fY1q36n1sq2lJY7VOJ&GVQlNPBYu{kMcNYj)an9bL6k?OtUAOzCxm}RB8Zah_L65 zviz|{ch2R*7z$Z^p2bJU(Q&mH3-P)ikOw`OsoG69lO^zeep;D1r7|COQKK zB7@7Y8P}?FzL%^HP@~8kS_ZT0EEXhVtejS3oVDgetLX?;0N-caZfPhC>k&aOjc5`r z+CjOxW;)?S)&kgu@Wv=YGL1I8!~TqVokP6GJp# zNZy30@N042_jW68pKSeI$ZjQnzh4joQQ9ggoMi`W7T3L{9&XA#K*)E-ZWx&w7`by| z%C(KM*fP!a6jP@9P)g%U;Ia=T5BUy1NS>~5fH%cJr$^@&s$%`rjlQOdcys+eZz6c* zB1;0^^HOm~ctc(pA6m3pwOc$48MgFfIh96)9!=YgwI4*wc1<%YbmS+-E)aPJPar}< z$Q~E-O4YHLxdv!27haKjJv6bjW~Wqv|( z4Nw`t-Oh*cV&@vXw;f*-t%M2ao9MeQB(przl&!H&g~X#d7YixcZ`3E;xUBR{EnzHM z*g`FQly!Z(iS>wDOGy~!Ag}|B9tLHjGV)JIO^R+NZ9=HA@N^zOV?HeJ`bXzZl8p z2&*;7hRF>}Z1;e7hJ)n}U$xBLMwy3vT6JeW-Xi09YdosnH_KeXy= zaiZG`f1}ciF5&H`UbDWaTp>a{3|hX+LjYW&fX{bq`xSrR{6f?UPN7$CdH0KEEGtto z##L&%30e)&Q7W7DFseHwxsP}a>Ig?tWY*G2sezXg8mi{qs;^BWj*h+E-lWP_P1^Ss z{j3&D;Cq02KMo$M%eTyh>Dhv3=oNVONbNlrsCfgXFWYmM zl!jWbF%#5fZ=g;6SRsWBPlnC3#dFLilA24`u9UrOvhzmO9lmyIT1kj{t4X#rNT0R@ z7n5S~;54#aJ zE8~O7Zs=$JWD_^K_0m=(Hp896q&qikQ&EVgN-rF0^F=h-kBtN6_A@$?xM)vos9uj^t=m?|}St=Q+CbOcpyH0fo z39C|bxhnGy{r!uO{Scim6blllE42hhg&)OG3Jifum>QDzx%>k?*2PvieXeA&p2OVy z1AYJg^Akc&{_(kF^h_RfnI7)slfi?`np~t`S}@OxHcuwe-6EqO`5~xLVPEA-j0~+@ z^S%Nqf?OvZV(YPFO?BUjDiR(Em2A(A2i&f&dajYIYl}u-ob87pllA)YOl2!F({I=_ zN9hmb<*v~jCv&?zsaqP1Z){CR_2frcFPQc1SSl_)UiBz&?6`^+9nfLVJpWTuyt+G)j7DXT51Q7Td{KyyC`RPK-c7 z$kk^4mA33P;3caTWaT$HfRyFLev;C??){X@ z)mW_58^8$d-eF0TR!nZlCfl+}Vng%QaNUm^Iz9AD^HF!Yb4q2t+fLdT<3L+BL-a;G zW)oJZZ#H{T#iMFlEIl;rFLu`lyFc*EW1-0vx?&-+P_1;bSm6vfvjAxd_y~6Ei4X6s z#cj~=bln*N+Le8WNXluSmzK0T;Hy>zEBCDQp?!?dPVifl zug9mE9C?+aKqDrcaodn-QdCkLKfSeJc5!sVqeUH}=b1ww_`B;5;G04v z_<3^Z@lJ>j+!m!6^@+OT4|>HNXl#2jB4_Yl*iTyR4H4IBmN%PZuWCges;g%_1UrR3 zar_4+o=nn{$$H1ltMz}|!oMMkw#v-~IHqA~U+{MxNTCQ`4nxxVI9pUzqRCX&aZgv9 z(V!|~N{gJ^4%99#wnT{#1$I8Hd-7q5&seo>R1V2#HN;@%>Q3A($f`+^lckhAG+Hj! z{u5R{x13Lpce3m6n0RI8f6T<3=rt4b3%N!8ii!IS58Pfd-cA})W7!gWS-q>scD&m5 zRCjNiX{9zMqBS*c%zIllT^f3(2;KR@-ZIOXqwchwQZDT`!?>jwoF$7Y@K`Ur;#I=?Lj3O1QVsXZP^ zq*p4<1}$+}>Xl20==8GG^Y@)@%TI`kdU?h8wbI1-@O%v%s%1&S3cB{*gDD;>XFW_ln8QaA!wtGy)R-IW(;U?0?oQ$U+yU|DrEKjEArFJ%BWew7I zBX=!#`jK+zj=ABX_cuD*kA_yA<0O##g-X}2Sa$>Gge8nb?rd@t3A&=U>)4QmtZ{Ve zoQa^Exm)yo6<8PNZ@U}K4g<*DAT+iDD;E`)pFj6tpzRUW{9^LtXWGf+$)RzJ$-`(+ z$k~qqD=Bdcm<(ieoqtPM+y!l%%SajL3283mB6Rs_;TVLgy;2b@c^K`A3yWrHh{)26CjVJO@{d}4|Iuk6U{)kqRE1Xwu z|8AqUM&U#PUta1d9x>_hqrk)#t}rHvt-n}UD&K{fTZA4sn~fYU%PxJOf}jb-y{h3#U0`BrjJl{4ij^qkX=9Iyg zL5})!#XTO!^4{}X9i7>MT=Q=-^4Pa(^BJ6uOaY{^o`U<7k%&=RsvJ7puE~2PIWET)F%?x&^Q)7oRI8Y(YxH&%V{b0T`vq$VFuAqa zNE7qJ?eX}0HUMSjD=C!bO?NPi+=7~mT+Jbe?W~^NBdS7fhMY9=C1jd^ACdBjuX{{@^o0STtE-h zx?)+bm!>Pyt7L~s%o8=PH>5N&#k_rwTUG~3U27zy+FUG0kQrb}b+zThzMC{JuHRcK z=85U|`*UCLYHxpw$lw133*K&N?p-MB{>vfe-J($%+Ps|Qljp$L6ts(X^ess_`N`JSk_Owm<;bwor zV5r}_(%T=X<8w9Z`w?&$O{jm_E`kGy>;uv!52XG)3h-U2M-vaP$>tjQ_7dy_l&yGv z*tQeJ;%gJ7Dz>RrJg+Hoc^(kTxZbN%li6^vO0Z<@HWR{Y2vo}NslqggiNF+&lBJfP=gMQXSw15 z@V5*0O%X^uHfW)b|ABV@Q}91voeJYC9xVj;T%fm)M(_tqf#?-0fL@_;PDJJf>s5`{ z4NMCcoS$i}N+;?=Al#rT<+3L^C4N@QWL1Tk_o}WD20dACYKPJy=EPYep-0xxrq|fo z09@;&?DpAJEfq9|do}F-$({iF@NCKdq|kH5eR=vm2D<`s(D{hWJ8?!1!^g(OJ?Sfk zUf(f~?Y*+aaX%4i1RKDMCbZ6ZTo{+Ofq%Cw%Q8i+;?8J?9%7bB{Ygeoek|4&^kjIF`SE zOQN#UqRTmr3Dw}2UNmroHph=7H)|Pqi|hM6OEw(F%OrD7hCI0kl=4)MW`1={PsnAu za$`4TxaNl0;5tsMS>Kv)(OYNw-5-!4iAI zvg%dq$t}WpuR&E}AXv0gei*5Z)^%f5G`Vw?DMUn0crkRu(ea~8^*u!AZgC4we{l)afpr4R&mk$!L$UH&p!`W; zBnOP}jqZLTD)alhkoAh4n6Lhg*LK6syF>pM0?ikKAZC|0Zv~hCKZdIJ6HgyW(*xF? zh3nldd`A~Je0ME(m0uMW{QneD@B1#iTND)E`P@;s+S+H;dfMw?wIOrxxxdyb9X7*B zQ(*)X6PzFIh_c$>tKp%xq&7rv2FGEgwMlD$EZ$+I@gVUaXg1p{QZtst zsb87QxrxBj($eNuKc00z7-&U?;f^IJBJ^ov9FRhcfPeBjmsfFxSR5_mgTwbJj>Si1 zsYkZv5r&TlYD0S)3R_G1xS52rPUegmYRjzv*43~YolUaM*fM7u!)RPix(j_{ON2?K zaag6)7w+mORu}{led7W_g3>-`G5PI}BSe2u*7Dp(O}y-A&fT&3S85rLPrm53lz*ftQF&%1&LH0Kj zPPTdkx2i8IzF7CVbBT+MEyZ1=?XSkko`bU;pSiHP{!T6e#Vba_ySI3m>^Y$=mRC-D zWwpi-YMIa-QYNa?-o8pTn(JAqX6sg6;1x5ScNW9a#;xHnzB8LR6INuPjX1`V{(3BC z`%IqULlaDGs{3hV`d48oMR6O9Y- zs#1wMFw1_}8SJIr7_#!?eQ86+>p?70&0sgT23tUWFzuaGT?|EiS6%gGm=>nB>ndp6 z){fOFD18d;7v&Gn#0}1dqwD#YXZghYJ9fcz^mA(aUE61icIOsVpQ7)+kgpO>Z3TKd z45`?VYGb!E2%NsS->wzC4$k6EG8Y=VZwy@x27F7ic-?D|bv`M#CrdoRmcwmb6sv5MfpT z=Gn$W%$Hwi+PNi)mhk2-pkq;3;kk7Lop9#5%8I+0T%Qo-q#W{5HZ$VGdIhjrGHwm= z8lgyoa4Rc=?K(E?R^N2}n!6F#TFE0ARlLCJdov& z%_Cqzf+q3g|K{b?uM8x+EiIDL78f*5;+8lW`k2#gY^FP7bBI^Os^0F~BWEbCtt1$! ze6`azqPW`avH{HD_OzY7#BkB#l!<4Gqd_+|AzottlJ{+UmLA$UR@EPQlM-I!T%K_4 z%e7T#U6Z4SIct<{J*HsaRt1QE7piAYp6>&y^mUG8kjMjwg&IzGMs(!qYJBMUn{usl znDr`6hLm}0M}$UifsbH#XsKUqT7fk7m?mXWc1_|ojVZMxCfiM`(Prvl&C=I+m%i}y zyA^cVv;Pjf`t7$|kf6I7^|2v+a049h7IJ{0hdv}ca%wr;@;y-i4+?ePJ0`5}sHSQ9 z=kZT+P=YroqEYOL1LX!zs-UUVpXkX_5s zjvmG%(IS5sWxpl~VsrgAmzh`5^)}>2_9Z{u`kCpuu%Wtg8*;@}n(UO?GiJQ>_mErZJqg$a1G7*C;2Zwo&t7hhx(5n{(r`)4mDQv)EzY zq=0rCa+1!mbQD4R?mHvi5J+#=0#`uyCHZ~ zvGsJ(P|?WUF@uDiDz@C)%cY`)QggwkUMx|DE4;bwuJLh0+XKkV#&;dx+Uz2RGBpO~ zqd3z>2f$+uuhbarO_+X8RyAs-HWn2km3J)zaCMrds;V1Hm0-L=HeQW}4?Xbzc|BM6 z_u=GskaYA912I%f2YD+0fSLYmUGtGe)Cv1g;R0M&xr5elvs zRh;J)M;1^}VEjX?pbO#hTq^Rz?k2o{Q?64~Vxj__Q+>`4N(}~LUV~sgzT4gMBboeA z*FB&N^6=m);yG-G=Qdc)Y*3zDt{b{KpKpc&If-g|eMSz3vyDx#Zm&KQwPY5NN`kYR zH_cQpo_A`YPHjwyDW!W_NOYmPs#FvMT;ceTu;uQLHdwLN4?{Fh)FH~LoVMW)+&^L* zU1B*8I1RuBA8xc;s%Y2UUI7jxG=%3Irh+mfZfeV6%M|okeROhRtSoApYsLUeg*9FE+?tr3%1to@RfRG~cX3;Cp9 zb|LzOddnB(w?7_OfTvHQQ!^0SY*$M3Sex;^#Go)BFB?xb00=)Zz<|j2D*-}zPS7B^ z_00d&bQC`;K1V^g^XK``1s6Zmj}zmHpYSvs_idHFZOofrb0;R~$9)(TjV=_?M!dX( zHdmzHpNj{tLH|gj)7mVZRh?Hldm~~g%-eN0A}dySoKh>1Js`&;Ql>{Uy6?6d6z@T$ z!_@6}ranjYhC&!acRQAjVt<`QHJ-d0!|?48eX)H~T|sbadAT~Qj4r#6&Mf4oe>idT zyd?K`xZYOxpXPDqS2H;QbB-WoXJ!yex*X#Ybbz!wFoZCn2eWYpYtBcpf-T!jr@6E( zNw;I8vlAxM8owQ5t|9nC78i7<-4CG|bq>b2)^0#M_j3%O$6=@QbDp_}Dx)CZSXc&y z{(GY8{&`?z=U;xm2DcmNyp!+t4(dSu(B&(Lfd~{Ya;N!Ktc;u4&0w&PQ(KbStfZ(4 z0L(~hb}|>59-VEmpv7%wFi{F)0G+7f$qh9g)K^223hh2-_NJ{>J<`Iyl2Mj#Fs)UR zY=;+x`ri&6D2X2&xpLAFx~|uW+Sw+1PkX--#IwYn_u8FA z7F)0Q)o%hxe4~1;or?QF`8=v0c&^obf#+Q{nDx?;DN_xP#H2bgoy+aYfzZSxuG-2p zWBPN;6-{#yxGk>=!wwo{uWnEtfKAwwSPz)U=w!xH7y>xyoB{jcS2U8W_+gCT!s5_( zPQw6@20x$A=M7y99z560qk+MNjr^@;>0FVS7b(GEeI^AQ`gWSlg-*MMk?G|SU?^m)}@3C-Ovypx$Lcj8@-?3O4p0&e&Kp4#iswzZioDY3Fx zcQ}peMXgRX8)NBWv6uaTjhKd5SsI%@#gx|N5=C?vJ_zCpsfLw>Ws+S%QEL81B>P*3 zYz)_>Bnhyzt6#L*H#^|jM!vFOj`kWX%2Ur)HynU#{BrdephjZ^WOT<>R9^L=T@`*K zn{UP2J3;L(3N%08i%Hl>O(WdRN0`oJ%YDsql8(J=bir3qrsIeXp=l{>Zj|YMu%q1l zNEM~7oC24t2I7*0bkv?DgO&ce{rAH@Z2azFh;~4ml#vxn%HrR;ZWKh^P1APFqKz(hrZ3~eRE5h2fx?EM&qzPi6*V-tS6e7A~90Tm1c8c(^g5WHW9jEVn4>Hc`I)C zj-y;W=Is!ryWNT+2Osa&JIYsVy}Dl>mUy*imqyuJ=n#z14>s3NN=!g2wk;~q0=p%r zG@!NB0a|7`!a9w%6^gBvXr*###|*MWNw$WxnC)~C!&<93AGP@Uq}#)G>f)K+R)hAAZyEj(Ed&wj>lCmx<`OB78-_G^(i`Qyo&Qm+~) zj%otRH^G8>bf#U!LQX&96-?O;&C3!N_W(Nq%$yf0r)!(TW*gAtL>xj&oqd#0Hnyf_ zbbOfAWUX!MYC4=u#$`$3)Sxa~FgIf&R@_pS4C!){ki?AFdBa!REum8D<2^iUF+T?E zep1XZhb2cMz7uZ0(AQhejERzM+7)bw zGud)#=?2${H-qV-yp=law$Cc1Z6n6&^#-=)!6Dg{xNVO){_f#8TWA(;Y(+KkxoN(!g>Tij?j9Rxd^Te1mBU1S>c7O zT}k$Blv+P6NZ#^%bamd`2#~n=)qdx5gWc;YgyV0@xALRSRDf-K=&BXMf)1A}*~!nE zoZJiX2^6fqa+gueY_e#=?Ww>X2$h)U24jI^@ZE;R`E89&n2H9;zjlC?cBTZ$D!wBJ zyNnv(<5JD9bLnoIj5?|()hDa$s;B0Arud-={zO4NZ7V(XiQH{KII57j{vjiwcPHl{ z{EO^EbpAiLeCXcBo~3KiBEuO$5UrS{!`ZUb=r*#E5%v!(6MGIDYitU5R~y4fZQI)F zRb|1n%+hc?S;w^LkU~>)8<@nknmiLv|8BUe=Z(3co5uHSch7{SPb z==UJB{qXnL|9w~c9gzNi_x#@-A8H=L-;ae8&~a4k&nuO1X}f!T!kCV)e)7%mIi-c>&y=LP9B!PdC#QNwn@)Z;@s2 za$6BjZJtRJitI_4)7ne33Ax_(D28LJ)3U(z`lBkzDC?*cQdxT|w>nQo383Kz$!g~|mlu2d zvS`Q|$bK}-7K34~j?E2;H9ddpLlhwwE6l@%_i+lSyj@l zIu30y{xKWy6I=F=&f^MHK7G7j_Bj*wpo?Dz5=-)8c&KOdsmk(xcc9fqOr_kDsAyAW zume%@mSdhUZE;A1LAa#X<|>+L{*K-@RZFr*f}|f5sa;x6l9=*FzQ!@jTIb=j{|q)B z=`I}3j~>4cxtvcQKPQGbkiUR<3p$aX+j*9=rDDeL$}UD$f?8ufQAY=X56NEF?x%ij z-X}HQCWE-PRVN#FaG0r!EvYxzHEWf&%k`$;+Cd#K$4&Wky!Kg{G_%LU)h}kmje~gR zK<*zF@4i_FPdt(5EzMC;opra-z)VD7vmbdPh*r*8Xw53V|AMpHqE<^ zt;>SzuMhS9fe}~WNw)bthsG(fsZ-s|M^t$_fLgI|H0(?y&F$4RgPJY7d!_6xI$FsK z18{kbjxmMF7K`z=xd&{G<|KKA3iV>IezsGOdglQb&wA)>xdIi2Bn$ok?@-;qbKWJ` z%C7q^TZ+RzJ|XAS)|SL6zzZjObv*7#)L_nz_cT8+IJ!C;Ok686c6MoO?uGq+w&lqB zUZybUVv^JaGxI$^7N_b%_vEroZ!Ggk=U$f>7iQ)Oi=UI-_?`^y z(E)-kf(yF-%O!lrq6NPs3jjn=Q_h2uimO|HcX#;eg=D^rm#1%OVgT2I_Nb=Kcjg)p!*}#fbrGF+6*mLtyuJ+G8BpVK z`gUYKb8qjRjr2kt2Jk(BgBtRjhdJphAN+%jbVam~lB*YT0eNM5=-RNerg+b-*9L}F zlk`ngC3p4RM1{fhhgyHM7entsUdFU4?v|L8ZRuvbVR$g7A0!|c)Z+XXVwHV z0U$tVjO9%jvJji~{&=^k+k=^GY{XVd^W-R$47F)2oHg=|^DlE#>_@BHYB z7kmHl%A9^rKKeahfugAOJ*DxqQ|~+JJKJ(qKKr)9II=&#-7Dtj&+{X#SL%W)l@vw~ zNreVYS7Bs)Mh%-_lqfO_h^Nb=H6N{=rn6bU5wgPt4l!{&UJp7 zu7ldBoa$L!HDw8Z!TKjI=nD=S&|&v5<93w}c%OGUwVZ4Dqg=hd(kuRSF=*p6wd{?^ zb;`ggD%d97_Twi9K}fyC<3sI*hlRR>#yI8BaSY#vSjJyIeVu1~#c5G;Y=lcgt~{ z)wHx6OOkFsHb0lh=kZp4;(0g{0GlB1!gt|szRlscUHjhGKij%AexCIs!ePu;X{8{+#?!#CwC z{Yt|^!DxG|ZDHO|S_$CL1|w`~7mUhk?1|)#>+RizuG;28(Ccou5z3n~&ei8mHLNn- z7K{RE?cB-A@3aArw^=sA)_`T!vlkZW@k+~3eE^6Eu;Y#@;+%a$ne!jxn#Y05xd`=V zvJtAN7CPg<@j& z+U3xhW6@fr8$-N=#i2Ml3@j~cG)GIPQt$ieaNgqQjNF^yGtVH2;YP-ZXz1Zvsj{;u zBcAHp*lkMPhauvVv{NyD3mWO55&cn2cI8pLZ*=pKUIopZFIUgsQo-_^37T9m)2&@Z zHrN5CFIJmrX`;~$Tjh+h7xedWZMlPjLe$wzANp$!_n9mhEK~fjEG1C}z-YcE^oL@j zN=cO691|DLTzr^+Ln`k-Dx0pVPYN&Z8~;4wG=5L>DFeA0mTx%6~1S<0|=d34*?fOmXF7zXri#>`_;M{|o+c zp|~_JtosK#*E(lszv0xLP0f`qIeSIVkI)Mhm+UPB0Uc`_k4Vn)&r7|rDquK~+Z227_&+JW?X;g&H*zHw!Q=``^=`D6+YaBD5 z8mm#ZnJ8u{Tl0s-rrgjLD!?ogor7wGui#W3uGkYRO~8hJRmQj%p)LE|di_8tK(_J2 zguoR^#NVr@pcqi{^7&C{qpA-8hNkXuhp9y*Yq`?%#g;Q!4^{*=S~#33ucKIE=lqcN zOJvn=9cm`CwF$G|=}9=}rH$0%FeUNYY-0w=T57f)yg0CU!o;r|A-7!wMFPd=Usq|q zAG9sP+MC-m3Z4iBU*@=IgWo$-~|^u6+!G^;w+g}_OyJk4TbfonJV;|RmLj{ zsjd6Ma#S}Gc`Em3-GwQZrHU`Euz_X?db;Yb2f75NxN9rRN^PD9E*@>qEQ z+7+5&Q4CM|?H^E!b(!jZEgZRW$OYux6Bh)>bdpXO_VnS>pktrY#EM7DR7^)G)FRwH z>NLy4x*QSz|0x3q(-!g*Uiv$H@y;&*3G?N-qI+JltJi+zgkLOQPu1JB?*z<2;`oN& zuk$ORMMC2#JBlcCxtz>ch8WxOp}LwzEp6Ny8WP_SHnW;y3GF$wp~@YSsJk=E+EaFk zE(wdQ|1fKKF$j;B?=oT`Um#Rucs_AEl{O)8naW9~kJBddtC6H05j+b3kxnqBNY@~z>Zi)T^RNDO z0w#Unw&X>Sf0&;q%C~$yejV@^3M^kSIBfNn&}}PcKAQ^b!Ma}86tC+}8d$)*lb1M$3w|o;auH7VgU3wAq^aw#{n%gb7GEpRaA(&XxcG8fdvHvt6N6vYU0A zE$tU`eMZkz@e(0kxiB}X;0O6SSif`_$oJz=zAHEJZLjW{CgRWa^BjZhm9JdVhOX3< zcyn4(us$v8{Y81~&1P#!n2n__UfVKmbDU5WI!>l7oSW@-C9^zXvF>7t^~uV%ZkS!O zPq(KoOch_#{Y}KpE}Qv#5M9i%0^jGlMK7=L#n#q>4 zE>}-C!jPL{u7yjz(xkI9)-}rKbZ@w<`O`kkAZ|#V!Dh(_<{Fnh%j2}U5MZsrKrx+* zN`cZR)`m)W%bMqF2*35xO!(98DD>z<#q-#IM73IQ{&*^=g8Gz`ibwt%3Jsu)UvzT} z-G3lNv-A6qaF9a7QqZw0y35>1AouHNJ^|cZ%%M6V4*@UURK1*Tu%o(AkPo$%Mqccn zt277|&^K?>{0O#w(ul<``+1J_2Ok;%UdTyuZoaMfe&}3q18C6|(N97wM_xrC%Rl)R zNBOyNdGGjlr?9V>^{!63B?+gEg7qRC(M<10x z9IOHo?X)<*9h;?*XrY|Xr;gc)Ua_$Y+EBpsdPb7tG&8+SK~s0PRth_YiD%`Rf=8Bt z!Eg~H+vweFE>@~cJfm1iwK%@r9?ZwARo_{|GDFcbKz?Yqh}~En;dH}j?AN28;ZvS$ z+9wJU4tbu!zx#eZ(f&^AP<;C-BY_m+A^ge1<5v>apbm3AOYx?WCJ{Gg%^~k}Ly=0H zX_j%jDb{QDsx8dp2M#sgkjpuMfq6@#$#}7k!O-d%)b;JRhstoYm`FOu_9UsLK9sN* z&emg{w}-AISInnH{|2CiNMq%JL$MCeSpK}6>C&}y$MmbBz<0O$sR;@({zy45zI(;| zSv+nwHf4(Q1{moHLpEbZmdKPWs$#@$<{uz8v6V(cX(10gq&yLwIt01&V zyV~4^!ko61IW;ct7j4N@9y0%un|Q{v7l8M$D*-_G$)JZ~_II&1?kPe({YRIuUpvljjb^cV#lC1q{vq zf;9f`*x>dvC;!iFYs-1!tD+~PR?$02LgC`4#}HQ}&pWjEhCKRw6Onp0?0CtrWKqelk2shYFJ~*2)0S?hKa8E>0@DNBstR$dEg5yU4})xInD1crQZ8L^ z<$X5O;tSKRf|84Ac&ydF)y4li3fi8h?{0tojfT4=b|;jppvT5uoSGJo!7Is^<7y`1`9srmvJ;-c!sgb%6~}u)IA72oR!1a8WdXm1G&WK zxE4VAw9vBhp9$R+5Q@4EWZm)$uS)t+mpU31BSJe<{vOIOz3WK6@FO7)w>>gezUsTN z#T*DY|KigH$JF>$p!6Rr`#<%{o+ai^PZa|6siS)o!Q+N3sOQPaSH?i7)K}cRCm&44 zYrBj+ZA9UGy+4FuRCCsof!kFVFbB``Do&*OFGIiIv`Dd~1MOsR}H4oeL@ zE?myDV5uyB?w2oq)?;IUw$Rm@-StR4F~%=9(vcxQZKLCA6nA&EiT-0P3eET@uLY_W z$~)GMGXw7SKoO1qfjw|T`Meo!UWFo3l9L%W&|NN3(ztsFVr!}NvrVP~%GgCUIc{V1N7_jGUQQY%%rf(iibTeNX8Qhy9< z5qUev#{BKCM|m$8bjknce0-~S#D{z@ilRLjeDn0EDe&y@;anfY!%r*3b7IA+0M*=A zeN_b>dE-)x>a?JNf3exw+*plcJt5^*v)n7ObIQ|rvTjrBq>ZOk=osB;D-j3TA(Xqz ztr)FwfXCVz-7FaP>roQYSqwndm*b@i@BS@M?-m^VX9jYP0ka114Ou%-EjWgPq_+C< z6y(SFA#luxhZ`j1E^hK3ozL~D>cmB;I&);#rw;nB@}UNn&mSGYfadqTd+uwLezj@^`pr z)iyHKE9Z}LXA9+U&K?^a3YcI&V5)><6_t+&aO@aP6kFwq2|2-5{jjGVPH6q@i12VK zHo87=L-m)?pACI>WbV8Cw0kl9}`Y4Q6MF)k<}+pHBu-QZu@p3O2P$O|w5O3l=w^+x?WV^~1#P z4R_sM=5!Y8@q7nSCa-4$^QyE1O#c-aDrc!M2ZnG-jMYN$lmvBB>CQujs>6jSt|tK@1Q$&Oh5@I zhY%3Y;3cOd;{Q2Oz#1M*SxXfzR|XU=zh(g~M=k0{@N^{ETs9h_}e;Mh3d ztS?mt35W&Q*MGq)$H7C-E8{NPFfTKFwAGP5{#31hBYV5yi-N|S>R zDh)SvY|rfbu7WV4ho0dF-h!m%Kr11fs6iF|&lTQi2|vsf-V+AdxPLM4se0uO^O7@5Wp3=R z;!3Ns7}jbHhBCv}a#U67?d^z;OfsSS*2JYIEp8V;_9m!b=47=?5 z?FpaB(_UjhT-7+duhgCx@UQ2`px^h&P3lm&j9U6>Uw$~l@`@s2`-Oha?^iP_HzXNy zYD?>Fn!VLNR4YABO9vq_#k@B3E+o=o;z#>s*>!4K6v+ue@zp&WHf)SrT2n)>I!hsv zyLii&f>l&cDAcoasTg{n-;a$5QuoLqDzq|4X5|zLi+Uuv`008NGUg9klUNu_n%pBKoC0Lkqs0|AMNd%M~!EKu-NCvD8NA_ z*Uw$-10%1ob>CFBC9YLTcAHHXrm@Y#1u$daNcyQP3VXZp@*XhkbJUU8$4X=8|aBb?|qtVj}SX=vZgnHIA@NcYvG*>ixE0b{!7qT9jBM&zJGDm>6J5x z&P$sPa(OL(C;oIERw&(;zy5nsg#DJI|Hu;D@bOM6ksy8O8(awHQh8b>$kHQo@?0t_ z4ci`Q8lTmZG2U9137Klx4ZGFobutID4Pkjm$Rs^1?bK=7(=_GX$wJxCssT{U^;Rpa z;kBR|yN%6$yV6#=-Jn~K0YBcA$|tJ-yl6iJ(oc22W>_%jx-h_K@D9k>9rKase#rdq zWMK)uzw{P}lD^{@w(ihn!kBU^wYrtOK2_n|(pn28qQtYr&{}sSU+lNyMJ*0TdZosX zp%Fz`xOP@=)O;UyFGzA1RZ|9?gmqfS)d{v)m$6i7IaNwx!if)BgyMADdf9eK@GJ(iQ`?d}`o4tKWN<4r$4A z@N@Q@&k%dx#SmS7*2@|IU?iX^(2H;&c0SODAT21saPqpD8zuSQNiuTq>01x~Zet=h z;zPgqJ6VvG?=OLd1hOwQq={w9czfU%-6koj>}=&m+RBlt+OS3_3r$q@)Y@V~bs5dm zc?3wrLa8-LIdAJOy(ZO5twXe|N9?BS&%*WIcYSk(E&f$Pb7_@A3w=y4{pGhSVRQm~HbPhQvc%DQ2bNi`*mNzX_Ko(y+vTNNm-Ft)@|1+{rWZ;HKx^x?qEBw4KQh!tn&Lx_3?&> z6qyymiF4sN{rgp_y~s@el66%!(aM3(fqd^b7wa`H`cq43dEqZvgztuLzT1hYyyC;1 zc&3Mz?nFDUxqr3y2JW~yn>H(K9F=y9CKd+E)k^a8Qs44CoQW{CMs~YdlbNi?lwy{d z$Phbxec0O%YDCA9dsRRMw#hA9kH)yD`YmSbG#>iQf2guF1!a9g7b}d|A9f0F{Ny{P zqXqpiQn?TWu3w(OwgFNvR9=bY8*a0$n3XlZ-!InP`o0pn-DTeoYU^1yOGY!mq;CQ^ z_S$Bdairm1kK~TAb+VYQG!M-fuVtO63q3Ns z^`vTM8pWFQz?@dM$+T4Q6{d#qdwp8v%F-(I*YaTulesEuZ@iqv*`JqUeC}nvHkik? zzxcBkAMCq-kvmscuFGGFmaT6q_uCm;3~~FoR7JRawT5VjMhtLfN`cahpr+6ATL2N< zx==v%O;LPaRPoeOe7oU0y@l@oLt}AG@+g2OmgLg+%4EoJJd9zcLkw2i7F4$DFn`?T z)E;ZL*#oEI5mA*>Mdx>WHO$18wqNp0d(+2_re0fwt8BG#mf4!+sX^Tz36o8|8g3eY zp7dR12y^MUihcg_n{k4n#4n%>a3r)B{oD!2v4-Dv0{)q0z4lA)7WK+adDm~dC|IR2wZ7f@J7mO<(x{r_2wm#--oxS)}t%I#;*5^p3e#JN|vbbct|yDJD~uQlj{v%y==ZY7HNE}E2=#!}Hcs}|O5JAa<`ejttKjLN+<<}Ao%05u`L z?Hc|Q=UeEo<{UKMt!xoPe&i8e2*Q$Hp5(#zt8Ck*y`S`zXu@||Lf^`o4r8`UQ>-K{ zQ+hJTtzhfoqwW%`Xop_6q)0|1XfaD-rZlDlqXT(2!P7aN=r1+O+sxRoHyZ?#KQD1| zF+*sHj=}evF4I4011@pn-5ULYipV21SfJx0$b}*s{XFq6R+rtW$aB3~V?S9h1-scL z)Yz7%^Quv0s+C1l={wC)$tPBw=6>E7`-6Jd?Tm(AyT_S6v!L|KfLu(R<=7@%iJCd< zstbA7Vj@z&F7zkwYI_yYEa#j#Qt3qn_a-Al z^!(PC?QI6b?lv08<)OE8w5Hv%CPARp#N8}97_(-53KM`NsIifQ0jZD+pA-+9b*oug zOXeWZjp-kefJVNRlRalg7uYfGSo+H%&v{Kt> zdqSmC;}xHt^|Bsa?Gi$JeQ>%pX3<+3?RIs}N*=kEhntpw)l#uR4|alTHYT-X(u{ZA zLkaII-Bxo>O&fFB`SbD>ck@EdN&BLV>i@@2>zrSM3ST%>g*iB&ssLZMsC9%) z{p|_rGhg+UDRCVy@S#H6%8zI77L>XKneT2bD1x~n3hR0M0+c4Ae9Uvzba30vXl6D1 zAe{6fb>Q1n&jwt)D5_!jffeOJHnk?@(lAcsMhQC*@i2|59W4s#Sh>T2Iyy{ReqZSr z&UW3MHA<5xzQR$tQcbV=*3dj6%_`<-9!nYCvkE1GI^ZaFZD}h%1 z=tO?J5|6*-dTAbC0nyp(WRDYIyuF?`w5?)pSy|;&a@HH0INz+7_8}*d@_eVxDy#Z{ zpbm{z3-=auWrn*{FTwyLEN#klpIk*zy~C!wM%M9BGx+Dm3?8fV3ODCc_nou!t(y7Q z^A-xElebZ-}etdEv z>rQMJIjXBOb2vQof8D?>+`M`7&Q+j`YCNIh+cKo1vwWoi_oiZar?dz-u%{}JiwkwC zF?w%fp31);8$YxV8cHnRtu0!d56`FzV#nnL#^fYktT641&2^mYllrnnF|)xzb+QA^ zIWF1gtmVBB!w8mjlr}l0QX_hOpHs}09xcQUCmgi#)(a!G&-T;GqDfdKBHPu6@buvd zUsT`249Lr6M;_);*kS$hRr)1c0JD+LmqZwGSM!AdL$Fvwl7Ew6y8|bhioj18T7DGWs&DH#pDl^Ok6~Ru6r{%bxtU zVn56BnY(w~xym2EYfvuBb(HCXI>_~FlOFEWgCwiX);1n(pn}vkiC#y5BRYGzDK#7# zrcMpA#Yz);hJ!6>7<^9QHi;?q4#8Frmgr`e+<)nvK;(Y*XT^};sDe~{d*wfz_y2CrJFN8g@c4{!bhoxww)Wl4ezMiD zsHcZ-rIRa+y%R!Jj~XWFL=##{Couh3X?M6a?iy`PU)61}V?l4S3%NM8R=mH}7ej}F zPM=-7GD~Vnr`qczb$*X0_K2YXyMuYS(J$)oNd)xN55MQ($=SH$Allp?8k52%BK{S| zF(>iD{70(;OzKFr3O`JUxvpS`ej^E^!?u|YxM9;cunRgk@R-9ccbQY+RIUTy;d}!i zJt~;0QG)N59fLnex}T|Co+lP$H+xv`a54Y4IfWAk^731Gue@JT`@*x*4Hz){D~O#( z-WB4_r{2|>AhJg1Cwb8FDl<8uS&Ze|YN-=fNepiYLyKJD*wU{_Mvbb{k=3Usa?@-P z4K|yW_d*A6lCyLSO>Y~{=(YEuqa@LmA(y3SGr@;=%}Q1mFYAds!q0pT;}#fpn#bd) zQvQJ$p1Yae-N+Y$h)-wgz$eu7{yg4Tb7ITnr+vlOD(-O5Uo~7X#^rfQ+Y^&ry~%P8 z-dZg6ZOuu{Xx}K!S9F(A^mLL@F#Bt{(4u5yH2tdYl4W%>A?UdDxH$MCj_>(4@H^B* zl^^lwk7T)zu9cfG+IeP>4aOTRYnsNb^x9Nr4AbTNY?;Fyxp$GH7v1AOxP6wAyB&A3gJ3!9 z$5Mm0g$EHsTtR;!L`4){$jr8-Z1CBxJy)X{#~4Xjcf}VEr|#N_@0s%h;y3juLU%>GXZwXkt6Es|+hIRM_KSrMPJs`f_t8>-hVx zysR6|mtT!8gjdVgDb*)z1`jYN{^#*Jr+8*KBL5!OJB!_N6(zDc#5LC8=2B%qQ|x>k znEYIfH;Stz-6S?xZ{tizTI#Jj>-Cbg*pN^6E+?w&9pj)n(hMN;boVj`7twTk!#!*H_u4^gn;c_f*qiG^ZY)mc9 z%65|06jli~Rj(7N!-MWAeXcY#%DsdP(g8N6r4rF?r?%Q2^^NYqYj)c6v01;GzVTw4 z7Ky0GA`0L`0FWRZQ@;h??v;Q(F(lB-{49$fI54Hp4h+r6Y2OJbGPs|ns;)@p>u~vo z+~~eaUO6>?4jts?w%lQ-<9)AMSz+Z+^@g4cAY_WU)ayYPQd8>M)m%Vs8>qH5cakzO zDYq8ZW*N|Gso$minxNAeOz7pTX7ANt$R`YZ0jHW{51gCd?)ZnApFZXv;2IPZbM~E| zOYi3bpK%=N*60?6D1|J>#g>L2h))qv>*m5tG?;TQhtUVDI|YrauOp zUpQ=%l^|}bJR7sUPFGy|3%}iNigo|7Uwtvfk7f2XK!c&GDpjlIS{D^X-ZXqYS(1|n z`9ta!9X}(Q1^U7%4*zqY83M0L0`0qXMhWm^=>E3N1X5^$;SV?w$Qya?WQ{BQ$PW)I zlC%~TtnIDG61_{RT{^a$xDt_`SKYVeh;%($i}g8%d*(J}lpQy22}(oqsG+-F;U#*n zW4P9{4@6hldOsS>A5^!GK)-*Tdid_xfKrzq9zTG;a3+b^arHFMgue1U+so}@GsF!q zu7^ZJ%DhRZZ1?K5-655!YKNTrIbd@oBOKCroRp^_iul#LIw8oJSSKMI1hXzj{tYj z_&8E3$Q6r#2>&XmypT%`V;GlJbm|O8qX{LpSVbAiQ9}6^m9;(}{Yo&Utjv+vao%K%3+5ks+%VsNe=2LgVPmqTKfR z_bP^_9Y1w8>X4EbO|_VDe7v)(uZim5XYO6p5SjORz-;g0=mn+c&o`6iH-DF#y!D#T zqWYVH=7YnVSFO6R>2rCL<9t@v>55lZK8f~sRYKfin0&vL+sOylx07r5zx*)z;Ixm>?H}*(>GiqC`jcTPMC*Y?L5<*r zrGK+KA@G4#uEpRl*buaHo|{&u&xDO?Zz`1rD0x?YlUB_4EE^~%VcggtK73+C&=VA*J}Eis#yiHD{YgYIVJk3Xnq3PRgf ztzUWRAS~t`Cr~%}r?&NqBx1sd6in1qb~adwn;MrVUh1o%A&9IZdY*`kK9vxvy@xQD|vf#uH_`F=gbYR_jjct@1urm%F(mS@Rle zV=}KFT+J`nxalRE`mOBi2W1Tm0sOMZ+<*?;PY)>yaz|buV6;3t{px&zD=+-4vUK~6 z(g1V3w2cMvpvFxIDpPkuL5N2_U7xda4aV48tKPaK?u@N5NqK)Dg=?2)M!QZUqn5j2 zCR9qawVI4ZogX>zOb7oRrl0{Tf#sJGtebcRBnQ6C$tyvCzQq|hIE#?PI=}9^zdXkY zVA@9xw^2-AIlp~I!9qB0=P(Off(r`V%|DAd*5LDaQ z&EHy1;nqUDQ@z}-uZzg(L;gAeufwRSqr-DE@hyKJ-PiekbN}P`v9t366=k83d$<2U z_jA(1(I(awJF*u|a0m3OaSd$Qj? zdUUO?KXvr3bKkAFt0#H{2c3*m($&?kD(f5=^#uo67QiGLk@`Et8@X*no9l|ocaQb7 z@gPRqI?fgaD3%if7^y3Q7gZFTK}Qal7X-&vv;*ajZf2GJ9T4Wrx@e2=X#>)r$3;Wq zx6eW=sYsZA!I~c@%N=pyb80z^Wc~<52mV()L^0%98gxYhqBm_D06`KHO0LNkGW@JqMoqM}7;(hv*j0^14sq;v#rO zH8{1L+)W7QfS$C}<5ymR$wH%25op$-O6yF|-TngT5m>GK1`wh1uZ6b`kgH9v%VCgr zbH3p6lkQ8((cF^0jIQov z^qr60iS0Wdx{}p*F24xyJ6B(%`mL*v0{+&uC;5Ko;ww>q=ki?s-@EX#Dc-#P+fI4s zQ>X3o&b8;w@#e+XlQUlk~Hd^cPFjVgF*-(EqnD`=_bk;K|=veC4zj!I^m6qmz?2w}WMG!~1DzFObF%_j`+kraj*}oZHk%&3kz{GX z%9|ZMTd{4sksiu!hcOpc18ORgtb^NPOAf}AFd^%nF)D9|^Nv9IiZ_zdx(@wW9U%-4 zlZz94@n8N8N#dU{4;R?wz?8w9S}xT0M8OgERFK`m!@KK7h~xP8<0ZMn^ae##gBn^; z*R%bJXhGz6$J?HIagMi$ZuGwvuHU!VsCe;lUO)mMZ?am~|ac+JS#p<`6BAIP?b{ct8N~Lg1)YRG%>zAl&X8N~t!>Tp|pmWm}U-m>1?MM0wvL z(pkq+W2!IJbb4*HI}Jn9CRFU2b))L5(SYnQcG}P>T98JSvfq1{4!D#qKISpRTI$ww zLa0ENb(pz(^vABEi=8N&z6(_~=eN$s4yau7viXrGdvQRoCt)JI`SXV$O=m)e5|$Mv zcI55rC{tdzQ^-3yEisDZe))3fc?Ap!xNq>^wMTyw+nge>Jnaq zDbrEc4Z$bILP~b`QMt_38$KPVja4JoSoZnCv9+)+%MH*TyF54#;ROy@H@QLR*4ap)uEOWs}P}|mByONB}uI* zk1Nu+Q3pT^s@(0fHU1rg@A+SV!Ve0i7ZV#slgmYy4f~<&duRWX`B!6aKtl=kvb4yX3dtN}(Xp^Y z#yR$wURB$@wC!(bmeor8O<)1CL8l%qXKk6zXiX#(brvRqq0~3FTUH55?aZLY21Ye* zyMXxKYF{?jW5?$2`A;cd-E3}eQ1b4awLM;QtbhR*6POQ3*5_2u$)|u39#=`Xf@FYZ z!=M}$_`|2+BsVFuOMv^^Kk^LQSwha@j_&B+ucj*g<+rO;F6z>~NrT=B-SQej--4eLuJCx|27=z54IuhnnDuD2Q26Gjt^MLaelA(AJ{ruD>nONwC(Vw%I-` z2b)sWG0Xj_2@U+6fj+FYIvrakfEXID803jb!n|PM@3>@G1WbZUPE6p2xNmc}A0UdE&1|YZn(HC{HPCnK_-L zyWu=6*Cn0nXwv{wu`(?Y^~z$v5g6kWql3fwUSl3NYmJ&WYd1^NQ6|WM*Upvt54rTp z{VVoLF?}@Oi#M?T=-uPYM08wold49zmzQy<=vTi?G92y6)%L%Y<;(88ljEz+`mVom zq=xk5u?{IG@?vRp&I+havrW&YT@9;@h*~8LhC)T@N!@YRYaSX0w#P4p#=0k1ct*v7 zy`6iN$+VX2L!<5xO)>E&G1!kKb#O9pR?2@~E!f#( zITwsUJPA@3TpwQ@*XvxQ_2098>zUmLnLbH%krVm(yy{}4(KhU@_pvk^Ws#x*vJM1@^1R;m_;@uod{xG(+(ICzE9`zI~&$xXOhDL6ev zUPB>4Zx*}d#9)>3zrP9!FIK37@1rQ_bg^Mg3(C7LZw}oqUfN^oG?{2(D_V@{p(pat zp%%|q%MqV!mXWaS3-UlGcik>)_8GlbWjEYZpH&wFSFZ0{4`a_)n(Jg-e*+)w+}N3q z41(-*G20Yo0HMTD&Xd3VhQM@X0Mmv8Sv0jEH$*52aXzg*7Qg-iFc3IIgp!EkC08__ zTe1|d7qz(P*zioY;I0dZUZv_bQ0JzTch(79}DrOp}yYy?$yoAI2hKa``3bMl9|3^`Ime(1=-raLD%-*p)Z zYQVMP0RrVgg>TQDxo|ew@R^FuU^uGL9IQBB&>1Ic3=@wV^@byQ%0M?ZOKjU?QnFcg zQxD>-)$Yq|Sb#G;wk`GdgOycZnfOD^c-eQ)_yx5o$1L1q({uc7xiNrk zbSHHAkN!|yUDLd*~^s=BB)gH|ivNeiRPjjVteDcGXXYdrRd5fgu6i~&ctxdyVu zeCXsIZ$RIiwATAFbwf4h&dn=Otcd>iT*y{?7u&HeVcC`1at;!#G7_qlJbTT^V(bHkJGYC2C3sa`OBnNw^Z;mp$>BH#uZ)h1d+8sv__C_(f&gU=)j4xgVw%IUS9^jdwD zt+m%pYoFroszl-x-)i*9$`U{s#jMw32#rvrzL8YdnY(1L*}Srmb{NxTCjba2*0{1# z?zSd^*lbVnxP*z>(tM#OAF}v~^86}lmY#Tmq3s%ZAL1QLQKIQ%wTX!3%tR0JKH68B z(%q$mBoxi6o-9lK!zD6C;(q!O*@iB?i@6?*{JTNy0~4!#OHd8 zp;q;An5ii@KFmzNS<$m#@K6)HWo}Th_cJ#LOCOrj^PRPvu74o#FszI}GAAj=^lx1*`EEj|+-Z=PleY@VVN9V{9Gk^)yZ$ z6541zAT!LbtMb5aXkI%U_9I(qUBvE5iT#6#caPJeZiqlXj*fBBYxzt9J?q!b$)}q< z;OWQNWoV$y$>m%v9={8Zb8WxGXifU829QWG1}8*A5# z2kL06GH8+ zV-rS)cKoH0)JFPo|kd*_0)vZN{y(@nW++ma)7j z?KU%xB<&o6okpfZQLA@-@c&E@0bc->pFWVnbE(4HI~&nL{6&93{*W?#?h%y$SEbcp zdN3x@;i)FUZv=5Sk)tXjcV^YL=5$+_!i}{=GOFDPR-4TLc`v9rB2g(d;tD~FwkOE* zKJU9rqi+sJeyKt|3{!rTzh`wau4p2tqoVu@YRsSW@fPLxj-iMfpIYl1=-`60SKpGG znpaK{&2?%AJY1(t(s$e5&~D4P4K3z?njw;%P0Nv+ticisao%oHt9Bn>_B$hS+s;^X zJ@%J^*1;8VP%aM_^t7HB0KeP| zjy8*kH5a&2S}@(HGdVcQM79!a%A4laAQy@w*0=4trgfXmL)LIJZn5E;sU-QmoAw752a_^rDxmW41ZKC6jFHyE$Q zL#3m$PK1$_kpu7mD=pN9dZlkM#6VdLD#^ux`ex`KYk^42Q+{{sL4os!iV=}Q?0`S8 z;a`U>t#Mc4hnj+c2^S^ZTrXF9Y*!D%Xhx?Cz2-zSv8<#{Ty7nj(x#ubOEqm-0^qnw zwr>tH;ULVK?A)2@%G9fHJ}dU(%>ftTfKwg+?oOgKQD(|=;{}=fqSruUr+-Zh=%Qd!8uZ3 zKpy$aSD{4F2+}XZ_e0hH5^d$n*PYN(zHBG>lGLxzSCucQv;2aIJxT~YN{;=1eIcTI zpalJ7sEGQ@(Y^wB2`SlT%jc6WDaZ_&|h&&<<()s9R5LfiS=kB9I z^!Z`ii1r&{F=w5v+8Fs;m;~nqH41MTrp$K*bJxaA-Lj;G+OfKa4FSb4ojP5%`eL2$ z@{&Yx*1ld|_k|V3_IsPnkG{?8ZT)mp!>^RR)CKIbpAhE&neS6;eoM6A<<+-R%}vlA2(@}#uUstrH{0-ROZ?r1J#WEqma^#EIGiaTE3ikgEhV0u}St#aouq=FfzbT?HN zZ|@YQM2n;0UPxNKbR76ndNs-L=pmynq3Gi%-#`)t za6ZMb7=#7#?$|NFTzDq0w_LeB#k~2_J0UJ^;v-#uAqWd{c>-Bx)WiB*uUECUNi||$ zA1T?$CA=O+9(b=$R}IHH4E6RB=Fb`-yIqz_vldorRL53lSjWX+LUq@*S_rG6I#`hO z#z91#Jdar&M-OZX~FGZhDkj;UkTAog^_B_dkMG;uo_^xEa&S#J1No3{o1pp0M~sf_wx=L_@Q$TCVRcO>RZ6 zMEfU2Zsd)Lcl`YWXMV8`v~t$M-?z^BT0^$7uc5`rb_ia6!9n5nVNWfxen$`I-2_B@ zCWw~i{3MUIUp3*WozY|_h}?Kn^`~qY3S6hBOe3wjP1{Vvqv=&S<&v%zbLPxZVtQ+g zmdo`bDJyDH8mU+eEg+S7X}n$bM2<=;qpsE2Ka93ttm0?ozqIR zdFpZlFQo2S7*t`BAwsP?;d^sV@QS6<$uUTZejj+l?nGDZdtCVKa2xm^SdZ5 zjAY40iD6WE`8?9Fs&lv8nfU99j)L{JDl2NMH!jgZUdl2jkO8WZD?uL7+BmK zPnQ0s5>jP$I;qAo_cSbu!>(M=KSZcDHeoU9rf2RELxlMkUXMsG|=h-vV8yx$;iqF2_ z(QLpIVt+GQtD|J)+8a`m&C<+|Gh0mdnX()jEsq=s)Y@Fwd_9Z;%zy&MTF2}y+xps* zOJV3`F}VIK7ANiUI@>83{=%1kVrWr7dCE@}_VVYxh9Z7N8LzV@IoAw?f%dEZD+1pU z5S?;g_HTbhq0LP;`8(-b2&EhHxe&jAsJZ*jryt6V+2gH~?2lC)1>QknDd9KX?Ig4X zK1?e;qz&@&@bz5Tyh<_Ao;-;A3#L{LXIVBLtq;Vy=8xF4UeEfGHYPfI#hFGa#Wk6J zDd1*HGFA9+0s%^@?I$}WZA}-7%3@vYp%9e3uy-#|(PJcrhr++SW4Ji*x(XiT%czVh=qI^L#Nu8|QC- zMaiqP=LJ7h3IW-pXgm1#T+QAb)jZj~g)x6F*%#vX;{3$R9?A9EYNeFK1+Wl`3xE~!t{=vMezPn)N!PSSXt1`GZ(MyY+{vb zSh=lLh?U(25cJ=_!KOpeM`d~Or!eu^onJmst5W<6AQ@gUlg zv>!=|wHlTfftCa6M~3~xhXQN%jthXsi=bye(oWwx4|57HG(4ruvurjkmD&S)465E6 z*urc@^#=oPxgOCviTjS&8*CeUkEitv(25v4fh?X?A{%lCpM+$kjaSA?u5IsG*^3VC za74avZy#IROXnfLtDd~J{~?`+=L6?=I}cH!^#)1n*|s=(*iXda*a7%R<3djx`l`>R z;-{{{oZbr=305vlx28G@l+rW}#+BF>u2v2SJN0RD>~M0etS%G}=CZY`IyPMSsAEa2 zY)gBhCtEEx-1Ra)*;H5kRkPGxhB8qeKJHF^Pq94b;KNk-RmwtVqUkN6(HZXBuTqLnWbEmn>C}NPTm%UmK0IzjPT|qleVbDkibw-rp5?toubd-q zW%`kCgGuUxZ`qZ}WDC=U$8}X#+lg-sNA-OY(&eDMpR5jjHRi$zzBh*MqBk{W+r^+g zRFh$Kq&AtAxD9-&w;ykai+z96%Ml>Y_*1@NLs^aQ zczXC0_bziDI#~c+4?XUmZFwLCD*^w4^rBC{*x;5xwL5&ZLbtbkBg7Z-qQW}so@S-7 zwmNi#Os&Quoz-z-H!Dk { console.log("Shutting down due to SIGTERM"); await gateway.stop(); await cdn.stop(); await api.stop(); + await webrtc.stop(); server.close(); Sentry.close(); }); @@ -54,7 +62,12 @@ async function main() { await new Promise((resolve) => server.listen({ port }, () => resolve(undefined)), ); - await Promise.all([api.start(), cdn.start(), gateway.start()]); + await Promise.all([ + api.start(), + cdn.start(), + gateway.start(), + webrtc.start(), + ]); Sentry.errorHandler(app); diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts index 9fba2d4c..94b4fbe9 100644 --- a/src/gateway/Server.ts +++ b/src/gateway/Server.ts @@ -29,6 +29,7 @@ import { import ws from "ws"; import { Connection } from "./events/Connection"; import http from "http"; +import { cleanupOnStartup } from "./util/Utils"; export class Server { public ws: ws.Server; @@ -74,6 +75,8 @@ export class Server { await Config.init(); await initEvent(); await Sentry.init(); + // temporary fix + await cleanupOnStartup(); if (!this.server.listening) { this.server.listen(this.port); diff --git a/src/gateway/events/Close.ts b/src/gateway/events/Close.ts index 311ed32a..dbbc41d8 100644 --- a/src/gateway/events/Close.ts +++ b/src/gateway/events/Close.ts @@ -24,6 +24,8 @@ import { Session, SessionsReplace, User, + VoiceState, + VoiceStateUpdateEvent, } from "@spacebar/util"; export async function Close(this: WebSocket, code: number, reason: Buffer) { @@ -36,6 +38,39 @@ export async function Close(this: WebSocket, code: number, reason: Buffer) { if (this.session_id) { await Session.delete({ session_id: this.session_id }); + + const voiceState = await VoiceState.findOne({ + where: { user_id: this.user_id }, + }); + + // clear the voice state for this session if user was in voice channel + if ( + voiceState && + voiceState.session_id === this.session_id && + voiceState.channel_id + ) { + const prevGuildId = voiceState.guild_id; + const prevChannelId = voiceState.channel_id; + + // @ts-expect-error channel_id is nullable + voiceState.channel_id = null; + // @ts-expect-error guild_id is nullable + voiceState.guild_id = null; + voiceState.self_stream = false; + voiceState.self_video = false; + await voiceState.save(); + + // let the users in previous guild/channel know that user disconnected + await emitEvent({ + event: "VOICE_STATE_UPDATE", + data: { + ...voiceState.toPublicVoiceState(), + guild_id: prevGuildId, // have to send the previous guild_id because that's what client expects for disconnect messages + }, + guild_id: prevGuildId, + channel_id: prevChannelId, + } as VoiceStateUpdateEvent); + } } if (this.user_id) { diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index 4f1c7e2d..fbf579ff 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -183,6 +183,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { "guild.emojis", "guild.roles", "guild.stickers", + "guild.voice_states", "roles", // For these entities, `user` is always just the logged in user we fetched above @@ -485,6 +486,18 @@ export async function onIdentify(this: WebSocket, data: Payload) { }), ); + const readySupplementalGuilds = ( + guilds.filter((guild) => !guild.unavailable) as Guild[] + ).map((guild) => { + return { + voice_states: guild.voice_states.map((state) => + state.toPublicVoiceState(), + ), + id: guild.id, + embedded_activities: [], + }; + }); + // TODO: ready supplemental await Send(this, { op: OPCodes.DISPATCH, @@ -498,7 +511,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { // these merged members seem to be all users currently in vc in your guilds merged_members: [], lazy_private_channels: [], - guilds: [], // { voice_states: [], id: string, embedded_activities: [] } + guilds: readySupplementalGuilds, // { voice_states: [], id: string, embedded_activities: [] } // embedded_activities are users currently in an activity? disclose: [], // Config.get().general.uniqueUsernames ? ["pomelo"] : [] }, diff --git a/src/gateway/opcodes/StreamCreate.ts b/src/gateway/opcodes/StreamCreate.ts new file mode 100644 index 00000000..80325a2f --- /dev/null +++ b/src/gateway/opcodes/StreamCreate.ts @@ -0,0 +1,131 @@ +import { + genVoiceToken, + Payload, + WebSocket, + generateStreamKey, +} from "@spacebar/gateway"; +import { + Channel, + Config, + emitEvent, + Member, + Region, + Snowflake, + Stream, + StreamCreateEvent, + StreamCreateSchema, + StreamServerUpdateEvent, + StreamSession, + VoiceState, + VoiceStateUpdateEvent, +} from "@spacebar/util"; +import { check } from "./instanceOf"; + +export async function onStreamCreate(this: WebSocket, data: Payload) { + check.call(this, StreamCreateSchema, data.d); + const body = data.d as StreamCreateSchema; + + if (body.channel_id.trim().length === 0) return; + + // first check if we are in a voice channel already. cannot create a stream if there's no existing voice connection + const voiceState = await VoiceState.findOne({ + where: { user_id: this.user_id }, + }); + + if (!voiceState || !voiceState.channel_id) return; + + if (body.guild_id) { + voiceState.member = await Member.findOneOrFail({ + where: { id: voiceState.user_id, guild_id: voiceState.guild_id }, + relations: ["user", "roles"], + }); + } + + // TODO: permissions check - if it's a guild, check if user is allowed to create stream in this guild + + const channel = await Channel.findOne({ + where: { id: body.channel_id }, + }); + + if ( + !channel || + (body.type === "guild" && channel.guild_id != body.guild_id) + ) + return this.close(4000, "invalid channel"); + + // TODO: actually apply preferred_region from the event payload + const regions = Config.get().regions; + const guildRegion = regions.available.filter( + (r) => r.id === regions.default, + )[0]; + + // first make sure theres no other streams for this user that somehow didnt get cleared + await Stream.delete({ + owner_id: this.user_id, + }); + + // create a new entry in db containing the token for authenticating user in stream gateway IDENTIFY + const stream = Stream.create({ + id: Snowflake.generate(), + owner_id: this.user_id, + channel_id: body.channel_id, + endpoint: guildRegion.endpoint, + }); + + await stream.save(); + + const token = genVoiceToken(); + + const streamSession = StreamSession.create({ + stream_id: stream.id, + user_id: this.user_id, + session_id: this.session_id, + token, + }); + + await streamSession.save(); + + const streamKey = generateStreamKey( + body.type, + body.guild_id, + body.channel_id, + this.user_id, + ); + + await emitEvent({ + event: "STREAM_CREATE", + data: { + stream_key: streamKey, + rtc_server_id: stream.id, // for voice connections in guilds it is guild_id, for dm voice calls it seems to be DM channel id, for GoLive streams a generated number + viewer_ids: [], + region: guildRegion.name, + paused: false, + }, + user_id: this.user_id, + } as StreamCreateEvent); + + await emitEvent({ + event: "STREAM_SERVER_UPDATE", + data: { + token: streamSession.token, + stream_key: streamKey, + guild_id: null, // not sure why its always null + endpoint: stream.endpoint, + }, + user_id: this.user_id, + } as StreamServerUpdateEvent); + + voiceState.self_stream = true; + await voiceState.save(); + + await emitEvent({ + event: "VOICE_STATE_UPDATE", + data: voiceState.toPublicVoiceState(), + guild_id: voiceState.guild_id, + channel_id: voiceState.channel_id, + } as VoiceStateUpdateEvent); +} + +//stream key: +// guild:${guild_id}:${channel_id}:${user_id} +// call:${channel_id}:${user_id} diff --git a/src/gateway/opcodes/StreamDelete.ts b/src/gateway/opcodes/StreamDelete.ts new file mode 100644 index 00000000..76c87029 --- /dev/null +++ b/src/gateway/opcodes/StreamDelete.ts @@ -0,0 +1,76 @@ +import { parseStreamKey, Payload, WebSocket } from "@spacebar/gateway"; +import { + emitEvent, + Stream, + StreamDeleteEvent, + StreamDeleteSchema, + VoiceState, + VoiceStateUpdateEvent, +} from "@spacebar/util"; +import { check } from "./instanceOf"; + +export async function onStreamDelete(this: WebSocket, data: Payload) { + check.call(this, StreamDeleteSchema, data.d); + const body = data.d as StreamDeleteSchema; + + let parsedKey: { + type: "guild" | "call"; + channelId: string; + guildId?: string; + userId: string; + }; + + try { + parsedKey = parseStreamKey(body.stream_key); + } catch (e) { + return this.close(4000, "Invalid stream key"); + } + + const { userId, channelId, guildId, type } = parsedKey; + + // when a user selects to stop watching another user stream, this event gets triggered + // just disconnect user without actually deleting stream + if (this.user_id !== userId) { + await emitEvent({ + event: "STREAM_DELETE", + data: { + stream_key: body.stream_key, + }, + user_id: this.user_id, + } as StreamDeleteEvent); + return; + } + + const stream = await Stream.findOne({ + where: { channel_id: channelId, owner_id: userId }, + }); + + if (!stream) return; + + await stream.remove(); + + const voiceState = await VoiceState.findOne({ + where: { user_id: this.user_id }, + }); + + if (voiceState) { + voiceState.self_stream = false; + await voiceState.save(); + + await emitEvent({ + event: "VOICE_STATE_UPDATE", + data: voiceState.toPublicVoiceState(), + guild_id: guildId, + channel_id: channelId, + } as VoiceStateUpdateEvent); + } + + await emitEvent({ + event: "STREAM_DELETE", + data: { + stream_key: body.stream_key, + }, + guild_id: guildId, + channel_id: channelId, + } as StreamDeleteEvent); +} diff --git a/src/gateway/opcodes/StreamWatch.ts b/src/gateway/opcodes/StreamWatch.ts new file mode 100644 index 00000000..163dbeaf --- /dev/null +++ b/src/gateway/opcodes/StreamWatch.ts @@ -0,0 +1,98 @@ +import { + genVoiceToken, + parseStreamKey, + Payload, + WebSocket, +} from "@spacebar/gateway"; +import { + Config, + emitEvent, + Stream, + StreamCreateEvent, + StreamServerUpdateEvent, + StreamSession, + StreamWatchSchema, +} from "@spacebar/util"; +import { check } from "./instanceOf"; +import { Not } from "typeorm"; + +export async function onStreamWatch(this: WebSocket, data: Payload) { + check.call(this, StreamWatchSchema, data.d); + const body = data.d as StreamWatchSchema; + + // TODO: apply perms: check if user is allowed to watch + + let parsedKey: { + type: "guild" | "call"; + channelId: string; + guildId?: string; + userId: string; + }; + + try { + parsedKey = parseStreamKey(body.stream_key); + } catch (e) { + return this.close(4000, "Invalid stream key"); + } + + const { type, channelId, guildId, userId } = parsedKey; + + const stream = await Stream.findOne({ + where: { channel_id: channelId, owner_id: userId }, + relations: ["channel"], + }); + + if (!stream) return this.close(4000, "Invalid stream key"); + + if (type === "guild" && stream.channel.guild_id != guildId) + return this.close(4000, "Invalid stream key"); + + const regions = Config.get().regions; + const guildRegion = regions.available.find( + (r) => r.endpoint === stream.endpoint, + ); + + if (!guildRegion) return this.close(4000, "Unknown region"); + + const streamSession = StreamSession.create({ + stream_id: stream.id, + user_id: this.user_id, + session_id: this.session_id, + token: genVoiceToken(), + }); + + await streamSession.save(); + + // get the viewers: stream session tokens for this stream that have been used but not including stream owner + const viewers = await StreamSession.find({ + where: { + stream_id: stream.id, + used: true, + user_id: Not(stream.owner_id), + }, + }); + + await emitEvent({ + event: "STREAM_CREATE", + data: { + stream_key: body.stream_key, + rtc_server_id: stream.id, // for voice connections in guilds it is guild_id, for dm voice calls it seems to be DM channel id, for GoLive streams a generated number + viewer_ids: viewers.map((v) => v.user_id), + region: guildRegion.name, + paused: false, + }, + channel_id: channelId, + user_id: this.user_id, + } as StreamCreateEvent); + + await emitEvent({ + event: "STREAM_SERVER_UPDATE", + data: { + token: streamSession.token, + stream_key: body.stream_key, + guild_id: null, // not sure why its always null + endpoint: stream.endpoint, + }, + user_id: this.user_id, + } as StreamServerUpdateEvent); +} diff --git a/src/gateway/opcodes/VoiceStateUpdate.ts b/src/gateway/opcodes/VoiceStateUpdate.ts index b45c8203..61dad0cd 100644 --- a/src/gateway/opcodes/VoiceStateUpdate.ts +++ b/src/gateway/opcodes/VoiceStateUpdate.ts @@ -17,19 +17,19 @@ */ import { Payload, WebSocket } from "@spacebar/gateway"; -import { genVoiceToken } from "../util/SessionUtils"; -import { check } from "./instanceOf"; import { Config, emitEvent, Guild, Member, + Region, VoiceServerUpdateEvent, VoiceState, VoiceStateUpdateEvent, VoiceStateUpdateSchema, - Region, } from "@spacebar/util"; +import { genVoiceToken } from "../util/SessionUtils"; +import { check } from "./instanceOf"; // TODO: check if a voice server is setup // Notice: Bot users respect the voice channel's user limit, if set. @@ -39,6 +39,10 @@ import { export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { check.call(this, VoiceStateUpdateSchema, data.d); const body = data.d as VoiceStateUpdateSchema; + const isNew = body.channel_id === null && body.guild_id === null; + let isChanged = false; + + let prevState; let voiceState: VoiceState; try { @@ -54,20 +58,24 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { return; } + if (voiceState.channel_id !== body.channel_id) isChanged = true; + //If a user change voice channel between guild we should send a left event first if ( + voiceState.guild_id && voiceState.guild_id !== body.guild_id && voiceState.session_id === this.session_id ) { await emitEvent({ event: "VOICE_STATE_UPDATE", - data: { ...voiceState, channel_id: null }, + data: { ...voiceState.toPublicVoiceState(), channel_id: null }, guild_id: voiceState.guild_id, }); } //The event send by Discord's client on channel leave has both guild_id and channel_id as null - if (body.guild_id === null) body.guild_id = voiceState.guild_id; + //if (body.guild_id === null) body.guild_id = voiceState.guild_id; + prevState = { ...voiceState }; voiceState.assign(body); } catch (error) { voiceState = VoiceState.create({ @@ -79,39 +87,58 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { }); } - // 'Fix' for this one voice state error. TODO: Find out why this is sent - // It seems to be sent on client load, - // so maybe its trying to find which server you were connected to before disconnecting, if any? - if (body.guild_id == null) { - return; + // if user left voice channel, send an update to previous channel/guild to let other people know that the user left + if ( + voiceState.session_id === this.session_id && + body.guild_id == null && + body.channel_id == null && + (prevState?.guild_id || prevState?.channel_id) + ) { + await emitEvent({ + event: "VOICE_STATE_UPDATE", + data: { + ...voiceState.toPublicVoiceState(), + channel_id: null, + guild_id: null, + }, + guild_id: prevState?.guild_id, + channel_id: prevState?.channel_id, + }); } //TODO the member should only have these properties: hoisted_role, deaf, joined_at, mute, roles, user //TODO the member.user should only have these properties: avatar, discriminator, id, username //TODO this may fail - voiceState.member = await Member.findOneOrFail({ - where: { id: voiceState.user_id, guild_id: voiceState.guild_id }, - relations: ["user", "roles"], - }); + if (body.guild_id) { + voiceState.member = await Member.findOneOrFail({ + where: { id: voiceState.user_id, guild_id: voiceState.guild_id }, + relations: ["user", "roles"], + }); + } //If the session changed we generate a new token if (voiceState.session_id !== this.session_id) voiceState.token = genVoiceToken(); voiceState.session_id = this.session_id; - const { id, ...newObj } = voiceState; + const { member } = voiceState; await Promise.all([ voiceState.save(), emitEvent({ event: "VOICE_STATE_UPDATE", - data: newObj, + data: { + ...voiceState.toPublicVoiceState(), + member: member?.toPublicMember(), + }, guild_id: voiceState.guild_id, + channel_id: voiceState.channel_id, + user_id: voiceState.user_id, } as VoiceStateUpdateEvent), ]); //If it's null it means that we are leaving the channel and this event is not needed - if (voiceState.channel_id !== null) { + if ((isNew || isChanged) && voiceState.channel_id !== null) { const guild = await Guild.findOne({ where: { id: voiceState.guild_id }, }); @@ -133,8 +160,11 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { token: voiceState.token, guild_id: voiceState.guild_id, endpoint: guildRegion.endpoint, + channel_id: voiceState.guild_id + ? undefined + : voiceState.channel_id, // only DM voice calls have this set, and DM channel is one where guild_id is null }, - guild_id: voiceState.guild_id, + user_id: voiceState.user_id, } as VoiceServerUpdateEvent); } } diff --git a/src/gateway/opcodes/index.ts b/src/gateway/opcodes/index.ts index e925134d..cba9e545 100644 --- a/src/gateway/opcodes/index.ts +++ b/src/gateway/opcodes/index.ts @@ -25,6 +25,9 @@ import { onRequestGuildMembers } from "./RequestGuildMembers"; import { onResume } from "./Resume"; import { onVoiceStateUpdate } from "./VoiceStateUpdate"; import { onGuildSubscriptionsBulk } from "./GuildSubscriptionsBulk"; +import { onStreamCreate } from "./StreamCreate"; +import { onStreamDelete } from "./StreamDelete"; +import { onStreamWatch } from "./StreamWatch"; export type OPCodeHandler = (this: WebSocket, data: Payload) => unknown; @@ -41,5 +44,8 @@ export default { // 10: Hello // 13: Dm_update 14: onLazyRequest, + 18: onStreamCreate, + 19: onStreamDelete, + 20: onStreamWatch, 37: onGuildSubscriptionsBulk, } as { [key: number]: OPCodeHandler }; diff --git a/src/gateway/util/Constants.ts b/src/gateway/util/Constants.ts index 5c0f134a..26c90dbe 100644 --- a/src/gateway/util/Constants.ts +++ b/src/gateway/util/Constants.ts @@ -16,8 +16,6 @@ along with this program. If not, see . */ -// import { VoiceOPCodes } from "@spacebar/webrtc"; - export enum OPCODES { Dispatch = 0, Heartbeat = 1, @@ -63,7 +61,7 @@ export enum CLOSECODES { } export interface Payload { - op: OPCODES /* | VoiceOPCodes */; + op: OPCODES; // eslint-disable-next-line @typescript-eslint/no-explicit-any d?: any; s?: number; diff --git a/src/gateway/util/Utils.ts b/src/gateway/util/Utils.ts new file mode 100644 index 00000000..dac1c805 --- /dev/null +++ b/src/gateway/util/Utils.ts @@ -0,0 +1,63 @@ +import { VoiceState } from "@spacebar/util"; + +export function parseStreamKey(streamKey: string): { + type: "guild" | "call"; + channelId: string; + guildId?: string; + userId: string; +} { + const streamKeyArray = streamKey.split(":"); + + const type = streamKeyArray.shift(); + + if (type !== "guild" && type !== "call") { + throw new Error(`Invalid stream key type: ${type}`); + } + + if ( + (type === "guild" && streamKeyArray.length < 3) || + (type === "call" && streamKeyArray.length < 2) + ) + throw new Error(`Invalid stream key: ${streamKey}`); // invalid stream key + + let guildId: string | undefined; + if (type === "guild") { + guildId = streamKeyArray.shift(); + } + const channelId = streamKeyArray.shift(); + const userId = streamKeyArray.shift(); + + if (!channelId || !userId) { + throw new Error(`Invalid stream key: ${streamKey}`); + } + return { type, channelId, guildId, userId }; +} + +export function generateStreamKey( + type: "guild" | "call", + guildId: string | undefined, + channelId: string, + userId: string, +): string { + const streamKey = `${type}${type === "guild" ? `:${guildId}` : ""}:${channelId}:${userId}`; + + return streamKey; +} + +// Temporary cleanup function until shutdown cleanup function is fixed. +// Currently when server is shut down the voice states are not cleared +// TODO: remove this when Server.stop() is fixed so that it waits for all websocket connections to run their +// respective Close event listener function for session cleanup +export async function cleanupOnStartup(): Promise { + await VoiceState.update( + {}, + { + // @ts-expect-error channel_id is nullable + channel_id: null, + // @ts-expect-error guild_id is nullable + guild_id: null, + self_stream: false, + self_video: false, + }, + ); +} diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts index 8cfc5e08..5c110840 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts @@ -20,7 +20,6 @@ import { Intents, ListenEventOpts, Permissions } from "@spacebar/util"; import WS from "ws"; import { Deflate, Inflate } from "fast-zlib"; import { Capabilities } from "./Capabilities"; -// import { Client } from "@spacebar/webrtc"; export interface WebSocket extends WS { version: number; @@ -42,6 +41,5 @@ export interface WebSocket extends WS { member_events: Record unknown>; listen_options: ListenEventOpts; capabilities?: Capabilities; - // client?: Client; large_threshold: number; } diff --git a/src/gateway/util/index.ts b/src/gateway/util/index.ts index 6ef694d9..5a8c906b 100644 --- a/src/gateway/util/index.ts +++ b/src/gateway/util/index.ts @@ -22,3 +22,4 @@ export * from "./SessionUtils"; export * from "./Heartbeat"; export * from "./WebSocket"; export * from "./Capabilities"; +export * from "./Utils"; diff --git a/src/util/entities/Stream.ts b/src/util/entities/Stream.ts new file mode 100644 index 00000000..2787e3ce --- /dev/null +++ b/src/util/entities/Stream.ts @@ -0,0 +1,42 @@ +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + RelationId, +} from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { dbEngine } from "../util/Database"; +import { User } from "./User"; +import { Channel } from "./Channel"; +import { StreamSession } from "./StreamSession"; + +@Entity({ + name: "streams", + engine: dbEngine, +}) +export class Stream extends BaseClass { + @Column() + @RelationId((stream: Stream) => stream.owner) + owner_id: string; + + @JoinColumn({ name: "owner_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + owner: User; + + @Column() + @RelationId((stream: Stream) => stream.channel) + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column() + endpoint: string; +} diff --git a/src/util/entities/StreamSession.ts b/src/util/entities/StreamSession.ts new file mode 100644 index 00000000..6d7ccf9d --- /dev/null +++ b/src/util/entities/StreamSession.ts @@ -0,0 +1,48 @@ +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + RelationId, +} from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { dbEngine } from "../util/Database"; +import { User } from "./User"; +import { Stream } from "./Stream"; + +@Entity({ + name: "stream_sessions", + engine: dbEngine, +}) +export class StreamSession extends BaseClass { + @Column() + @RelationId((session: StreamSession) => session.stream) + stream_id: string; + + @JoinColumn({ name: "stream_id" }) + @ManyToOne(() => Stream, { + onDelete: "CASCADE", + }) + stream: Stream; + + @Column() + @RelationId((session: StreamSession) => session.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column({ nullable: true }) + token: string; + + // this is for gateway session + @Column() + session_id: string; + + @Column({ default: false }) + used: boolean; +} diff --git a/src/util/entities/VoiceState.ts b/src/util/entities/VoiceState.ts index 83a0af63..549d667f 100644 --- a/src/util/entities/VoiceState.ts +++ b/src/util/entities/VoiceState.ts @@ -24,6 +24,29 @@ import { Member } from "./Member"; import { User } from "./User"; import { dbEngine } from "../util/Database"; +export enum PublicVoiceStateEnum { + user_id, + suppress, + session_id, + self_video, + self_mute, + self_deaf, + self_stream, + request_to_speak_timestamp, + mute, + deaf, + channel_id, + guild_id, +} + +export type PublicVoiceStateKeys = keyof typeof PublicVoiceStateEnum; + +export const PublicVoiceStateProjection = Object.values( + PublicVoiceStateEnum, +).filter((x) => typeof x === "string") as PublicVoiceStateKeys[]; + +export type PublicVoiceState = Pick; + //https://gist.github.com/vassjozsef/e482c65df6ee1facaace8b3c9ff66145#file-voice_state-ex @Entity({ name: "voice_states", @@ -96,4 +119,13 @@ export class VoiceState extends BaseClass { @Column({ nullable: true, default: null }) request_to_speak_timestamp?: Date; + + toPublicVoiceState() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const voiceState: any = {}; + PublicVoiceStateProjection.forEach((x) => { + voiceState[x] = this[x]; + }); + return voiceState as PublicVoiceState; + } } diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts index b2356aa7..6f132084 100644 --- a/src/util/entities/index.ts +++ b/src/util/entities/index.ts @@ -47,6 +47,8 @@ export * from "./SecurityKey"; export * from "./Session"; export * from "./Sticker"; export * from "./StickerPack"; +export * from "./Stream"; +export * from "./StreamSession"; export * from "./Team"; export * from "./TeamMember"; export * from "./Template"; diff --git a/src/util/index.ts b/src/util/index.ts index dba69812..9a84d1af 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -28,4 +28,4 @@ export * from "./schemas"; export * from "./imports"; export * from "./config"; export * from "./connections"; -export * from "./Signing" \ No newline at end of file +export * from "./Signing"; diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 253a013c..5ef3b05d 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -21,7 +21,6 @@ import { ConnectedAccount, Interaction, ApplicationCommand, - VoiceState, Message, PartialEmoji, Invite, @@ -43,6 +42,7 @@ import { ReadyPrivateChannel, GuildOrUnavailable, GuildCreateResponse, + PublicVoiceState, } from "@spacebar/util"; export interface Event { @@ -431,7 +431,7 @@ export interface UserConnectionsUpdateEvent extends Event { export interface VoiceStateUpdateEvent extends Event { event: "VOICE_STATE_UPDATE"; - data: VoiceState & { + data: PublicVoiceState & { member: PublicMember; }; } @@ -440,8 +440,37 @@ export interface VoiceServerUpdateEvent extends Event { event: "VOICE_SERVER_UPDATE"; data: { token: string; - guild_id: string; + guild_id: string | null; endpoint: string; + channel_id?: string; + }; +} + +export interface StreamCreateEvent extends Event { + event: "STREAM_CREATE"; + data: { + stream_key: string; + rtc_server_id: string; + viewer_ids: string[]; + region: string; + paused: boolean; + }; +} + +export interface StreamServerUpdateEvent extends Event { + event: "STREAM_SERVER_UPDATE"; + data: { + token: string; + stream_key: string; + endpoint: string; + guild_id: string | null; + }; +} + +export interface StreamDeleteEvent extends Event { + event: "STREAM_DELETE"; + data: { + stream_key: string; }; } @@ -681,6 +710,9 @@ export type EVENT = | "INTERACTION_CREATE" | "VOICE_STATE_UPDATE" | "VOICE_SERVER_UPDATE" + | "STREAM_CREATE" + | "STREAM_SERVER_UPDATE" + | "STREAM_DELETE" | "APPLICATION_COMMAND_CREATE" | "APPLICATION_COMMAND_UPDATE" | "APPLICATION_COMMAND_DELETE" diff --git a/src/util/migration/postgres/1745625724865-voice.ts b/src/util/migration/postgres/1745625724865-voice.ts new file mode 100644 index 00000000..d9f7101f --- /dev/null +++ b/src/util/migration/postgres/1745625724865-voice.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Voice1745625724865 implements MigrationInterface { + name = "Voice1745625724865"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "streams" ("id" character varying NOT NULL, "owner_id" character varying NOT NULL, "channel_id" character varying NOT NULL, "endpoint" character varying NOT NULL, CONSTRAINT "PK_40440b6f569ebc02bc71c25c499" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "stream_sessions" ("id" character varying NOT NULL, "stream_id" character varying NOT NULL, "user_id" character varying NOT NULL, "token" character varying, "session_id" character varying NOT NULL, "used" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_49bdc3f66394c12478f8371c546" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "streams" ADD CONSTRAINT "FK_1b566f9b54d1cda271da53ac82f" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "streams" ADD CONSTRAINT "FK_5101f0cded27ff0aae78fc4eed7" FOREIGN KEY ("channel_id") REFERENCES "channels"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "stream_sessions" ADD CONSTRAINT "FK_8b5a028a34dae9ee54af37c9c32" FOREIGN KEY ("stream_id") REFERENCES "streams"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "stream_sessions" ADD CONSTRAINT "FK_13ae5c29aff4d0890c54179511a" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "stream_sessions" DROP CONSTRAINT "FK_13ae5c29aff4d0890c54179511a"`, + ); + await queryRunner.query( + `ALTER TABLE "stream_sessions" DROP CONSTRAINT "FK_8b5a028a34dae9ee54af37c9c32"`, + ); + await queryRunner.query( + `ALTER TABLE "streams" DROP CONSTRAINT "FK_5101f0cded27ff0aae78fc4eed7"`, + ); + await queryRunner.query( + `ALTER TABLE "streams" DROP CONSTRAINT "FK_1b566f9b54d1cda271da53ac82f"`, + ); + await queryRunner.query(`DROP TABLE "stream_sessions"`); + await queryRunner.query(`DROP TABLE "streams"`); + } +} diff --git a/src/util/schemas/SelectProtocolSchema.ts b/src/util/schemas/SelectProtocolSchema.ts index 09283619..d04adf71 100644 --- a/src/util/schemas/SelectProtocolSchema.ts +++ b/src/util/schemas/SelectProtocolSchema.ts @@ -31,7 +31,7 @@ export interface SelectProtocolSchema { type: "audio" | "video"; priority: number; payload_type: number; - rtx_payload_type?: number | null; + rtx_payload_type?: number; }[]; rtc_connection_id?: string; // uuid } diff --git a/src/util/schemas/StreamCreateSchema.ts b/src/util/schemas/StreamCreateSchema.ts new file mode 100644 index 00000000..bb650791 --- /dev/null +++ b/src/util/schemas/StreamCreateSchema.ts @@ -0,0 +1,13 @@ +export interface StreamCreateSchema { + type: "guild" | "call"; + channel_id: string; + guild_id?: string; + preferred_region?: string; +} + +export const StreamCreateSchema = { + type: String, + channel_id: String, + $guild_id: String, + $preferred_region: String, +}; diff --git a/src/util/schemas/StreamDeleteSchema.ts b/src/util/schemas/StreamDeleteSchema.ts new file mode 100644 index 00000000..0e2aff75 --- /dev/null +++ b/src/util/schemas/StreamDeleteSchema.ts @@ -0,0 +1,7 @@ +export interface StreamDeleteSchema { + stream_key: string; +} + +export const StreamDeleteSchema = { + stream_key: String, +}; diff --git a/src/util/schemas/StreamWatchSchema.ts b/src/util/schemas/StreamWatchSchema.ts new file mode 100644 index 00000000..263bb11f --- /dev/null +++ b/src/util/schemas/StreamWatchSchema.ts @@ -0,0 +1,7 @@ +export interface StreamWatchSchema { + stream_key: string; +} + +export const StreamWatchSchema = { + stream_key: String, +}; diff --git a/src/util/schemas/VoiceIdentifySchema.ts b/src/util/schemas/VoiceIdentifySchema.ts index 618d6591..82f846c3 100644 --- a/src/util/schemas/VoiceIdentifySchema.ts +++ b/src/util/schemas/VoiceIdentifySchema.ts @@ -23,8 +23,9 @@ export interface VoiceIdentifySchema { token: string; video?: boolean; streams?: { - type: string; + type: "video" | "audio" | "screen"; rid: string; quality: number; }[]; + max_secure_frames_version?: number; } diff --git a/src/util/schemas/VoiceVideoSchema.ts b/src/util/schemas/VoiceVideoSchema.ts index 0f43adc0..c621431b 100644 --- a/src/util/schemas/VoiceVideoSchema.ts +++ b/src/util/schemas/VoiceVideoSchema.ts @@ -22,7 +22,7 @@ export interface VoiceVideoSchema { rtx_ssrc?: number; user_id?: string; streams?: { - type: "video" | "audio"; + type: "video" | "audio" | "screen"; rid: string; ssrc: number; active: boolean; diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts index 9701faec..f19eef0d 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts @@ -68,6 +68,9 @@ export * from "./responses"; export * from "./RoleModifySchema"; export * from "./RolePositionUpdateSchema"; export * from "./SelectProtocolSchema"; +export * from "./StreamCreateSchema"; +export * from "./StreamDeleteSchema"; +export * from "./StreamWatchSchema"; export * from "./TeamCreateSchema"; export * from "./TemplateCreateSchema"; export * from "./TemplateModifySchema"; diff --git a/src/util/util/Constants.ts b/src/util/util/Constants.ts index 34e925e5..df4501fc 100644 --- a/src/util/util/Constants.ts +++ b/src/util/util/Constants.ts @@ -52,23 +52,6 @@ export const WsStatus = { RESUMING: 8, }; -/** - * The current status of a voice connection. Here are the available statuses: - * * CONNECTED: 0 - * * CONNECTING: 1 - * * AUTHENTICATING: 2 - * * RECONNECTING: 3 - * * DISCONNECTED: 4 - * @typedef {number} VoiceStatus - */ -export const VoiceStatus = { - CONNECTED: 0, - CONNECTING: 1, - AUTHENTICATING: 2, - RECONNECTING: 3, - DISCONNECTED: 4, -}; - export const OPCodes = { DISPATCH: 0, HEARTBEAT: 1, @@ -84,22 +67,6 @@ export const OPCodes = { HEARTBEAT_ACK: 11, }; -export const VoiceOPCodes = { - IDENTIFY: 0, - SELECT_PROTOCOL: 1, - READY: 2, - HEARTBEAT: 3, - SESSION_DESCRIPTION: 4, - SPEAKING: 5, - HEARTBEAT_ACK: 6, - RESUME: 7, - HELLO: 8, - RESUMED: 9, - CLIENT_CONNECT: 12, // incorrect, op 12 is probably used for video - CLIENT_DISCONNECT: 13, // incorrect - VERSION: 16, //not documented -}; - export const Events = { RATE_LIMIT: "rateLimit", CLIENT_READY: "ready", diff --git a/src/util/util/Event.ts b/src/util/util/Event.ts index bbc93aac..f56d6664 100644 --- a/src/util/util/Event.ts +++ b/src/util/util/Event.ts @@ -23,9 +23,9 @@ import { EVENT, Event } from "../interfaces"; export const events = new EventEmitter(); export async function emitEvent(payload: Omit) { - const id = (payload.channel_id || - payload.user_id || - payload.guild_id) as string; + const id = (payload.guild_id || + payload.channel_id || + payload.user_id) as string; if (!id) return console.error("event doesn't contain any id", payload); if (RabbitMQ.connection) { diff --git a/src/webrtc/Server.ts b/src/webrtc/Server.ts index 0ba2e41b..08f9439f 100644 --- a/src/webrtc/Server.ts +++ b/src/webrtc/Server.ts @@ -21,6 +21,14 @@ import dotenv from "dotenv"; import http from "http"; import ws from "ws"; import { Connection } from "./events/Connection"; +import { + loadWebRtcLibrary, + mediaServer, + WRTC_PORT_MAX, + WRTC_PORT_MIN, + WRTC_PUBLIC_IP, +} from "./util/MediaServer"; +import { green, yellow } from "picocolors"; dotenv.config(); export class Server { @@ -69,14 +77,25 @@ export class Server { await initDatabase(); await Config.init(); await initEvent(); + + // try to load webrtc library, if failed just don't start webrtc endpoint + try { + await loadWebRtcLibrary(); + } catch (e) { + console.log(`[WebRTC] ${yellow("WEBRTC disabled")}`); + return; + } + + await mediaServer.start(WRTC_PUBLIC_IP, WRTC_PORT_MIN, WRTC_PORT_MAX); if (!this.server.listening) { this.server.listen(this.port); - console.log(`[WebRTC] online on 0.0.0.0:${this.port}`); + console.log(`[WebRTC] ${green(`online on 0.0.0.0:${this.port}`)}`); } } async stop() { closeDatabase(); this.server.close(); + mediaServer?.stop(); } } diff --git a/src/webrtc/events/Close.ts b/src/webrtc/events/Close.ts index 7b71e9ce..0419a70e 100644 --- a/src/webrtc/events/Close.ts +++ b/src/webrtc/events/Close.ts @@ -17,11 +17,9 @@ */ import { WebSocket } from "@spacebar/gateway"; -import { Session } from "@spacebar/util"; export async function onClose(this: WebSocket, code: number, reason: string) { console.log("[WebRTC] closed", code, reason.toString()); - if (this.session_id) await Session.delete({ session_id: this.session_id }); this.removeAllListeners(); } diff --git a/src/webrtc/events/Connection.ts b/src/webrtc/events/Connection.ts index 6c5bab03..a068a8fd 100644 --- a/src/webrtc/events/Connection.ts +++ b/src/webrtc/events/Connection.ts @@ -16,11 +16,11 @@ along with this program. If not, see . */ -import { CLOSECODES, Send, setHeartbeat, WebSocket } from "@spacebar/gateway"; +import { CLOSECODES, setHeartbeat } from "@spacebar/gateway"; import { IncomingMessage } from "http"; import { URL } from "url"; import WS from "ws"; -import { VoiceOPCodes } from "../util"; +import { VoiceOPCodes, WebRtcWebSocket, Send } from "../util"; import { onClose } from "./Close"; import { onMessage } from "./Message"; @@ -30,7 +30,7 @@ import { onMessage } from "./Message"; export async function Connection( this: WS.Server, - socket: WebSocket, + socket: WebRtcWebSocket, request: IncomingMessage, ) { try { diff --git a/src/webrtc/events/Message.ts b/src/webrtc/events/Message.ts index 22189e95..f503bd1e 100644 --- a/src/webrtc/events/Message.ts +++ b/src/webrtc/events/Message.ts @@ -16,10 +16,10 @@ along with this program. If not, see . */ -import { CLOSECODES, Payload, WebSocket } from "@spacebar/gateway"; +import { CLOSECODES } from "@spacebar/gateway"; import { Tuple } from "lambert-server"; import OPCodeHandlers from "../opcodes"; -import { VoiceOPCodes } from "../util"; +import { VoiceOPCodes, VoicePayload, WebRtcWebSocket } from "../util"; const PayloadSchema = { op: Number, @@ -28,16 +28,14 @@ const PayloadSchema = { $t: String, }; -export async function onMessage(this: WebSocket, buffer: Buffer) { +export async function onMessage(this: WebRtcWebSocket, buffer: Buffer) { try { - var data: Payload = JSON.parse(buffer.toString()); + const data: VoicePayload = JSON.parse(buffer.toString()); if (data.op !== VoiceOPCodes.IDENTIFY && !this.user_id) return this.close(CLOSECODES.Not_authenticated); - // @ts-ignore const OPCodeHandler = OPCodeHandlers[data.op]; if (!OPCodeHandler) { - // @ts-ignore console.error("[WebRTC] Unkown opcode " + VoiceOPCodes[data.op]); // TODO: if all opcodes are implemented comment this out: // this.close(CloseCodes.Unknown_opcode); @@ -49,7 +47,6 @@ export async function onMessage(this: WebSocket, buffer: Buffer) { data.op as VoiceOPCodes, ) ) { - // @ts-ignore console.log("[WebRTC] Opcode " + VoiceOPCodes[data.op]); } diff --git a/src/webrtc/opcodes/BackendVersion.ts b/src/webrtc/opcodes/BackendVersion.ts index 60de3e58..c97f4b49 100644 --- a/src/webrtc/opcodes/BackendVersion.ts +++ b/src/webrtc/opcodes/BackendVersion.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { Payload, Send, WebSocket } from "@spacebar/gateway"; -import { VoiceOPCodes } from "../util"; +import { VoiceOPCodes, VoicePayload, WebRtcWebSocket, Send } from "../util"; -export async function onBackendVersion(this: WebSocket, data: Payload) { +export async function onBackendVersion( + this: WebRtcWebSocket, + data: VoicePayload, +) { await Send(this, { op: VoiceOPCodes.VOICE_BACKEND_VERSION, d: { voice: "0.8.43", rtc_worker: "0.3.26" }, diff --git a/src/webrtc/opcodes/Heartbeat.ts b/src/webrtc/opcodes/Heartbeat.ts index 3d8e187b..ef3cae44 100644 --- a/src/webrtc/opcodes/Heartbeat.ts +++ b/src/webrtc/opcodes/Heartbeat.ts @@ -16,16 +16,10 @@ along with this program. If not, see . */ -import { - CLOSECODES, - Payload, - Send, - setHeartbeat, - WebSocket, -} from "@spacebar/gateway"; -import { VoiceOPCodes } from "../util"; +import { CLOSECODES, setHeartbeat } from "@spacebar/gateway"; +import { VoiceOPCodes, VoicePayload, WebRtcWebSocket, Send } from "../util"; -export async function onHeartbeat(this: WebSocket, data: Payload) { +export async function onHeartbeat(this: WebRtcWebSocket, data: VoicePayload) { setHeartbeat(this); if (isNaN(data.d)) return this.close(CLOSECODES.Decode_error); diff --git a/src/webrtc/opcodes/Identify.ts b/src/webrtc/opcodes/Identify.ts index 3f65127e..065813fb 100644 --- a/src/webrtc/opcodes/Identify.ts +++ b/src/webrtc/opcodes/Identify.ts @@ -16,76 +16,128 @@ along with this program. If not, see . */ -import { CLOSECODES, Payload, Send, WebSocket } from "@spacebar/gateway"; +import { CLOSECODES } from "@spacebar/gateway"; import { + StreamSession, validateSchema, VoiceIdentifySchema, VoiceState, } from "@spacebar/util"; -import { endpoint, getClients, VoiceOPCodes, PublicIP } from "@spacebar/webrtc"; -import SemanticSDP from "semantic-sdp"; -const defaultSDP = require("./sdp.json"); +import { + mediaServer, + VoiceOPCodes, + VoicePayload, + WebRtcWebSocket, + Send, + generateSsrc, +} from "@spacebar/webrtc"; +import { subscribeToProducers } from "./Video"; +import { SSRCs } from "spacebar-webrtc-types"; -export async function onIdentify(this: WebSocket, data: Payload) { +export async function onIdentify(this: WebRtcWebSocket, data: VoicePayload) { clearTimeout(this.readyTimeout); const { server_id, user_id, session_id, token, streams, video } = validateSchema("VoiceIdentifySchema", data.d) as VoiceIdentifySchema; - const voiceState = await VoiceState.findOne({ - where: { guild_id: server_id, user_id, token, session_id }, + // server_id can be one of the following: a unique id for a GO Live stream, a channel id for a DM voice call, or a guild id for a guild voice channel + // not sure if there's a way to determine whether a snowflake is a channel id or a guild id without checking if it exists in db + // luckily we will only have to determine this once + let type: "guild-voice" | "dm-voice" | "stream" = "guild-voice"; + let authenticated = false; + + // first check if its a guild voice connection or DM voice call + let voiceState = await VoiceState.findOne({ + where: [ + { guild_id: server_id, user_id, token, session_id }, + { channel_id: server_id, user_id, token, session_id }, + ], }); - if (!voiceState) return this.close(CLOSECODES.Authentication_failed); + + if (voiceState) { + type = voiceState.guild_id === server_id ? "guild-voice" : "dm-voice"; + authenticated = true; + } else { + // if its not a guild/dm voice connection, check if it is a go live stream + const streamSession = await StreamSession.findOne({ + where: { + stream_id: server_id, + user_id, + token, + session_id, + used: false, + }, + relations: ["stream"], + }); + + if (streamSession) { + type = "stream"; + authenticated = true; + streamSession.used = true; + await streamSession.save(); + + this.once("close", async () => { + await streamSession.remove(); + }); + } + } + + // if it doesnt match any then not valid token + if (!authenticated) return this.close(CLOSECODES.Authentication_failed); this.user_id = user_id; this.session_id = session_id; - const sdp = SemanticSDP.SDPInfo.expand(defaultSDP); - sdp.setDTLS( - SemanticSDP.DTLSInfo.expand({ - setup: "actpass", - hash: "sha-256", - fingerprint: endpoint.getDTLSFingerprint(), - }), + + this.type = type; + + const voiceRoomId = type === "stream" ? server_id : voiceState!.channel_id; + this.webRtcClient = await mediaServer.join( + voiceRoomId, + this.user_id, + this, + type!, ); - this.client = { - websocket: this, - out: { - tracks: new Map(), - }, - in: { - audio_ssrc: 0, - video_ssrc: 0, - rtx_ssrc: 0, - }, - sdp, - channel_id: voiceState.channel_id, - }; - - const clients = getClients(voiceState.channel_id)!; - clients.add(this.client); - this.on("close", () => { - clients.delete(this.client!); + // ice-lite media server relies on this to know when the peer went away + mediaServer.onClientClose(this.webRtcClient!); }); + // once connected subscribe to tracks from other users + this.webRtcClient.emitter.once("connected", async () => { + await subscribeToProducers.call(this); + }); + + // the server generates a unique ssrc for the audio and video stream. Must be unique among users connected to same server + // UDP clients will respect this ssrc, but websocket clients will generate and replace it with their own + const generatedSsrc: SSRCs = { + audio_ssrc: generateSsrc(), + video_ssrc: generateSsrc(), + rtx_ssrc: generateSsrc(), + }; + this.webRtcClient.initIncomingSSRCs(generatedSsrc); + await Send(this, { op: VoiceOPCodes.READY, d: { - streams: [ - // { type: "video", ssrc: this.ssrc + 1, rtx_ssrc: this.ssrc + 2, rid: "100", quality: 100, active: false } - ], - ssrc: -1, - port: endpoint.getLocalPort(), + ssrc: generatedSsrc.audio_ssrc, + port: mediaServer.port, modes: [ "aead_aes256_gcm_rtpsize", "aead_aes256_gcm", + "aead_xchacha20_poly1305_rtpsize", "xsalsa20_poly1305_lite_rtpsize", "xsalsa20_poly1305_lite", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305", ], - ip: PublicIP, + ip: mediaServer.ip, experiments: [], + streams: streams?.map((x) => ({ + ...x, + ssrc: generatedSsrc.video_ssrc, + rtx_ssrc: generatedSsrc.rtx_ssrc, + type: "video", // client expects this to be overriden for some reason??? + })), }, }); } diff --git a/src/webrtc/opcodes/SelectProtocol.ts b/src/webrtc/opcodes/SelectProtocol.ts index 0a06e722..4a2ee85d 100644 --- a/src/webrtc/opcodes/SelectProtocol.ts +++ b/src/webrtc/opcodes/SelectProtocol.ts @@ -15,51 +15,41 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ - -import { Payload, Send, WebSocket } from "@spacebar/gateway"; import { SelectProtocolSchema, validateSchema } from "@spacebar/util"; -import { PublicIP, VoiceOPCodes, endpoint } from "@spacebar/webrtc"; -import SemanticSDP, { MediaInfo, SDPInfo } from "semantic-sdp"; +import { + VoiceOPCodes, + VoicePayload, + WebRtcWebSocket, + mediaServer, + Send, +} from "@spacebar/webrtc"; -export async function onSelectProtocol(this: WebSocket, payload: Payload) { - if (!this.client) return; +export async function onSelectProtocol( + this: WebRtcWebSocket, + payload: VoicePayload, +) { + if (!this.webRtcClient) return; const data = validateSchema( "SelectProtocolSchema", payload.d, ) as SelectProtocolSchema; - const offer = SemanticSDP.SDPInfo.parse("m=audio\n" + data.sdp!); - this.client.sdp!.setICE(offer.getICE()); - this.client.sdp!.setDTLS(offer.getDTLS()); + // UDP protocol not currently supported. Maybe in the future? + if (data.protocol !== "webrtc") + return this.close(4000, "only webrtc protocol supported currently"); - const transport = endpoint.createTransport(this.client.sdp!); - this.client.transport = transport; - transport.setRemoteProperties(this.client.sdp!); - transport.setLocalProperties(this.client.sdp!); - - const dtls = transport.getLocalDTLSInfo(); - const ice = transport.getLocalICEInfo(); - const port = endpoint.getLocalPort(); - const fingerprint = dtls.getHash() + " " + dtls.getFingerprint(); - const candidates = transport.getLocalCandidates(); - const candidate = candidates[0]; - - const answer = - `m=audio ${port} ICE/SDP` + - `a=fingerprint:${fingerprint}` + - `c=IN IP4 ${PublicIP}` + - `a=rtcp:${port}` + - `a=ice-ufrag:${ice.getUfrag()}` + - `a=ice-pwd:${ice.getPwd()}` + - `a=fingerprint:${fingerprint}` + - `a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host`; + const response = await mediaServer.onOffer( + this.webRtcClient, + data.sdp!, + data.codecs ?? [], + ); await Send(this, { op: VoiceOPCodes.SESSION_DESCRIPTION, d: { - video_codec: "H264", - sdp: answer, + video_codec: response.selectedVideoCodec, + sdp: response.sdp, media_session_id: this.session_id, audio_codec: "opus", }, diff --git a/src/webrtc/opcodes/Speaking.ts b/src/webrtc/opcodes/Speaking.ts index 97055e0a..bff0db97 100644 --- a/src/webrtc/opcodes/Speaking.ts +++ b/src/webrtc/opcodes/Speaking.ts @@ -16,25 +16,37 @@ along with this program. If not, see . */ -import { Payload, Send, WebSocket } from "@spacebar/gateway"; -import { getClients, VoiceOPCodes } from "../util"; +import { + mediaServer, + VoiceOPCodes, + VoicePayload, + WebRtcWebSocket, + Send, +} from "../util"; // {"speaking":1,"delay":5,"ssrc":2805246727} -export async function onSpeaking(this: WebSocket, data: Payload) { - if (!this.client) return; +export async function onSpeaking(this: WebRtcWebSocket, data: VoicePayload) { + if (!this.webRtcClient) return; - getClients(this.client.channel_id).forEach((client) => { - if (client === this.client) return; - const ssrc = this.client!.out.tracks.get(client.websocket.user_id); + await Promise.all( + Array.from( + mediaServer.getClientsForRtcServer( + this.webRtcClient.voiceRoomId, + ), + ).map((client) => { + if (client.user_id === this.user_id) return Promise.resolve(); - Send(client.websocket, { - op: VoiceOPCodes.SPEAKING, - d: { - user_id: client.websocket.user_id, - speaking: data.d.speaking, - ssrc: ssrc?.audio_ssrc || 0, - }, - }); - }); + const ssrc = client.getOutgoingStreamSSRCsForUser(this.user_id); + + return Send(client.websocket, { + op: VoiceOPCodes.SPEAKING, + d: { + user_id: this.user_id, + speaking: data.d.speaking, + ssrc: ssrc.audio_ssrc ?? 0, + }, + }); + }), + ); } diff --git a/src/webrtc/opcodes/Video.ts b/src/webrtc/opcodes/Video.ts index 3228d4ee..d78827a4 100644 --- a/src/webrtc/opcodes/Video.ts +++ b/src/webrtc/opcodes/Video.ts @@ -15,137 +15,243 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import { Stream, validateSchema, VoiceVideoSchema } from "@spacebar/util"; +import { + mediaServer, + VoiceOPCodes, + VoicePayload, + WebRtcWebSocket, + Send, +} from "@spacebar/webrtc"; +import type { WebRtcClient } from "spacebar-webrtc-types"; -import { Payload, Send, WebSocket } from "@spacebar/gateway"; -import { validateSchema, VoiceVideoSchema } from "@spacebar/util"; -import { channels, getClients, VoiceOPCodes } from "@spacebar/webrtc"; -import { IncomingStreamTrack, SSRCs } from "medooze-media-server"; -import SemanticSDP from "semantic-sdp"; +export async function onVideo(this: WebRtcWebSocket, payload: VoicePayload) { + if (!this.webRtcClient) return; + + const { voiceRoomId } = this.webRtcClient; -export async function onVideo(this: WebSocket, payload: Payload) { - if (!this.client) return; - const { transport, channel_id } = this.client; - if (!transport) return; const d = validateSchema("VoiceVideoSchema", payload.d) as VoiceVideoSchema; + if (this.type === "stream") { + const stream = await Stream.findOne({ + where: { id: voiceRoomId }, + }); + + if (!stream) return; + + // only the stream owner can publish to a go live stream + if (stream?.owner_id != this.user_id) { + return; + } + } + + const stream = d.streams?.find((element) => element.active); + + const clientsThatNeedUpdate = new Set>(); + const wantsToProduceAudio = d.audio_ssrc !== 0; + const wantsToProduceVideo = d.video_ssrc !== 0 && stream?.active; + + // this is to handle a really weird case where the client sends audio info before the + // dtls ice connection is completely connected. Wait for connection for 3 seconds + // and if no connection, just ignore this message + if (!this.webRtcClient.webrtcConnected) { + if (wantsToProduceAudio) { + try { + await Promise.race([ + new Promise((resolve, reject) => { + this.webRtcClient?.emitter.once("connected", () => + resolve(), + ); + }), + new Promise((resolve, reject) => { + // Reject after 3 seconds if still not connected + setTimeout(() => { + if (this.webRtcClient?.webrtcConnected) resolve(); + else reject(); + }, 3000); + }), + ]); + } catch (e) { + return; // just ignore this message if client didn't connect within 3 seconds + } + } else return; + } + await Send(this, { op: VoiceOPCodes.MEDIA_SINK_WANTS, d: { any: 100 } }); - const id = "stream" + this.user_id; + // first check if we need stop any tracks + if (!wantsToProduceAudio && this.webRtcClient.isProducingAudio()) { + this.webRtcClient.stopPublishingTrack("audio"); + } - var stream = this.client.in.stream!; - if (!stream) { - stream = this.client.transport!.createIncomingStream( - // @ts-ignore - SemanticSDP.StreamInfo.expand({ - id, - // @ts-ignore - tracks: [], - }), - ); - this.client.in.stream = stream; + if (!wantsToProduceVideo && this.webRtcClient.isProducingVideo()) { + this.webRtcClient.stopPublishingTrack("video"); + } - const interval = setInterval(() => { - for (const track of stream.getTracks()) { - for (const layer of Object.values(track.getStats())) { - console.log(track.getId(), layer.total); - } - } - }, 5000); - - stream.on("stopped", () => { - console.log("stream stopped"); - clearInterval(interval); - }); - this.on("close", () => { - transport!.stop(); - }); - const out = transport.createOutgoingStream( - // @ts-ignore - SemanticSDP.StreamInfo.expand({ - id: "out" + this.user_id, - // @ts-ignore - tracks: [], - }), - ); - this.client.out.stream = out; - - const clients = channels.get(channel_id)!; - - clients.forEach((client) => { - if (client.websocket.user_id === this.user_id) return; - if (!client.in.stream) return; - - client.in.stream?.getTracks().forEach((track) => { - attachTrack.call(this, track, client.websocket.user_id); + // check if client has signaled that it will send audio + if (wantsToProduceAudio) { + // check if we are already producing audio, if not, publish a new audio track for it + if (!this.webRtcClient!.isProducingAudio()) { + console.log( + `[${this.user_id}] publishing new audio track ssrc:${d.audio_ssrc}`, + ); + await this.webRtcClient.publishTrack("audio", { + audio_ssrc: d.audio_ssrc, }); - }); + } + + // now check that all clients have subscribed to our audio + for (const client of mediaServer.getClientsForRtcServer( + voiceRoomId, + )) { + if (client.user_id === this.user_id) continue; + + if (!client.isSubscribedToTrack(this.user_id, "audio")) { + console.log( + `[${client.user_id}] subscribing to audio track ssrcs: ${d.audio_ssrc}`, + ); + await client.subscribeToTrack( + this.webRtcClient.user_id, + "audio", + ); + + clientsThatNeedUpdate.add(client); + } + } + } + // check if client has signaled that it will send video + if (wantsToProduceVideo) { + this.webRtcClient!.videoStream = { ...stream, type: "video" }; // client sends "screen" on go live but expects "video" on response + // check if we are already publishing video, if not, publish a new video track for it + if (!this.webRtcClient!.isProducingVideo()) { + console.log( + `[${this.user_id}] publishing new video track ssrc:${d.video_ssrc}`, + ); + await this.webRtcClient.publishTrack("video", { + video_ssrc: d.video_ssrc, + rtx_ssrc: d.rtx_ssrc, + }); + } + + // now check that all clients have subscribed to our video track + for (const client of mediaServer.getClientsForRtcServer( + voiceRoomId, + )) { + if (client.user_id === this.user_id) continue; + + if (!client.isSubscribedToTrack(this.user_id, "video")) { + console.log( + `[${client.user_id}] subscribing to video track ssrc: ${d.video_ssrc}`, + ); + await client.subscribeToTrack( + this.webRtcClient.user_id, + "video", + ); + + clientsThatNeedUpdate.add(client); + } + } } - if (d.audio_ssrc) { - handleSSRC.call(this, "audio", { - media: d.audio_ssrc, - rtx: d.audio_ssrc + 1, - }); - } - if (d.video_ssrc && d.rtx_ssrc) { - handleSSRC.call(this, "video", { - media: d.video_ssrc, - rtx: d.rtx_ssrc, - }); - } -} + await Promise.all( + Array.from(clientsThatNeedUpdate).map((client) => { + const ssrcs = client.getOutgoingStreamSSRCsForUser(this.user_id); -function attachTrack( - this: WebSocket, - track: IncomingStreamTrack, - user_id: string, -) { - if (!this.client) return; - const outTrack = this.client.transport!.createOutgoingStreamTrack( - track.getMedia(), + return Send(client.websocket, { + op: VoiceOPCodes.VIDEO, + d: { + user_id: this.user_id, + // can never send audio ssrc as 0, it will mess up client state for some reason. send server generated ssrc as backup + audio_ssrc: + ssrcs.audio_ssrc ?? + this.webRtcClient!.getIncomingStreamSSRCs().audio_ssrc, + video_ssrc: ssrcs.video_ssrc ?? 0, + rtx_ssrc: ssrcs.rtx_ssrc ?? 0, + streams: d.streams?.map((x) => ({ + ...x, + ssrc: ssrcs.video_ssrc ?? 0, + rtx_ssrc: ssrcs.rtx_ssrc ?? 0, + type: "video", + })), + } as VoiceVideoSchema, + }); + }), ); - outTrack.attachTo(track); - this.client.out.stream!.addTrack(outTrack); - var ssrcs = this.client.out.tracks.get(user_id)!; - if (!ssrcs) - ssrcs = this.client.out.tracks - .set(user_id, { audio_ssrc: 0, rtx_ssrc: 0, video_ssrc: 0 }) - .get(user_id)!; - - if (track.getMedia() === "audio") { - ssrcs.audio_ssrc = outTrack.getSSRCs().media!; - } else if (track.getMedia() === "video") { - ssrcs.video_ssrc = outTrack.getSSRCs().media!; - ssrcs.rtx_ssrc = outTrack.getSSRCs().rtx!; - } - - Send(this, { - op: VoiceOPCodes.VIDEO, - d: { - user_id: user_id, - ...ssrcs, - } as VoiceVideoSchema, - }); } -function handleSSRC(this: WebSocket, type: "audio" | "video", ssrcs: SSRCs) { - if (!this.client) return; - const stream = this.client.in.stream!; - const transport = this.client.transport!; +// check if we are not subscribed to producers in this server, if not, subscribe +export async function subscribeToProducers( + this: WebRtcWebSocket, +): Promise { + if (!this.webRtcClient || !this.webRtcClient.webrtcConnected) return; - const id = type + ssrcs.media; - var track = stream.getTrack(id); - if (!track) { - console.log("createIncomingStreamTrack", id); - track = transport.createIncomingStreamTrack(type, { id, ssrcs }); - stream.addTrack(track); + const clients = mediaServer.getClientsForRtcServer( + this.webRtcClient.voiceRoomId, + ); - const clients = getClients(this.client.channel_id)!; - clients.forEach((client) => { - if (client.websocket.user_id === this.user_id) return; - if (!client.out.stream) return; + await Promise.all( + Array.from(clients).map(async (client) => { + let needsUpdate = false; - attachTrack.call(this, track, client.websocket.user_id); - }); - } + if (client.user_id === this.user_id) return; // cannot subscribe to self + + if ( + client.isProducingAudio() && + !this.webRtcClient!.isSubscribedToTrack(client.user_id, "audio") + ) { + await this.webRtcClient!.subscribeToTrack( + client.user_id, + "audio", + ); + needsUpdate = true; + } + + if ( + client.isProducingVideo() && + !this.webRtcClient!.isSubscribedToTrack(client.user_id, "video") + ) { + await this.webRtcClient!.subscribeToTrack( + client.user_id, + "video", + ); + needsUpdate = true; + } + + if (!needsUpdate) return; + + const ssrcs = this.webRtcClient!.getOutgoingStreamSSRCsForUser( + client.user_id, + ); + + await Send(this, { + op: VoiceOPCodes.VIDEO, + d: { + user_id: client.user_id, + // can never send audio ssrc as 0, it will mess up client state for some reason. send server generated ssrc as backup + audio_ssrc: + ssrcs.audio_ssrc ?? + client.getIncomingStreamSSRCs().audio_ssrc, + video_ssrc: ssrcs.video_ssrc ?? 0, + rtx_ssrc: ssrcs.rtx_ssrc ?? 0, + streams: [ + client.videoStream ?? { + type: "video", + rid: "100", + ssrc: ssrcs.video_ssrc ?? 0, + active: client.isProducingVideo(), + quality: 100, + rtx_ssrc: ssrcs.rtx_ssrc ?? 0, + max_bitrate: 2500000, + max_framerate: 20, + max_resolution: { + type: "fixed", + width: 1280, + height: 720, + }, + }, + ], + } as VoiceVideoSchema, + }); + }), + ); } diff --git a/src/webrtc/opcodes/index.ts b/src/webrtc/opcodes/index.ts index 34681055..71c5f2e7 100644 --- a/src/webrtc/opcodes/index.ts +++ b/src/webrtc/opcodes/index.ts @@ -16,8 +16,7 @@ along with this program. If not, see . */ -import { Payload, WebSocket } from "@spacebar/gateway"; -import { VoiceOPCodes } from "../util"; +import { VoiceOPCodes, VoicePayload, WebRtcWebSocket } from "../util"; import { onBackendVersion } from "./BackendVersion"; import { onHeartbeat } from "./Heartbeat"; import { onIdentify } from "./Identify"; @@ -25,7 +24,7 @@ import { onSelectProtocol } from "./SelectProtocol"; import { onSpeaking } from "./Speaking"; import { onVideo } from "./Video"; -export type OPCodeHandler = (this: WebSocket, data: Payload) => any; +export type OPCodeHandler = (this: WebRtcWebSocket, data: VoicePayload) => any; export default { [VoiceOPCodes.HEARTBEAT]: onHeartbeat, @@ -34,4 +33,4 @@ export default { [VoiceOPCodes.VIDEO]: onVideo, [VoiceOPCodes.SPEAKING]: onSpeaking, [VoiceOPCodes.SELECT_PROTOCOL]: onSelectProtocol, -}; +} as { [key: number]: OPCodeHandler }; diff --git a/src/webrtc/opcodes/sdp.json b/src/webrtc/opcodes/sdp.json deleted file mode 100644 index 5f7eba38..00000000 --- a/src/webrtc/opcodes/sdp.json +++ /dev/null @@ -1,420 +0,0 @@ -{ - "version": 0, - "streams": [], - "medias": [ - { - "id": "0", - "type": "audio", - "direction": "sendrecv", - "codecs": [ - { - "codec": "opus", - "type": 111, - "channels": 2, - "params": { - "minptime": "10", - "useinbandfec": "1" - }, - "rtcpfbs": [ - { - "id": "transport-cc" - } - ] - } - ], - "extensions": { - "1": "urn:ietf:params:rtp-hdrext:ssrc-audio-level", - "2": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", - "3": "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", - "4": "urn:ietf:params:rtp-hdrext:sdes:mid" - } - }, - { - "id": "1", - "type": "video", - "direction": "sendrecv", - "codecs": [ - { - "codec": "VP8", - "type": 96, - "rtx": 97, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "VP9", - "type": 98, - "rtx": 99, - "params": { - "profile-id": "0" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "VP9", - "type": 100, - "rtx": 101, - "params": { - "profile-id": "2" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "VP9", - "type": 102, - "rtx": 122, - "params": { - "profile-id": "1" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 127, - "rtx": 121, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "1", - "profile-level-id": "42001f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 125, - "rtx": 107, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "0", - "profile-level-id": "42001f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 108, - "rtx": 109, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "1", - "profile-level-id": "42e01f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 124, - "rtx": 120, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "0", - "profile-level-id": "42e01f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 123, - "rtx": 119, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "1", - "profile-level-id": "4d001f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 35, - "rtx": 36, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "0", - "profile-level-id": "4d001f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 37, - "rtx": 38, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "1", - "profile-level-id": "f4001f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 39, - "rtx": 40, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "0", - "profile-level-id": "f4001f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - }, - { - "codec": "H264", - "type": 114, - "rtx": 115, - "params": { - "level-asymmetry-allowed": "1", - "packetization-mode": "1", - "profile-level-id": "64001f" - }, - "rtcpfbs": [ - { - "id": "goog-remb" - }, - { - "id": "transport-cc" - }, - { - "id": "ccm", - "params": ["fir"] - }, - { - "id": "nack" - }, - { - "id": "nack", - "params": ["pli"] - } - ] - } - ], - "extensions": { - "2": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", - "3": "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", - "4": "urn:ietf:params:rtp-hdrext:sdes:mid", - "5": "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", - "6": "http://www.webrtc.org/experiments/rtp-hdrext/video-content-type", - "7": "http://www.webrtc.org/experiments/rtp-hdrext/video-timing", - "8": "http://www.webrtc.org/experiments/rtp-hdrext/color-space", - "10": "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", - "11": "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", - "13": "urn:3gpp:video-orientation", - "14": "urn:ietf:params:rtp-hdrext:toffset" - } - } - ], - "candidates": [] -} diff --git a/src/webrtc/util/Constants.ts b/src/webrtc/util/Constants.ts index dba1c511..f11c95fd 100644 --- a/src/webrtc/util/Constants.ts +++ b/src/webrtc/util/Constants.ts @@ -16,13 +16,7 @@ along with this program. If not, see . */ -export enum VoiceStatus { - CONNECTED = 0, - CONNECTING = 1, - AUTHENTICATING = 2, - RECONNECTING = 3, - DISCONNECTED = 4, -} +import { Payload } from "@spacebar/gateway"; export enum VoiceOPCodes { IDENTIFY = 0, @@ -42,3 +36,5 @@ export enum VoiceOPCodes { VOICE_BACKEND_VERSION = 16, CHANNEL_OPTIONS_UPDATE = 17, } + +export type VoicePayload = Omit & { op: VoiceOPCodes }; diff --git a/src/webrtc/util/MediaServer.ts b/src/webrtc/util/MediaServer.ts index 0c12876c..848b3f73 100644 --- a/src/webrtc/util/MediaServer.ts +++ b/src/webrtc/util/MediaServer.ts @@ -16,62 +16,62 @@ along with this program. If not, see . */ -import { WebSocket } from "@spacebar/gateway"; -import MediaServer, { - IncomingStream, - OutgoingStream, - Transport, -} from "medooze-media-server"; -import SemanticSDP from "semantic-sdp"; -MediaServer.enableLog(true); +import type { SignalingDelegate } from "spacebar-webrtc-types"; +import { green, red } from "picocolors"; -export const PublicIP = process.env.PUBLIC_IP || "127.0.0.1"; +export let mediaServer: SignalingDelegate; -try { - const range = process.env.WEBRTC_PORT_RANGE || "4000"; - var ports = range.split("-"); - const min = Number(ports[0]); - const max = Number(ports[1]); +export const WRTC_PUBLIC_IP = process.env.WRTC_PUBLIC_IP ?? "127.0.0.1"; +export const WRTC_PORT_MIN = process.env.WRTC_PORT_MIN + ? parseInt(process.env.WRTC_PORT_MIN) + : 2000; +export const WRTC_PORT_MAX = process.env.WRTC_PORT_MAX + ? parseInt(process.env.WRTC_PORT_MAX) + : 65000; - MediaServer.setPortRange(min, max); -} catch (error) { - console.error( - "Invalid env var: WEBRTC_PORT_RANGE", - process.env.WEBRTC_PORT_RANGE, - error, - ); - process.exit(1); +const selectedWrtcLibrary = process.env.WRTC_LIBRARY; + +// could not find a way to hide stack trace from base Error object +class NoConfiguredLibraryError implements Error { + name: string; + message: string; + stack?: string | undefined; + cause?: unknown; + + constructor(message: string) { + this.name = "NoConfiguredLibraryError"; + this.message = message; + } } -export const endpoint = MediaServer.createEndpoint(PublicIP); +export const loadWebRtcLibrary = async () => { + try { + //mediaServer = require('medooze-spacebar-wrtc'); + if (!selectedWrtcLibrary) + throw new NoConfiguredLibraryError("No library configured in .env"); -export const channels = new Map>(); + mediaServer = new // @ts-ignore + (await import(selectedWrtcLibrary)).default(); -export interface Client { - transport?: Transport; - websocket: WebSocket; - out: { - stream?: OutgoingStream; - tracks: Map< - string, - { - audio_ssrc: number; - video_ssrc: number; - rtx_ssrc: number; - } - >; - }; - in: { - stream?: IncomingStream; - audio_ssrc: number; - video_ssrc: number; - rtx_ssrc: number; - }; - sdp: SemanticSDP.SDPInfo; - channel_id: string; -} + console.log( + `[WebRTC] ${green(`Succesfully loaded ${selectedWrtcLibrary}`)}`, + ); + return Promise.resolve(); + } catch (error) { + console.log( + `[WebRTC] ${red(`Failed to import ${selectedWrtcLibrary}: ${error instanceof NoConfiguredLibraryError ? error.message : ""}`)}`, + ); -export function getClients(channel_id: string) { - if (!channels.has(channel_id)) channels.set(channel_id, new Set()); - return channels.get(channel_id)!; -} + return Promise.reject(); + } +}; + +const MAX_INT32BIT = 2 ** 32; + +let count = 1; +export const generateSsrc = () => { + count++; + if (count >= MAX_INT32BIT) count = 1; + + return count; +}; diff --git a/src/webrtc/util/Send.ts b/src/webrtc/util/Send.ts new file mode 100644 index 00000000..7f8ab4dd --- /dev/null +++ b/src/webrtc/util/Send.ts @@ -0,0 +1,27 @@ +import { JSONReplacer } from "@spacebar/util"; +import { VoicePayload } from "./Constants"; +import { WebRtcWebSocket } from "./WebRtcWebSocket"; + +export function Send(socket: WebRtcWebSocket, data: VoicePayload) { + if (process.env.WRTC_WS_VERBOSE) + console.log(`[WebRTC] Outgoing message: ${JSON.stringify(data)}`); + + let buffer: Buffer | string; + + // TODO: encode circular object + if (socket.encoding === "json") buffer = JSON.stringify(data, JSONReplacer); + else return; + + return new Promise((res, rej) => { + if (socket.readyState !== 1) { + // return rej("socket not open"); + socket.close(); + return; + } + + socket.send(buffer, (err) => { + if (err) return rej(err); + return res(null); + }); + }); +} diff --git a/src/webrtc/util/WebRtcWebSocket.ts b/src/webrtc/util/WebRtcWebSocket.ts new file mode 100644 index 00000000..5bb2da46 --- /dev/null +++ b/src/webrtc/util/WebRtcWebSocket.ts @@ -0,0 +1,7 @@ +import { WebSocket } from "@spacebar/gateway"; +import type { WebRtcClient } from "spacebar-webrtc-types"; + +export interface WebRtcWebSocket extends WebSocket { + type: "guild-voice" | "dm-voice" | "stream"; + webRtcClient?: WebRtcClient; +} diff --git a/src/webrtc/util/index.ts b/src/webrtc/util/index.ts index 66126c1f..264f1ecd 100644 --- a/src/webrtc/util/index.ts +++ b/src/webrtc/util/index.ts @@ -18,3 +18,5 @@ export * from "./Constants"; export * from "./MediaServer"; +export * from "./WebRtcWebSocket"; +export * from "./Send"; diff --git a/tsconfig.json b/tsconfig.json index 63b5e96c..5d408b24 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,4 @@ { - "exclude": ["./src/webrtc"], "include": ["./src"], "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ @@ -37,7 +36,8 @@ "@spacebar/api*": ["./api"], "@spacebar/gateway*": ["./gateway"], "@spacebar/cdn*": ["./cdn"], - "@spacebar/util*": ["./util"] + "@spacebar/util*": ["./util"], + "@spacebar/webrtc*": ["./webrtc"] } /* Specify a set of entries that re-map imports to additional lookup locations. */, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */