From 756c86dbbfe3a12e3176f59acfd0c56573154fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prokop=20Rand=C3=A1=C4=8Dek?= Date: Sun, 14 Dec 2025 15:33:15 +0100 Subject: [PATCH 1/3] actually did not use AI for this --- static/favicon.ico | Bin 147482 -> 18451 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/static/favicon.ico b/static/favicon.ico index f0429ba72f14875632633968fb6ee19cfa95c67e..bee70d584efbe48dfe062c1548cfd6c4a8fe9b90 100644 GIT binary patch literal 18451 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

=~)67Ov0qI3MrGDYOTj1##bQ?6D=3gU zT_!~bl5wBaxboc?;cj`u{p5fQO^BwWQ(9P%*wfrDmR9d5ejlryQAK~Ij90N;+=de9 zl$3&;w#rb~EO4CJxqNTW_Aq!HC94UT2>*g%A+KjP&vlLC!Y}ty+%hHm#%Pd=OBwHkd$?lG#+FxC}h|ww(|p8n{-3RMhG*j3*Sz01$a2LVGw_c^O#{+iN{N zVsS4qcX)Oi@QPtAT`PEWMcN>J^FGh#Pc5qd%XgzAF@(WzerR$f#-TC&r-WY5rgZ;pV1U?3twTI}I2^34^{3|>j)nn}Bpt>AD{MNt z;gqQ3I-L5(NW8@1=m>{_618400svv`F>zLPu4qC~nW`aaP1QjOau6@*7|K!(XV!4K z$?@e`wzNej$}+^Wwxh!zjaIg_lxAd{Y-A$sz@0FDV_=kge0?z3=<_MyL+W#P%W~$W zgY_nT-S&oEKkSh<9OzN+8)M{cjEqLALLsn}(6(*6olY}QH5!d<*sxl!*J&e* z*49RK!C>u}3J#+=maQ@)?e&hb+1zHcCmi+=dk}!VP19Z&J%j=^3^T0Bn>BN(1yIgN zZIMVrBod59V~YAK*W_5OSyro6r#m1s>t(Lsmg6qFczAZPi&i13lOxmvwOB|yW|>%p zEZtL7zU8|0)}eEX+L3aBZrw~dMat!4)qrj%&uB_b)nc@+MZjn`R8D|q;bQxCvwnY_ ze{@GU*KW_T*_5I(GUg|dRM$Q-1W4Wgws5$Y$s~mZzZl!;4fx686D$FGSmrFV8C(=t z)+5NP3Y?a)r4uh4UOK3NuFHkyNb#YpWrIrY#2H%asT6iVgtcWX+R7qYVIrkesmpZ2 zG7}2<$dm?|EP`n#E5az(Sgm%8#adrqcQ6n-rit2`_3GHPp+mV_ypt1ZFBI59xz;8A zn6)mD(G;*rjFRey$IZwU1_KbhroElAdbh^|&sE9)6P6e_&SkN<@mOSif@WD>FL9o1 zYx&h9#*Zn_FUvBRbRc@dRZ&JF=`-Ey_$W$`F+H6Yt;h+x95gB2%MUA{@E25H6RqpC8o*uw&lFQveo(o4r zsWlYb+*s(3RwO^C@?H#4G?2_)w%Keq7$EOqnc2>>bv@j+o>~1*>~;F6QiEBqEaJv8 zF_IutixO!at~%9A!vnfjs3&oa2#ryHqLmnW#uyc7W+`wXM zf^Ub-0PWO{bS2PYnr3lLCIxWSDjfozon}rk44$B6Tk}cYa3Q?UNQ9)uI_VE=(!wP%lI5P{KI=zl1m6wT1Db;=KDh5p`fkYL8o*^UkQL>Jl zwr~i+rX5y^BvR=~2HxqzKm%$p)-8apqi^B_fJ56S@TcQ)}mJ`*f~<=pwv5%K^0ETtov7M3oQg)n1d&w! zNm3|gA(F0T03GO}Q04Ko#A5NZ2bVQ7M1H~z{fW{N{UiKG$f_wkX9jeAyuqBVo`=Il|(wLiI3w-O$)6!5+zAW zd+=_Tt?tG>J)FXfdsuWZIpku&$WvWi}rdZV6}=#qhTM%0v2sl$q7Q5E*!4Cq3hWP3nysf#6v z`U!0jwIsKN%^VvF$C4(qvuqUF$1+md1Ezs+x764Ed8q1WIW5i5r52=oL!Sac+69&q zWr0jefhGXfxW{{ZqeA_vb|r;i(&>6v6%uqVsxYFFka((z$1 z^DBqhGNGaXq&Mm$S?&~hu`~BZ|4f!4j?OwF=r4E60PWPUG%S*|COV-FT82q6Z^RIa zTN!3=B*JRMA2*9LtiUoU5eEI!@AoAz>7djBKI8-OT*TFk?d(t9q{9_ncF zCeP7h3;N_TtSS)7P_!rpDG#VkPB97UOv+AXz$Q2P3u1y&_MyoHLthaPB3=OlWMaZg zN-Cw7m76E#l-qs{Sgv2T0C$S_qt zAMH_|&E{b_PkDwj&qlpTNm-&pc}B=J9hIT1C(~U`g5W2(&yYGQI;tgaS-743atAO? z?P3+D;~yNK*)vOv%QQCDDuF198V%^|ExMsx85Ls9!QOFYX#-i~bR&iV9>q9WU^;8= z;Qnh98z}@)$<>`3+D+(sP%o&vIDk%=>YRu}k4YgWbq;0nm7MVX716e~#d29D9E}Cy z$~1JSvqi735k$a{N))|MlexPRS^buyY|!Fx;-vQJ6c<*51(zbl{%f|1Ugf4pkj!RbLED1`^yVJ73W8LRYwE8RLN+7Oovf@~$@+N`D@dbsv6fjl;JUvM}IRG?6n z|4T?EkcYxIy^;`1JW$yrezq+i!9*YsA@5v*xVMKw_(A`jjk}gt#$(Dn1EL$?gpw?R zCTZ+&l06{JicqESUQbcIKUU;tV-z=uoNv>2h6OUH=~0g{tZ$kEtXLs1x?PG=D`of1f| zx4=s(MYy)bAQpQ0c=Q>WxA_!2mhk3rc2k zXWf0TUVY_+U##T;MPWq)M2VBTR+FS#OE8$p$oAcyxHlSC=q?gMh=fvdb2G>nr~L4G z+v4#!p_Ckp#G9*HYF}M*TB8qo0Xa=U*#@5vv>P3Zg}&HYyLNv#81Cq3QhqH+nq5q$ zz9__maGlq?RY=)H%#1n^R3=XraF|)72FN$y#_goV@f;j0YrJ zb~!OUT#LMYz*%r&#u)JtZL%e?$Ti{3@XoVe? zRNN|%ShU+-kEeH5)`>xO!|aVahUVoBFSZpprOrjF z<%9#;i9E5W5N`B`cD5ANbc~9Nay+)PwKb(#c)<>%u~n8kbaF&_34t0>cOg$=7$-FW zlU59R*xAq&FDNu{#2$}FB@;({Dxd-#s#MH!OG{6; zdyL6+V0B&P`o`vLtA1dfvCz&Nb!sj;8i~@T{zC0-;UaCgTqA+L@mikdc$@ zuQJ1b#fs$`^UG?)zcYbD#UFi*a>^R!QDVxpu_diKhRWl7mc@d=xLEP_C8Z*ch@qm@ zyfQl`8jZC@qn=PGocwc!lu)>g#*GY9%=6%-mrTE*zon~KqH&kmjKI4@Su{mvV`Ar| zGpMF0c!QBW!C*);I-R~1+U<=z-)dwdU86k;5h^RUk#MeSAqr4~ygHZBCIyalNRl!@ zQeQg6bk4vhGe9{_MvzI6zVm=Av8+%MSBE&pN2ymG``x$0~g%2{aRi`ZBv~OQE8U63Z6IDaAH1z-J+N#5< zj_%INcOIi#^c5IsE5yj~`CD~T9Vyt)AobNh*H-3og*k4&)9e_OXD!XxN`{e5JpXgG z?=+bZ0I$^XEk>nkxTU2z7K>3YHm&%D>6R>X<9XdrW5>^Mz(^n)>g8+J&nkvyI zdqc9g#l*ut=Gg5&*K1Y)4nY6yaK%n20ZbW7UG>>kAItV|yMLA&K)rgnT(umx*TA*# zl1AjU$4nwEA!*$Ilp$h9r(=(v0S8-*DtRbaQZrDR4s|I3oaYvuw3g@KuBg*aj%8Dd zK{GNMIBs{ZOx9(JM&q%#z>Fx^q}RdHfX&C)KEMam7ddXVLAT4S1n%1CS9LdOVM{^- zq!jN{9U}h875fgC0Vx`gp8rd@Xq|eXNdn9DoLH&n>N&2$?baU~O5o>xva+H)zs6xQ z^vjBiaFYb=jMKQkZ{~KEDRnrk$G~P#zsTtX{w&e)J4|wrNf}w|lzlbV(k&^p8lW=Z z*fpS=Pwl!S$txvw=>hHfR)hGXo)>t&Z)T?9sCb~{oOQasSy`-3x58ow4$F({d5AyV zp#*H1YjdunnPvNBWjQo68;+nxlj8tlu)V8zeuIUr*C{=BI^7P3E4kP!>0z~&Ae}^j z$)nc*x*3heGtM}}YPD*|{#DbjStKv1i-{4z&I3OqZZL=o40=|tAC!{=tT-Zj0KtTl zGcY$-)a$>o8XE>=iFQK_bdL$@9%tpq;OuWRt?eAw2LYPJ!X5T4I8qujASWloZ2pep zmRPxJU1#dFyv#0UxqiQ2@dupF4?p~H>C&asrcK+m zYnM*Pkz7UR+N6mUtCgIDx;Pk(AIQ5Wqy&$pdY+LDjL7K6#>M_wS?#e{YcSYOrVcO% ze>pbd1CTPuYK2cy&GBn8jJ}Et$!%t1@kCXYI(P@Kn~C>zFWSkBX$|#uyAf|U1p

wYuW)*cVal^7ko<|UiIIGyyPV{#G0=36Xzb~_YKPv?vgVb_uN5g`{k9I&^zk*L%Cpx3W9a_g;3b0_uX({S;`|Y>?_P4*$iMZN{jLA!-B(30@SU9M2Nzr_n&lK2xF{w;qoD5?j zFNe_)d8A|Ta4r#3`8dwcu+=)YI@1(&o8k_`A*P}y87GM`c%w#7w4yCsB+5=iQnG9! zuNk1fLHqEG103&V*&2@7ZsObZQWGzUQWD)&*>}5hl*(ID{g9#u+L$077srl0{_eZ) z0chLpcAVV0b?fV|zy8<1{*^hR8uZkhl-&-N-get-FJqc;(6!GiJ-2!8l6r7?ZFSHm45aT!Veupk*(@8oqr=9Iu zurg(SXiBvJ1!aWSoG2Onf-MlYMMORRB;%?KS&%!<$Q=f@*}(caDWbf9KuRg3R*xGte0=57gt7<&7!mC`%)YQ6D^BkjN5);`| z)(T6X7|o>D3nvYZ(j`_!iHRpf6%_BRB-OeMvWDndd&{I8YpG8MO8@-lN6$O&yupKq zkQpnjD^{$y=bpQF@2)x=svdz;xMazaRjXFPI1e5?I3ptipBG(pks!pYt7}4`Q1WDD z_7HV;T>}rAnj-~ySf$QHAjc}ClJuW8^{5J|oLpj+`jxgA3*aJ%3eh3M0-0P--dmC+ z=9+Ly?-oD&@WYQj`Y70Nm{dKo=$%^H6HXX6 zW5(mdhYyFk34-!InfKp+@3q%nH=Fg^1sJHXC*u;MS&cYlsbem&fJRpfMx!}5x1gb+ zI(@lNR+Bo_hAe;!lgUhKSmi|0u>0IuO3C>loO$M% z7K;U6Z{NQC%{SkiIdf*WOC$bl3{ZJsv6!#9=9*h>xfNELQ(g(>U%dERU{rN=6?&t! z3Ys-an3Q#hQ@Ewq8_LSc_wB2O6-Zm7HGRES`YsQqsHm*IzE&LyCo9G3Bav`=YAn4T zG^sE>xllLZWhysXxd(NKui*l22U9 zTIngyk46L7oScGS&=(8_wA0g4(lROQsi)@TsdICK!&`r>ih4PTz~!b035oG{lDXNwi?bUG+MZD(WXtCK#q!v3b)%m zV#J7xFPh7h&USM^eOP_05@Y^VF#BidT)9(l0`wgn9`*Im>7_9mh?g`5 zR$CjIKD5iIpYVK|o0|kdgqWXt>gk_+^2vo4UIe&@PApoq2*Njg`t&fVI6id3=V#Uc zhDmylHytLMFk$>tPyGeKx5;DzN-kJ1f7Yy7AAL02ZZ~nAP9&GI(q+neNh#}?XazrI zS(B9EisIt3*4Ab?0x8*;l<}$bJbs!gq(r&OXCM$c>#Q>`x#ZHxlP|GatswpW{rms^ z_rE{;?6b67UOOf0-^T#0BS;U>*X-=f>#x7znrp7@-McqVA!c5<@TJwPCiGu735XU+QHop;`BXsFZc>8h0} zODrnwK#Jq>7(}eTejlh{Fc@t%yU*t#WzvRIPd&3wpT01Id-m*JwQ5N?93sQgQcg}W z{%dKeBbA_xAVkKEn>cXb5O^kljyZE?heCc;6|{vTG=ZZ@C#cGy~Vbd-rqpmH6^XHs%_MADN0YjBlXk)SU>(^g& z(FJ-v>u{KzPD@6H#qG9cX4Pf`StTW| z>#x6t)V+n_Yk?387cS`Aw`ktHFJK#c^vD`MycZ5;&z{x0cfp%)z6QGc{l5D8y8ZiW z&p6}Mva&4FNM>=d8_lBef`W|v{0!U+cf_sH0dxbML6^`0_XY_0F?7#pVwj$BjDys&o0}7gbhn&&Y7YFi?lGJ1pvEMq3~d zKJB#AF2DQ=V2;6HK;zZb)z3cr4Eh|2L}(_MIoz2q|39cg%1f0vQtuB~w>|&-%$k}S z`mA+z`(JzQ)vT-x7-xtZ#otJ|%R20Oq3J0Y2)q$KCjM;`_QAcie1&1al( zN<~F6d2NE6n`;|1s25?t;_~u>bI+Z$ZQEA3j6f>j-uCU=U~kX@ZH=~!Ti`|%aBypM zproV_o!Pv36ZQWCfxx_Za|aC?Xf_)YZ&gX5KJ%ZuC7Xno8v^Uhl(C3)-CDR0=#$+3cmf4{h_EZ^l)1|xHF^yi#2Y0H+)!C-LY$ial}RwD0K+CFYXr5+cf1FKeb!Tq=2 zesjYOH&CfhyxNRYW5Zv9`nsRDxy}VgI$>Q-3`UW88G8Qx_um5pCQqI`ZQ8WFy!>0${yz5n{G$8sfAFT8Zu#!J#igaCpahg49vA2;8CPF@ zL$6-F>3s!3I^~q{7hQD8{{8#Vcu`R?O!tl*n+*o5c1C+*T?g^F<4(Bx>Z?`n9@zNQ zQ%`>W`R7edP3Wq2{@0<4JO3+Gp=-rSY8W7WufF;!9+d*_-o1OCe)=yK3&2VbOn`|d zI-#gVetzb|5C8G2uNJIXvwH2?HJ^RmIE?4GRXPtfj z{SQEy0PTnnzx;CE3opE|apQ(qVsd1<;Qx1te)rx{@)E47HYgFp)Ty_vTD6Ks3>_UE z3l}b&IPnaew^$6=A;M%g63s~3Efzg|b{yb>^l$MK`diu#8k{`&;>C*>gXlCg`SHge zufP8HX;Pme_$>3kv2}|`DkxP{^t}7-yINXWXbl2Dx38!}|D*=ztP;gjy5V_sf1 zC8)(>Oxq3WuKol+w^~hO#*BuG3DT2EdcwYa`)<1F#`5wqE$RPlnDd_!E2OXi-}dg^ zYxeBfU;~BbZQHg1{i*c^9kJ{6+_-Va8w@-sYqP4cLn>z6cvb+k}07 z`|Y<23i65Ir;`3Z+Q+PU*Z8bGD_uO+gspMeN ziT;0@?gzsqEhlguZ20!uZ(){cq`G|h^1JW8Ta{&XXOuU+I-Y&@naavasy7V{4ez}3 zPG|LJ8r{2n&D5`(mKj|y%7O8nGGz)}Rhp|olrVq({6T{T8I1-?S*O!+!37uW+O>;C5DSix`pb;4msj8}q3V2&v+bgfU(z|!>$&)94`Q?}71;2{X-nen& zRaaf5A^op(|5>*&W5&Gt>Z|nS)-Vi>jg4e}sFGo=uCAUoZCX}V=C4%x#kbL;N5dEZ zi>Nk$4d9Ai^Wv9Zw_kkYdH$Yz?m?9B@y8z{bolRmkpB-!Pe2+1uNfc!00006;4V~PZ-h1zjBqV`A3LQcy2?5f3LP7$B-ccYF=^`pBML>$80-~r0 z*z5CIpU>WXc71QZ@4sfRx%ac|w>;2n0@p}TfHwZrkgmVX9wH)cie&=x9{8N8XR=cLE(-& z?g+2G`f3s#Nt9sAqu{+j~>Z+T1DLHizi?int=_~L;5E^a^h$xp(( zdGkW|?%l(K4?b9Y+5Y|c&woA~I(Dobi{sh89RqI;4E(F!C&!^4-;(Uu=aWu4Dg5w< zKdi#?-~ayiaMMjUg%&MZRD5Q_gbCrxU;c8{`+35-e)=8!osR?l{#U)m+ke;VcW)Dn z6|tB8d(fak;l&qUEXVGXPlnE2x`elAOqA_A%D?`Z>3`;{qF7K z;NOooZ*f}}abCn##lItd50v(J)5eYApa1-46<+`2KmH?3nKH$2&K%IxzTf`)hpVr- zCj9lUf2}&!kAM7Q<3x;)eB9_fE6yqTcRm+vx0kPpoH6^OdiDwG{hL ziZ6APE@We>i2L;4CBJdIPyBC&e_?=o-W*%*7X%{t(;3z%%^`Be)o3U|1XaH!u>YP zr+A2e7jX{W_uglpaLJ{Ymh$ZX7SEo2=9yuqU3MwOH_vbCUN%k+Jow;n&9&DCeA?n> zTwmj2Cyg&3pZR0SZ?t)=a6Pu${(m9-7w&Bt*2{1XF9CbtnOISAGaD-m?svcejbmE8 zf2X+j?$<9IbjTt3zy0~D$416U&)&TQyryh}>yJ6~&<0Lb^d-vhU${QovHvE5emCx@?n?Q2!%M|}68haWbs568-Vil*>ixL(__|0cqI;ocSf(R|K#3`ATw z2XEcBZMgHUy8`}bar@S{z7>v|I@Rp7W4jcu(rZpU`Q)&R9yfR2S)aANFmCMo;&|A7 zuf4+jg$u(kfAy>4sfIuO=}*RmTI{!9?qAfxfAJcnzRq^+zk#v88CZ9m!(n#YV~?<6 z<;w8;-~YY}Kfn3SZ^Ft|s{$CSz<3#^b(~#F_FeT{Q+wIx?771C6vx6Lb#>w9TW$&e z_{Tr0&b`XtfU~*%_i-N|w`2bejPF$I-xRE;Ha*c(;0fRS<~OVGf~|kgd*2)CTemiw zTVb;kTpN~^W3y}7u5q&KrtW3O;ko2Hi(`RtfWPqchd*5PJ9y%iS6&HYr0ZAs$Bum- z6Z~(-{u>ngMXdAh#79f-q0i}N*AuMw=+&zxMn3Vv3t`ZZAr0Il;;jnb5zo8nG2fLF zyxW%AljG)NCmj>%IEeGXWW|2Va+fQ(esUdhyWbf03)g!)=h?>NqHz5;3G32C(EYBx z?z&Rl@4x^1zr&of&(8Tt8OCd2dw2P#Rl9P^c8~jB$}KolIb|oGdTQvbSnJ->e0s_@ zv$yKdA%9=hF?8IjF%-vwjf0Xgu&2IreHXjc?UZCMZ%qLH{E=5{$9qz+itiayj#5L-uvzg z*T4PkVa92vg_iQKi{I_%?UXd+O<@Ls#K!nBLPv`Zu^S3`KsjyB>3_ z$u{m0_fe{6{rI!bmTEkIQjB!CwDLV=+u2W(&hHy|$G2ph`F4|h(6aaX>pbFLpR-^= zHa6lp*c{^XOUkG88&drD8!#YzHNZ0~bhVy8*_muv}K5x+? z!_6Q5@Q3ioqmPEw=baa>P%gkj@ zwe8#^_G6AaE`0y{-*<@QkLcHT3#0Y2Wfj~dvFUC%-IjwX$2d55yPaavaXm;mW{-a8 zL&4`1aNeS2OWPkw>SFg(-X+iX9y~Zdx0^K&_QE+6pB|PApU-{b6Zw0O zJo@NtOvHJ?&Wm&NF%ZY7|CW^hq_)4VO`C9w##&kK|Brt3qsFo=Y^Sp;sjM#PkBpX&r_Vc{RZcbZ+nc`95H@;_{hgTmSY`o z{#<#GgXJIWD}1v*^7!L9W)G0w5AOR&3-#@0`M&<%nX_jH{L8ZKn~DYV`Sd*U@y8ed z`vv?nPZ|E5|7eV-*zd2g_fegH+4j=sJ{Lxg9&NUyX!lc_za96Ri1CE?xt*}bICqwE znFR0TZPkiHe)oId3p1s8IM#E26s)KGJlaUdOexMI#@!z8tN7>0!ww7o^FRNS!{s-s z7r9Z~ypw$4{e;c^m^usn_h+U5{!d4>@jwF zKk0yXz4M(pL~asKXea$=e_`VQ;kt`-cOKbO%pW8z{s3|Mej`TYpS|_YJMDevELoC2 zcdhiwk<#-Ik{|A4hwrpq-*58*=Tl=0RN+72K0e+Z`+kk~(R<+fzc2BFQm&ut`kHf7 zdylSAihJGPH#I)D^f*iLp4f_9mnq>g6DCd!U-`;ca(wt7aOsCX_(6;L_uxKJPe&eJ#CUGIg=?_=F4;de@X)Sj&v2ly+*|hha^)HSK>ZI66kfsjqv}`Y+adCK zd2WEXk8ih1!{NCzq}BL#rgTY;$M|5}3>3%Uk?*x#^8)8n90L_|Kx+RJ?qjy!Igf0A z5&QM}&RSgmhyV3o#`PPs`8n>npYi@~=lGHTY$D#<3m43@?k74vc>KZ_zZk}8EaW@} z-!$Rt=9kKj8Gq6JufTd@(~CHF8|(JlY_~809%o8R^bOkx9@2B`BwK$ly`7Sr_m=8` zz;gq}jPZ|#+wQ*Go@*~o^xee&{=UY|msB6fala$p!5B)%K|ThAea^EK{|Wyw+Yk05 z{wuIwmMvPhetiXZcEJ4l*T1&; zn0Pz3-8Dzo)SJY*Y`WoG9KktD70!k4+3LmQb{m^~kors>to>T`twI0v?N>UV4@sLp zS#hX3<=KPr-r_OC#8cQ?@|^b8#KGcoJ>*~WKK9OvAKmq?cZHw)>}P&RdwfRmFTRiO zweQzC6vu$u{|fvk{KvU}lh}`1md%xs>z8R^#`WcAkcVg3FLIq^-u}0@_nUzCV~#!6 ze7Fh#=pXb+V*Wi?+_uWLYmDSrkG4Iv>&{0`#|Z9;AZkM#~7!;rsg=07{^8* z+_`f&gk2bAZx!*|UD&NtE`sfg5{CIMBh>H~MeVK3jZ^v_* zz`Ms&2WsB@v( zKXM=UeYp-*c3;@9;ya1W=X$r(wYW=hjxt^UJKyZwB7c9ycoI zs_dHn=YRfZIQIDCEnlEf+m79Wv%98V3+oZ*_=#pGqt6M4sK9yTBW}M_e5crMEgNpT zY;tSuZP*nzX!agI$7lUm`Pe8M2abQ8pUpWb{+;{iH@P3o9Oiy+E$o+Z{R$sfxF=tO z9Hv@a|2f&D{sRYQHviul_f6p4`dUbveAx7!?xpE_Ped`($d>%Z^tf*J zXrp8Kc&Qo($r!MC!0{jLzw;mG|BUHU?7P;9uUp1`MzkJvxp~YE?^3+(kCC3g?lBxD7WPCfH7K-Cp+1cI6JVk9!C4u%>o@uRX?N%V}@t@lMQtU^*!}Wph75PqT^F5xM+I+`7IoRtjxS%w@iN0>@lqa{la+?kN|0cM1 zpWXE<*Jmv6QN6#)wpZy%t$bbT;J|xRTqfo1Mb2(IOtkG4K4~LZPwlyL6tlmH-A=Kc zV_G={9ogm2=DYlZPH|tbc5VK*Ki5&O*Za%3e!=FaJRg0a!shG#aG%=ZFhog z$8V?M!$W6w>67^`J*Z1zA0utZpX;pG^7ryNeV*g^F?~GvIPi057so(~|J44cwjaN@ zX!moiCt}~Yel43HwH<0?CP?GL-nW&_7$0LI+$XQo9j@Q=>Cb$o2z&eYv(jAqX|8hn zXOEEw{7b}Y3!H{A&RDP2qn*5&17u&U-nzuTS8(@&pPKV_-7|PQw%xRtL~ktmrEr)c z)`v(lKSuew#>L^`5$9%eix^L^ZTOX6*F}3*X^i=<*i%>S-3t3j();~6Ubj8@9LKRe z84o@V%Emy7f8#%i?a#3vIe!KAqpxdRUvr^r`}nxbmt0rp`8n4~IR0Mx+jV@${oxuD z+`}0&XIebE2!3*0?^M3U!8&#Y=7pCxHSP;|AEbHu#iTwiy$LwdYFO4SgUZ%H4 z{W0h5&SQl4)Ia53WADna4o66By7Lxnt6>ao0>&fG9pjE|!>{U%x@qsOe0ST$&AMyv z;k$A$dldKh8t=0`es+%X|9k4a9M`|2Vhk`3Xm?)3dActE|EaM3(v#plW!T3Ks;Q9{NF}L}Sdu+b0JJ+B4!2K}awy5bOJjeS`wbQZRt(a|^ zo8fkkUj5UiO?y-0o-wAKweF`Dg4=$MIwO zIPmZE`GE5=P6Q8%94O~MwXmOZp6Kg3*DtmCiGN$6>m<16{#bot!QMN+cihv55U&53 z8eIQlAOCphqw@nlZzhhz`LaG_SwBVBZ0p6h$;;bZyjO8|=Q2r53ESni-Qxak+jULN zwnzO1duVoDdP$j0H+v4>h&Zpnc!F=kwXocmT{fD%pYjH5SI?P#P3`f${#lM=V?*P@ z$3m|-4$?8ed3DqIx&3D@D%$>{-RJk?>*joi{0F$c+kA2w+~&J~t9w?(@izze@xBwU zt>xFTZZzvhzZr1fMO?YodU)2OUH8N5vtJ9|)7$}%FS!QkeyZ{IqHT|yojD)AZde~I zzCL(#=rm}2=saXX=rVL-=rU|l=sMzv&~4P=p~s}@9oPMZ>;CF# z*0}erofJT&IA0)AHT7-Avqr1A6Sx=bS#z#f{v-Wm?OH^`zg3 z*xyt9jrAeR)fbw8>u@h<#y(#;`DcZ015OP+M;|ZyJJsyA;ZhiPd>f{vK@MV9ZXEky z)#q01%AFYOd*L9OeXCd(&?@zq?9O#Rga@21U^;a#=~ zUB)#TVoJnK+w@WFzvm=j{It-a!+ha;VQAleU1-(nhEP{`pZ3SKKcoG*P+$M4(7N?! z^x9|i_b2q+N3}nx{r&d-cJ0p1$4A$GXNEo#PRj9ZxE7X&uqz%vM74H9eb*YAL*qV7 zkG}hJ_88yG=j`*zxEL_?_|S9s>7i3s&L4~~32oY3rE|Jh=lPJ%@56S!oIiW3Rxj%D zc|HFT?T_g1yFMfN-{pJj)-UTlFV}p%RjW_yI1h!kZ8sQxy7W3H^gm*zVcM{)_`@)9 z)#2=?oMQV3>C{c_{$4)E=ZEPyLykLHSf69x)3M`9Vfik7_s1GNzgoYG=Ucb_oId}Q ze%lQh{yS+7=t;iMkj&=8^HZ)<wkwX({qa4zBk3;)A~I$fLxDE@Xmb7b>?~# zQ^m*1@t$bz;Jt0nF~Yj$ec3~7AvkTo!Ykq6gP#lg?e}EZYp(~xZo9opobe8=WqGUg zf}3?MZ?gR@Z@I;u-({D3!k&A+Pw#&!v~2l8a-3IXKR>2_Uul@^G3e|t^n_Ex2=${J zsk(FaQKz4t?V1Zm#eK9MefQ_=F}~M7JMhR^p<`!k`YL_T2P$yhs?}%1fd@Vl_TKy9 zu>0=s4m_YF! zmJe6Kb>R3O$C3@RniYK81ot(04q8r%`W%9!TeeNO zJ{oFT=kMe2SL*w}NcjR4YZ~eNov-CKz7~gf`<~)G)!!4mV`r-H?!IczUZGvrqYZ;? z+T3V!yx-%44*FQwV~_U=8@J@RaC~pdBV%Lly&n?(Kb7Mj{?nnun$W4sf-q$IoG@Db zI>xZek6}MlHH1y={@yXOW`{mw7_SS2(VO)fK5O5Djox?PC!_)2v)Sj*m}ZPHUfQ&I z#g5ym)l2%0+x2}5#s6l8_Wfn^Q;uKsZKI}>+I!b?EFVAB?-Se?W3_JYD{Q^`S)?`P z#AsO`vDVsk=$c#G-d~{HxpX6jd<*#k(#<`8!p13hkC;#KUX#Pi9+-WH!;2SM-n;xG z%N29|9X%4fr+fzcp8Kl3L;Ef#>E12Y{Qbckj|Uv^5wq8ug6rnKmoZdVm(3wyC*uqY zLcj6z!&vog9e2(-VVvr^*~j~?oP+Vc$LIV#AD)wOy!!f&+PGzYY}a>`;hq?V>zTQ4>-Ik7If;%5?zw-Kr=a_3+CBC@YWL0M z_Tb1dh2J<%n(P<8@P!n9p|7~u*2et#iXr>0#6SPl^kNlm#ruX$34VPK0-|%$&4WHI;c;{CA4a^&d z9P$bKKE`d^wpYov&I|4P9$JcfVjUK@E&8=7?sfkR_cda*hWjG77f!H`d0lPfIF2oF zHb!y5bR$=7TVkF*xrXNaIcJuU6Q1_{?{gKj@1}O#x?sGjS)$ct|p@{n?xP1l3f!DKxd(XFj zoiM*%`2xP>n2+aFYU86ulX99g4;~+GghN2`_A=6H_KzIQJ-noW1O(EKzMu1 z?iqN;|4i|?t^VF=r#r=UUW_*S37yx%F#MRM;Yj6+*{*dEjx6qaZPZE2;%~?}*|x-c zdTsXH^Zj=28Q-~1I>21<`w^A6cfYo%V}g5W4BWR;EyuZiNq$02{jy-|M=S3%-TEuO zu(j2=hU|CwV`Xz#bMgo^TW{>URbeDzu>~qd+1Wd$j;B-zqR>$+4&ZqLj%Ru6W{9C zaaril<7b_x*A@y}==`rrNB`g(<~L+|Cb)mhu5q@)TUl zk-g6~Oy@YduN86H0>{a9oS1KKu_}Do=+CCM{`J88ngsLoLCrDW92=kH7Qkstr%7~4 zIA9g0Dfp~;K7su84n3#oUM!b=%;Kio+GlOfcsk&KOn<4Xd(dz*b@e6T#4E3~{e;V} z2;KTD)%j<I#{-(W$CTj4eMhVaGqh$Q z`}kQGY8))q>zU8H%P#l6;eG@0tBm7(($2L*@2OdS8o6mPPk}r>!#zH&b9>Fv+(*wx zGj3l)$0^3D%%3GMA@T*dz1#W`NiS!BI9R!&TO0Fh63nB|8|F8|##6g(wNAPYo-3K_ zlFn&LIK1b*cj$Mh*-dg!xF>G}4(vEDKJK+wriIkiJ)PP5Rab%X4-mk(9&(FY96*K=B1CG ztKQ_kRbXCY(Z}Vc*m%~{Yl_nl1IanOI1RPV9mxe&tRK9i)x1&gQFb10dCSIJziij; z^3ea#)#0dRR|x-THCg`7Ysn`l`_3)-+|E1SY1f&2Q(_<;`%JUCgZ`=$8>zY&_i5?b z>D<1I<5cK49;=G}Y;$e>Fx3I18~@&#^|QpER!dvozae#rKd4fp*!xJz5QV)02iH^zcnRe`0Q&&mMcc z&vGkUwD@3X(c*E_eK=<{qg{5nTlcIXPjpkiBYi*GYsTx*E*FMQ{gtDw`@sEZjN@3& zeIZtr`?CdG4^Hs=ykAJl>s`YsVjg~PnBN+>kCKI^`v4I}TdGp!koG+Tz!3RHUxYQHryDxX z*1|l;WnZykMgEoKRC>G&E^juTyqcsJ)5*#ur*6RXc(j_LHd)l-Q@>T$FV|@zr=ibG zr$H0VPG)+?E4q(OV}^T~KZ z9Y;C$$EzNom3%Q`v_*e5xAnpU>+7a2os`t9O!(@Ys{}L4%HXSEhceWrIZ|j0^By zY;;!B)TQU5F!+e2VazEj!lZ?3!}Rkn49BX5{dldFLCol6)$z`_{`zpr+ut5ez469y zn%2*mam@{3`kHIPgxQybp_9+kSdfOH@zTEiMK%ZcnA(5;r<=6-wQ$Ar%m;@Y^1S`N z4C@a_FIi#V)qChlegCE5n041n@7x&9Q0>C$>ebKrah{wn=Y7(ZSBEJpFAn2QUl#_9 zTdq1y)q|)$k}*S_FXzg!vvJqJzZyF)Uh~*2b(S5w&9?i2tv8N?t=IixZgyLr<|Wj$ z^#xv!t*;fEWgU)m8?D@8$GqWwOY~)R8_%OKzf8WlZ`1~wJ{fs@F?SMvhMhMXpX%|= zweiw^@sF50=~F?kBI{i@5S?nXP8YV$(7La)v|jgY)kU7EUW;emc~?00f*Zo%iR%oH zZQEX>c_)ioqQz`Zt{b-3W1n?(&uTtdZ#-hm2^+%9>u(EZX?>45>if(0a~_-z=f(M* zrt<{%oHyss?>JGvhjBc8{RWNibF(?1<075sJ%)L(zbQEz4f@j~_M5wm9GmqI#MYPi zvt`<4DX%Z;^+jJ+9EaF!5%Y(tr&R^!h11u}aUAo^AKI5Dn5Qp8ig|L$s_H?czvjF~ zx$y_duD3{Y49en9(Z-wK8g0CB8u=^qZR|4WSY7YMhI#TAYhj*zGwvz8A3ND|$igsb z@iO6kWjG4FD@QcP`{kD#J`C?~ds~>PwOLPE&H10A-;v{9zmIXvd^Ym74 zFrV@oj=e;+4e3_Pcg=GrV_uENT#r`0!6C|pC+3>x7?kn&f{mwNBsSi9)eQ#oa2s@w z+VLoA0yE6t8aj7h7$z>nz6 z&QlM@`C1p^?7QC;1{}K9u*$r`cTchU)axWzRq*D zbDc(T&+q2 zTlO#5d1_?SI;z)<`2w#Yw`S4e#i^M$ygTMAcuk4#TFk4-W3JRb#acgd405fez~iy; z)}wSZ`IOW}p*y~!IjuTQ8k1FzLU>;atYqezNcJ9w)8A` zjPP~-Baei6>bC{QN>C*7JVwkD7`Xc-~pdmLIfh+jaDKn}e+{cxvmThU0mc)*Fi2650BS*sRx#*Yaf( ze$!9-P`Zs*{sXw&V*I8VnBO(#Jm)rE^UT#9U2}Pqdnp$7AEIKkE3S>{_>M znbnP!;fdN*?3Uk`3ZHeu2D|SkXic{3gemh^#eZ`?Q^0%9+Y{TqP_`WmVv8N?OCNnS zEPL#+u>A4I!`yeiKXjGGiEhq$z*}l@1?OOOb@!@XYEd}np7)34ANr6ThvRWvuz$rz zKN{BQ*oJ+5f0fN|3itT6(xdp@;QnY`2YmQZC!z^0)V!2&!KUUVbR~Wh`VaP`+wh|@ zep9gZxrUSGpNYr1t>0u{R{W-){DpKIuO6aX67xk~!?71GT9kgJ`K%RsO>=oo;=6jT zu;s=RbB%g@s@1rSr$;J1N;`HY_aKXrl;s{Y*rJC__sBUs8jb2UPLNM`($!a+u4np8 z!ryaW)ww%fyFh-01iVH1`{pG7X=6fFkbkTQi^V zvwH8+aO$et!+P-;KF@JD9%BgXuhGAk3;$;G_5HccPq|JJ_h>reIp){8j;Y_x@8>$; z!{giZ9K2X_%ms#J;#f`j-dTT>S9H$O!Ui30eir^#ZtJ67Z~14^B#L=Brdzr%>oq0x z2PyMqBfkM>eG8aBOn3m7TZG@ZjptE3-i{0A)sG_GY|W>lUejD&Q^R+)p26^%sK=w# zxQ(YLF#UPE_L(CtaD&|=`im8@M30Zm&ZB=UQ2q6qoYR;FFHS=ayX)`nufm5-n@RAF z-)MM`yxna(+{5j0iGx0o<-Veczrctz|R=MmqzOKY|m?y>pNzja+F=Q#<#&-Hu#Zr3oKerHPyNk=1Myat#eo&rICSRl?M?Pxu6}eZVUm#WM^z)oel4mnZkF*A3K&E?GLClGT&gO|^0@h0iorboKJ zTEsit-MI|BJ+%#0B^%GsA^#f+z^j>{GwSNuqTF$w+w^v;2&=ao+Yqh=@ zpX2jo*oX6^e8+h{=Vx5UeOtHpuHh&8y=$0Uhm_;=8L>qE?1gr2^lm6#)6}lG&)urk zEumBQv+VwGpSWMSFN>`&i%})~20Y|F(C}r+!AbQS@rFF_Ot!U%`AJ&aq5|{aRoLBP zm{-p5{Q2`!$cH@?cdE(dHBVXGmAVjNKG$nfUQ^3=#l}njVto(eHCk(i^#arhoMPtJED&#%CJ!gI{8m4(6gGvhJ7Zmb!nrZdvqYV=Yya{6>7i^3D>?vUkDG z8|H;eaQ|BU*fK6p-tfEv=J(Qh<(RL<<&CE-2J33D)&{(W z^$6(C-lf-EUH7-!wJ*y--*?}vM^RnfyDj!U_1q0Mrn}p-D(BBnnLZ8X`d%?c1 zl}%q8aR>fZYYbqIbF71N;db38J`vWx@IpBMlb;NyUUR={0@n%qwf6yI*9+s%+xz(( zpU?3x?BD+CtC`)`_ca^$u4AI#qvbfa_Z)@DaZJaNKRb2ZdgW6vCuM$jWARz~zJhtz z2zrj!nh&~9+%N7M>$=TV-nr+VHHY8id1sAq`rev1((Q2i0Y}WgCQk3XhR1~0kT+b! z{NB>wYig95r%aP~%xfOYbL1Q2HPX90&!FhLvOW!K5Dd~<1^p(RTEQENzASxa$k8$l zu)_tR*WhK+KIb$ZZvyYcnq)8CUv*z~nd~auVg+2LXxp*tj&@e69Q%=knH5So2${UK2HW=QS0+E4)VQY_RqQ>vRk| z@g&)L^_SCH0-5&lxvE@2dS_sd>g!(+|IhLckG=5fJg>mwPLb2NCh7V+_%VM~{N3;l zpDEzocnr4P?Yd*#aV~sc^tsQ4i(Y;?T+IH;E7iN6=XKs^<3VEp{O8z@b|0?9_f_E@ zUB_`B^&I2&((iMhHsv^}j#IGp_&xYP?th>w6*(jSCMJ?yS8Br-gdSt2nU2i;f{zyC zvNiRaSWi;M>A{i3WD`!`Q#>i%Cd;?0iFx6;*|>Mir~a$P630GAeRR`pANdSjeZ|%*uZO8hl6zqh8}xHyhlFQep0$s+Q)2To<~ex zWjpC{ojNZHqfa>B@)BK_tl%{ASxtYB+B;gL^LMxJYh-Uzyu)G4u1BorI2Xn*5ihyq z^Pdlwva7Fd)vo7xo%b;&a{Q;*cbgw^&-dr{KI)mK<4eyWcFXyLdyCgbj^nvGa2(4= zi#a)F>z&u1&-Fp8f5VEUZXf$uGd6f!x}cMS;M*cY9__i|To}LXi(d?ved$Z# za`rEOxq8?0yw3X=4}8wCkKH$$uj3ovkehI59?>Yxdas zM8oMdgq}M$8|Lwe%Itim5oGo29Xc!zJw}{h_lY=pm3|{lJ&jMb)O{zfo_dmEOtvXb zU&Q=Tn)kN^=2QPQ=kwTk)v26S!2G`AUMc4H6UN)<_jo;2ns3k)m(S}$l6*3*VXoJt zyhdxUuvQK0bFh{{pHcK|Un1ORxm{>B#acKtXE4v4oUIpimp-G~2gN=uSNEECm~E`! zG{jZWBi&!Efp@q%wjHcuufZ-DHjIDeE8&W-e%1CXzxK6I-2Ju7^*-L8W8dvQoJYrJ zT$SQJ`n9fOqNT#g_#LT!$y{c(K5F*ZdgJwphJ#)|^r#iG3G3`S(eJhv=BYd3TGD@_ z`_Pl^J{jf{PH#1{&Co2tcxk^;ie0U` zQTdDSv-m2U#`JgjjIPg^1}Xk-eyVU-fp^(x$NCi!>sNmL>*1(JrC&~5Nc)78igF-~9LH^kOcPVcoOo`+DT)908MzwtVf?#jnXx2fW~Ddsm< zyQ#oDab4}q9~^s*@Re={$j`=q<(%+0n(e=)x;$K-e1jt96E062%DBANo@8A;))`~n zCDyNDJ&(hrp-x<+e(&R07Z08Tzt3<@th18i-(W zt-UoYdf*Y`&Zb97p8;>i-wSw$ujhD|U3aX5@oT>It#FOjcfa=A->%wup4WLF_;>8* zHs85Uiu=g#UC%LYA2n1sj@f#}6WrDpd3~k1yCwQI^ zvDj_{PtPzfO}UiQ>;B@ip;_iRXRgyX!;b}D;H+B#^V6i^flK2z5vykFBR_AJ59?f> zN4N2fVE#aH8PjZv{;TVh9*au-*O*6{*Stpmwb9sX%H_ptoXfKY6YJMZP<)uR4akol ze)Mvy2hG-!%xWU=w}_(@V~f~*Vv1?4bnDhH3IC5;&OnC$1!3?6;eX9-VabC}nC|ZW zs`DAcJA7R>8twvi4eJr-*Zt>zhU>obopAkkznksf`(FMx&+|It0sQCK*Ks%=T%Y3` z#~1Eh)4`W89W!b<Fn^}FbGp^U{A=-FJ@)GUYmB|-8fAgYvrZH1)3AmI>l%e@o(v8Q=G5KpCtzj+vS`;*E;U{e13feco)o<6gdj^BnhU;Z`Zf zDYNyth7;qnYh?S^Yo{gvo!0Wt3jF5i3obIuw``f^$&;g6yk7jU4F*!g0ONKvziYlYym+q%U`j!mimDsE^wWv%d;LOT>emTE!HDr zeWt@zpUxT{tZP8N;4v3o97Y^X-O@tqCsSXa)%U<-YTZZfCE7E7K(@AI=JRI!2hHbc z%QdG45L?*2@5*rG{F}q#2Rn&>cCg}S6_}S^?>c3(G#mU^#goB2xx*HVD#@etd;{0z z;qt6UnR9v7vK%4IlQT@6>rv;b4rsAz;O1=z!=?)N*876mh^$7oxjj+w1JJ0k|0(}r z3^0c3>+exMPu7>LXa7~^E2j(_s9jAOASq^pl^2jqE@DsXg*< zp6B%(`}!Q8PwhRvE&LumDCIbA>n{}U@i(ybXiRy07QG%{*7*&XN4Lb6jrj=9Z^*$R z-;!Je>IUhP!|$xsgU2!7rVW@s)AB;Ve1#wDHG{6*=zdr9L`dVYNxh(HH{>9YZ`KIr zkCkq{)i95rp#2Ql)^s~W9IOiSaCvc?9P??;f&3QBrEHF7ljPXs{;Oh9RsO5E{KQ2| z!iW==hW=xhs@^SI-yJ*54kzi*;au&~-`ZYl_pxQm+BF<*$BK{epWSx5PyGKwviIJX zJyAT9tu}|{oCP2VcKy1v!8{VfBtjZZ~4V9^1nCgd0yvz*nPA4 z;NE$Td|UHt;r5Z^RM>j)`gdwP;Ln<^Coe&9D)(h8_zn38C73^z_>^mws+;Z6AN?!K zyP;oO@mlb2=ksmaTphahos(f+8XLJ$W!g;-<>54@-Bjct2tO4$2+_`W5+_Qx9P`3v znO@l(Zj)*@xept84fWoq7ck#acs9(7bEUn?y!N$Br%ZDWD(V2qrBwd4<;xc}8**$k zkEOAwY^~vQ!ob7TKT+#3byN?Fwr#K0y}DmE>M7af7c00qHp}-~eO=Hfyl*ABnzb|w z?m6{Y^oaB|)BM=vCAMz8QRlNbbm_Jv9JA`4aM`Qh2)F$5m*G~e9c}xsf1Ur$^Na;- zKQ`ZS?>q;;7LHT2^~USb?9p&yP7Zkq@EdX@uF-SnO1mUR<@^Tyh8!HruP@>B#AL~} z>^*FmuAle7eY^?GUuBp_TO$w9Ft7Vp%%?w5JhZcN#Pf9;V!wLy%J$;92Z`6GIS3V) z7rww*#{%Y$lMMuy*yA$H8>eqVqu*4_*GZ2lV!oC(pXVHqGmo8*wXan=rE>E;ml7?& zbV^~q7=ImhoUUOvu+AEp_X%g&I+cz+9?@&)2ZHZPzeV<1{1hC!=IqCS&tT8UBWxP~ zk5$-zK1&~2xDVRrgwt*em%Q}#@Q&a9HoW6^zsnx=_iYjXj(fBB;zP#oo#RAXZ#oXP z9$p_c9OpOWA6rgB)Nd{l{#Qu1OfgUW3wfiKcUFRV>I%^5`yI;qE!UfksFh#YVCNe! z&;80VPmX0;LxNr@ZjeZW1ptD zXS!_`=8M|AV$;R^;fflOf=)^Nb>QT)j6<|(bG>k#>5=I1tPg-Di4ATHqh)h7x@9&_ zUeFl2!{&;jh69Jq@LyS@k{%ELxKjT?`+rQD-Uh?|(JSr^H~jFY;f_E2A>6JtcESH` zzxj>XeZxIGNBka+W42y8ru(zldbCS4oFcy=R#xQn74fN*)5n;s`?2I9P&4M3?>~0A zu7zX1x(+1&58?I}=GXpuVsa~1p73bQ`CTjFW%(vG#Nw*WE;T18zsd1^)&X-~S zROPp%Tb<$wn{4N6V7_Kul&~B8L@rcEA z5HIODsAqy8Zf&8^Yf*h^%bwLsGB11<~3rzowS~GJ5d_m*2ery=_TpLTyw1E7h>=j`w5U=P*x)a z4yp4YmzcSkK3(kiWAst2RpU(GUT~lFRay{Ey5w%&%IXIzg@3^|18Fv zjgiky_3I$LpsOy|VupxXm?ozHk`-E$O58MQq>x($Hhb ztP0H6%r*14l(?WB9wzw_p(cq^|BB+10meoLnWWU8}#b(;w8B$@;*Bc#1E6jK4bB<||TxW8Hi#*!rTCm4jW#Ezi>;uNG3PX=r z69!LOqn`5WPugjb?$a_IBU{f8+!yPCGJ6cSe;{=3x>$WXo;3~ummnr&xx(Vr%O3hz zn6~sCp?e?Y_=u~}n>e-oEn9vna{kJG*KjM>h_bcG)`ii>UL(9m-4Z``3)uNv6sslQ z42|A%ESqEJ`wT~`yvZ<4K2kat`rqUkb1yn}JU8?lIdhv}9vho(*x+rAdFGnNFrVx5 zHTZm!c0Tg?{zn~W7-o$Nu5+<|2mO)f!8>^Y#8f6PxybSb$s5E+qxO}#{Ir{H3x}SZ z<&@ObWw>X4@O#N))@upw{b?ObzYYGCeg}VNJp{a``{HoUJD#ZE@*H+_wvk#F-}yRKz1JjO29oLDy; zvhT#>t1!R0`uuqHGU%oJoU)h{+IYI5oo`9Zm*pN114uVw1Ymxmd`-_iXwuGaMjV!W zj#{yB?+uO4(yh;8>(NK=3gU@rzcTV>$)6>UmN+Ew$Wg~`u>Qf^KVq~g9?31{dDbIC zzaid9?mF>L;-Go{uJ z$Z1#F=c{x|eb1)EVJ(lom|NM5IPCG4U2d_ny1GZ~_Y;FluM2;B{0=<<%=bU)Sc{i$ zZXA}n(dN`j6){hYw1|1D8Hu^XTO#*>SZ}(a&x854viFX8@+4fV%=x^2lh*}qPQFd2 z?sJ9tY>m?5T$|%X9Hj5)B@KNk=~<@y5%1BLVxD{x@=lIga;=UI*9Y(4jtBpL5DB1{?gPk(~Fh{iO>qsviu)>Cw(j{)RP!6l9#4OV5&EMqVW#jif)+3s{ zS9;9ln5WOpCf39@C$F-l@{QB&blG{!C9aWU)||Y`nwXEcFJhkDPUG`lGqRQP=R0+q zCp(l&-WRdH{$|Aq&#``rhWV|WKTqy@x}BjsU&FlcS(aD1 zB`{CEa=KMvo>;8+bJ~J+Qk{C7Bg~g;XL;>H+v`G~k&E*>De8Ub+tQeJGhW(FhmLE_ zW);^(a38j=?(WcYz)H*4CB{!Jto1s;mvvqrbxYH59QV?3;3x2Muz&Q5yYyXIk9Wpt z(RQNcV>{{HaLQ%x$^BN3Ls{ppmv^_Qc6?$)l8 zqDCp*sA1YFm{00qsl`e+YPCvhz_)NcY}bCXg@=p})33m8(nRijo>QqjV(LJ+rp360U-$ZYdYG~1!s5_t z^i1m&?e(w~J#MyeJuJ1T=|-(;j`_6j!RFM%!t3=p?|td@u+-e98#TG5m{0sz%ej&7 zk@xMBze|3R=iJncOG)GP9%em;k$13E_W@tCvfn&;EacCi+mo}{Z}ieIEv^|w{eyG* zBEOlu@G6_b>+7?4Vw!{C$IQ6KqHx^$+p}JMs>7h)z~=Nln7a5Ks>?0cTfvg&BY>lm zFTOv^GXU?NM=88}&OG10IeibxYNU#Nou~_}ug`p^efFtdOAvm~c<{A#`b<2rN~dfz z9_6_f7B^CL^aqKwe>8+4%^j-k- zuF-F;{x-eGvpy#MC$c_eDL%oA^Go9X#4`tsUnY)oVFRa6VpGIpk6Lz>=_79Ai*tAb zuX)hkx8$M6vwqvsMmDFv&DG!drse0j_C$S^+f0A-cpF@L){PG}^fVK9_ZnEM{dTQJ zbIuj|`$CPwvYrQNtw?kG+e}+`v0dkuEwgxcS*<+JfqC>S((-PT7RmL59k)=ik-+&*UARSS5Sj@R5iW}Ujtl`YTu zC>HrJ`MMqp2luRjOzr-F@hifiGuMTQ=WNh^aTs&*1)=94a$^=KKl~P(pP4(-H6qba zz#q@IR}JmBQ?9h!+}F~_?4%9%S-g_|$l#^8?wj|vuCLE@o0X3}o7cWp;XUc`ai#vg zK;v*r^)Z_;cfDO3k0mhQ`Iz)aauG5+e^=-vJ&j(`xn{GK`Y@V5GFZ}qaFN2~Wfpf5yK z?|Eqf_(#1)ovi!8y6st>M^W!1FSupP3{!9t;)vwEU^l^d_PAPjzC|`F%LxS6=^`g6-R%AG!~mo%^r!6|La%>UmJJ z&xh_mYeu>@-`swc`w3I&MlZ<}^P3uva?CfTS57Hlo}QUim`}8uw$d`k=vwfb=wb5O z`VvnK*!nCln0|YotFo8advw6GrVpFM7)a|pcn)l*9ErR#YG*SWMsE8e^{%|h`Uh-k zU*Zct^HMnd`uBzYqch#Ft}e@u;GB2fxw0=HWZqB$2bx_e?t=XS7 z_V(~T2lP>XEq#f53~cC0T$V%Z>vWNO$2jgic19(aua#To>q^1pg$a5bHLIVKVm{v6 z2At6oINi-!-0se@;-cDcspYv!+K>+AFqLT`c|SB zjz0bptE+2H|M`_4dM3<&=cD1o3-1YIj=w@{!!9*UP*Yl0m*v`k>1uwwQRkS=In;~O z8-LZ4&zY|ZclSPh7HbuM_deWPqyPNWHROaY)cI!IrL6v}A!hNY-G}~19Ub~M(eI#Q zeVwv=%A)@&TVB?;56sj1J>BRHzeO=$qTSHrKHYk_od@&sGd8JDtLNRwC#c}`-rGsM zhdwFv2uk~zWjU7O(;C@m~o-rLszSoV~z5I*N0(~FR*>o zv6qD5M{Q6({srn`qxGSNu2H;dvEBcSKP}fhbgjnM13JGKZ45Dv4mson%}Lcg13gc_ zRjU_t4CzhFZ!_&b$9m*4-lK18t}`-b2EBRcdzIyxmeo&s?lbEJcJ47Z=QS0*oVMaR zBQ50DrWSOEAw`Ch5jnC$GnPQoYBXYB+1v3V$=JO(pN9R!wSi zu%Gc7w9rQlv*glpZ;73@Zv8glm-^6a^!N4lTzx%$!M);0AJF3y`b;)Yfbnd9#khO6 z7INjBLN2|>(&$mrzWur|*snZoWJMrSR0k* z%olpkZ|U`Ns=D!gSp9Gj0n zSgdpPd)C+^`Gb7E0ke+X=0Ac7dN45W6l<^iyU}PG{N&F_v)^Fz1@*RY7<^Rh`Z{Tl zIhRph0lE?E6#BX%UKdH-n-gtWhtt$Xi%=;imTXZ?Je|&&+=Q&>EJ?KZ^p)40GJ^oHR-D&-yd|qO0TkK_@ zVQa$j4?bOi^WyrjroXGMyV-b8`%-%ECa-%{z8yJ8>H4svPNe7TV$EOp`Y%`Y>*seg z@S5ADB}k(b&owzN}?A^NpMZ@(`-l z97%my@vC&r%%Q4dp|{zH8R|DN?i`z=v02n#z_W{d1^woL1D>&dOVn@dvdcT0w9!Qj zl>d99)fY0iv}loOM(KCs8@6nDuekeC^&DIhPQ2)@uvVOfK1{YN_Z_|6IE*w%`c9HR z!#WCRk;Y}ziw`}LK25%6l&yO&K4X3PWZx~nAg&2(y*z5Hg-otBIoIRQ%=$ObXBO^K ztu3Xw!&&UL@8QQ}yhb*@q7QM}t1s!{)D*7)A631GWz&hj`r5GA*4@P=(rtnI^HyL! z+Um`+^Ro42oPL6SW4iTGOnGOWOU`f5Zal9z(doU0q_p2nnSK*{b63Tuw2npE!;-$O z^yi+h9Lg#B{iBszI{NfEVepiBp?iPUmm(HL&eRRU|AU$XvUo}vXNPkz9^l&KDCv_@3vb5=8K$O+ETGrklKLKH3Su}ASOFUYvkA(4C-M^ zpC_x4RsBJ(TWYQ$)?Kvq`FcCCFFk89jGMD444Jwl^cuRvauKpIuvGfQ6`HHxAv`{y z{Yhc=6T)TIXRL_Z=KP!SLY#(LIclBA<7wND8uLu|>^owu;<2}AZJx(0Pk>y3e8-l% zJ$IX~zI(Y{k63rxj<4$LzuNj})ccj;J+32SefeUq?UUoW-_#aRV=#VBX5+mks?2xA zx91vQFZeyZ$DPpNyQaAY>b1@N%6-=)4#gT9wr*M)dle3fJy|`+Ce15Dmp9A{XS)ge z>9$a7ZGidL%xipHlyLg-`i<$BmhQ`T&@ppgwk#JR zwe^Y%yRFaHA5rZL>u0jg)M50dkLxi^UbR|l?yL?Yk6#rAO?qxjf1RLJ@=IL zPN}(SuN+8v9zlfNqhQvKZM-Yi(^&T z`edDCUxUQf-HB^WO0bU6JTG8_+shv8Yn1567Wm8+vnmeq?1#cve$@A*1H$ApKU297I>?rks zU`+DZ~z{>M}pxPS0fI-$C#{R-Nu*KjM&Q0zMJGR<(>~?XFulalVoa3D1<#s*#sczdj56;K*cl1b| zXNq^ri+B4T@y_q%cTZlvR{3+pTyHhJ7jvnd$9o)sm>juIeUB`+@!TKd@owY27i-Lw zPc?b&dzw?`y(z7ijqpM&-}B{DUIWhPagc6{Wy>6!j@4RNZuav9ep80|6VwmcZGFmb zrpxwPo@MIG3iB;g-&N+zHp|whydGO0`&kZ9PO@u9^W!`%=7hfre^2p_&kEl4ds%O*@2Dlp zlU^gtqv?`M{mv?V((BJW2GwiKiTOHd6NL_tt zoqZL@k#>ncYaB;9j&XZ&9Ow47z94IAg8TLB4F^^Bp1N^*L1`UH`X$g0Dz*8Bec9L? z`?CA=fpqMr`1dh@y@sDu?L3eEqPfEMh;zp}$7NnIyAF?W-k#caoge4P`EuU$dSKpi zK9l2Jzjx$uS#M46t6A2Ma>pHSi{pXyUoTc}%{jt*Ltg`116Fe`J?6PD&9d?0HE0*s z-=Me_Y^)C{^<5*cam@eU|NUPM!KIG*h|?{C`GRISM!z%N4D*_Aa(;s^D}K|MR?pbp z7h4~-`ZO+EwDoPo3!T?@OxK&z*el~W1CKr?44QgU7#21Ak>WVEz94IA!tLeL8n{gH^9;CXhSP(zA#s``}E8~Lt)SDf1Crru{S-KW4%NCQn*W*-|GEt>*_vW z{(0}ij;Zil@vG?NF-LPI_oYg!kzbZ;HK{(C@R|x6FU-RU;5E4>k6z->wR*z1V>XxOpUO8-L z>*Y7(^(2~0=?iCTP9-^)^r`PTQn_d9G1O0#(eT1VEoJ^E!_OP2MMt8gFrJ$eQ@ z9zD>A&C@dhj&QAf%+rM{v-!^R;X9G*JNB``*nawmV3%zSB;&xw19sc@I6mxo@a-6f zt4Et|{wW*=tn2%X$GD&Bwmotg&X@D%{P`XH9{Bro+4m{S*QrxgCNs;}W9arpCvW$GTQ zdpqN|vc6Ep@yV;Z+G_v$96rT3WG`Lkv<}MrR@r!B=AI*8rq$#;zRbqU1{V9UqJfn8 zuHcOQpa1-44nf2GmbUReN5tcfly;JCgA>eeXH)}`n-7PMK&+@89iJ4{?ss4KI~bV=W{J5;vRl)el2d|$TT@ozpaV<3(L<0a7tuD~^XC2|($Eyi6EoV$N&oE;vc?>FAAImm5$ z&SiAo*mr&hzvqZ$tF>OadhV;wi07Y{_1A{uc)XQZ9rI=HamST%8rB2MwMpF<(Pt2mBu-)-;-!uKM@R5Ps^JBA)nU;kdS`JKDZ4}+$h z5wQ0a8fJ`H<$8{AKV|LO+_%lSPPF;-zjVF>=FINn=O(t_{ay2U83S<~*jRuIv1?AS zM{w9vcL6bT>cZ(|vmkUIFgFZ4VRnZ5=-1|Yjx+$`R!6R0ZJLg89k=yfvcE~B~0Y`d_I zCN*x(O7VB`nfB`oc(2Z<@m>au3wkJb={+a(L8k%l;xzb9*0aWIplWd%YX6eF8t!LB z%r#vrY7=?9)rJUbfBW0tatN-H4q=#gKC-1BW$T9vXX!Rnzn!snsIJKYilx=E^;Nt+ zvGsYqY+~yx{MlGDTjV&d<8b0iW{rdW+jnCSgJ>u*z@)X6a zg!^&mnCSP4TTKG@a2;JsVpneSjpxI6z`kNw_=s=`I1iej@%;qYpl zBjoSJmM8YyaE=zE@8SFSUh`3jA?tjO0qf%J=&qbE=RN$`W#T58zv}#)S{9ErrH}M^ z&V1IFj2jYr>NBZfO>fsFi3$1|$EMR1I1Ot-qBX;5I?4w-TU@eEe^j(Abx7blwnnRRz_I(iN6ZZ)X9)Mo&2it>V^?nT z&A$~d22&N-mo7q|G{Zl(-|;W)Bpn0H189iGg=#$Vy1kG0V$0ceoE+!y7mYK%kNKvE zb$C1HIdS32&}$g^q{LXMYszvx%X&z0Z5;2+iR2b_>^wj8nJ_KyFYG?6_ZW5#?|GBn zrwN_LIn4yc#lNQ>ogY&_64T=&ry;M#W3E-XWaJE^O-lbL(`wwt7cqaX#<%M%TkCO% z?0ULk_brz*i^t_sa9^YIW57z$jm%ro?T)ho2k4}?r@cA6`DeeolzO{UJ!#uh5(bn%LZg+!t z>tB+&ievxz&wmaV$)@4|q#Va|oS1{=v8wi6P8Vi1ht{zsW>y=;BYeJW?Iy+pL~KQHEI0G z^>}m|awki)8pjxz|I?rTl*49?#{JgfG=AJN%yS;ivGw4dIjmm3{8zNzUb_8NzW8Ix zA%97|yL{ui9jQ7f!+nW<*?{{2p~Jutp<@qv`z_FYyioXUSf3kvm-XAI)+@4g=^xhn zvvmhE+edBFgH}5PFJ@hxUPBj#AyXEFv1ev>A8ix9kFF2zF@G2PFHGiR0RGQ7@Uh_I zfiaQpdLC^1d+{3>LymE{3&)?dcvTpF+%lbua{hHr)PGT%!n(>n&go%AUxlJ==ik(W z`20rwIX0qGw{t@8iBl{1dl}w6x1fmkwHlwFRk+S!!A7*YTc?uVd%8Q;n35U2#X2+W&eE)7zc{uz{dhO zF57wCcJS>OU$iQWp0QkOrz{EG`gxt>BK@wbWUCw2GUr_BM^o0%4Xk6^Q~iowSj-Jw z`plL-)1XJ1zZ&^_&SzxbQ@nd_L082FA5%@2Z?`FaoNDn0E0?S|k6)m9tcufM;}gF% zxAE|WsK=)^9<2S(|NM`CZ8%TYb8BJUk2^sAWx9=$e&yrO&#g?a_nO%oq{*e*_q6^@ zN7)Fg6_2?%H%hFddZARzZW5me~@g{N#(BG9mh`(Be z_ozua-ns6*q+R@2YnA)PoP4Tu-!%5rTRiwTzx}Ph#p0)9lpkgGy)6Ee@|7FTvm zImXc9|L})DXy*G>Z`hYnrT2PE2kFzXn`*CiH%j_)`rK7xNZ z0d~@Pfqy>BarlI=_#qnyty|wHd~T2p&3eV~oz2~`yL_&lYO%pL{YvWUp4Q(DoGZim z%XTiDQ(134&foc3t5#WkFXzoT#n!%tJ) z#6ObU7x3Lz_mfyF^EJBUNm{cw<@T?tmp*fSs?#*ZX^M4FDdyLU2W~B_`*Hh;N2J>r z`RR^%!#(^)dxfo+cDaY8cU7Wo|6NE)|B<9x~1m$q;DEy*s;#J)YpGd<8Y09e8r%L zVcWAcpFCd-?w-e2b)C3gsXha59H22VRrLs?RCk}|(r_Oxk+zj?uYBPP;Xv6a)8V5o zd5>~O(`|;vL9rghIgQ7kOplLPN^N{!>7%JudA@ul$L7|0~_wOq9ah>!|>c)%q z9^6}QPQ*QN#IC~+51o3d=btb@-|M=%cMBu*B+al(4`c9^uE9`=`$|8Ud4L|r)D+R@ z>%ap)raZ0Zb=*%2%ME^kAHVF8_hmhkpSS)Z=;m49oqNn?H}?JTGsY@?Nu0xEaks~W z^=$owuKmwYpS{darKS?wo@KQF^xv{ z2OIQ^`cS3YEz;X8){0IeJJv;Wu^)r?-JzUOk3YeA(;BFp(?nk5ct1dK`&-0$)BG}y zb%A{Ut%Y?zZXa>yvh5+&e0G$!lG^%QyOb~TEv=>J+mEy^JZq$|UW4mSi!>&#QtT$( z-l1GWbY132a1Y11M>Q2?+e^|t+i5(4`&7>{+()isHMN?%;jG<8O$=T7&Cog1_ivFf z|90UN-ji`(unBI#ADFJi{Tst0ukkrz0uB3Z=2PNVQj4@X$K_b~;&6G7NpQZ|yz*Wf z%Z%x+{bq&U6Q&z)_q-Xyx@=9#-90B5PRR8dA$$H6t;1QiEz!K|nk4aZ?nx(M0zJ|< z@V!)emg|x|3U+d%uv;_UTJT$U(!UoeKKQF&{mKuYKjOJsGQY&yZQ?LztKRx7;V;!Bz0QxgQ=0F{0Unq3 ze6eC{-_?4dzHyI-$?iCZcTPi0C%5sD(~MJG>`PiduDCI#9+aQgR@%_k!g<+o_Z6d4S_fHZ#PXksVZw?9{W;SOQysS11Q`~ZM`V2os1wey9%HIS z+n#b6>zS?V^j^)^zJcKt@)60CO!!Pk?EM7h4UW6Pm#v1b?4Rpfj&!c7{hw3X)+%*YLJVBU#s;g3zwZkZzw- z9QGvTD3FWcy>Uir+@#y1nlrtY)^Yz-;aYz|=aOofaGhzVo*EV?o;g!_!+kZ+9Ou|{$9m$Q znq7AuquQD52`N+kMx#lLtbXo`xO=0l$c-^?ZuG7zd{`2CgSLpJ* zm};#4W;7dYf#>9SF4`@sBl3-FI7s)8>z(R3UoPY_oTB&$x!i{P7`ysTa!onTi;9`g z7Dq*!Gffk&@7Pc6zQ#unurJ$*4edSY@X&MI)X-zp@uBE1A98BwskIV&PdHZn^3>~HzGaH@3Y!krBVW%sjC87_HOJmBzVa>U z#Q#kC1;8_~>z*5l??1M%ZqhlbB{1$TZHD*=_QiTJ)xbMiGjZ0#GzL>{k8aP}*^c*= z!_!0HlUiq@Z2Oq{HTTjy{d!<|Q@?Y6{q|a1zwEd1+_TcV>ckH{Cnv@I*T3HX?8{bm(_BNX5WL51 zf5bob74FP#+(P=d)mxB*t~|v)^vKZOd;D?A<2pX>sxcLg^L>)~T2G#CxTY_gVcYSY z*mC3OZqN1ox#pKmKq z6C`-&x^w-9$}Z%2V-fG3D|w!D^K^Sze(;{ksdc;`tYd6cO<2nJ{r%tm_P4`H>eYh& zy{XuH-QG`JR(m(;E)OO?k^iRj@drQrVW9t-=Ul+|in#w%k~?~@@^8G)vFn)cl)sa1 zCo1NFeX?AIn488teXDXpu2gNtyVW!72g>nDxBKKn@nio`MG1<^~Rj5gLR!BJ~@1Spgxz*Y5Y#?`bSj5 zal7IoH;Jd;uWPVI`n%;t#<(in{n>i zpOh`>Bo4y(cP*!gdvr_Rp3rs7^&I(3)6{!D&1WDl(>Xpi3hcv4Ep8iaK75DVksB2| z@r`+Z0vuQVT#EhF_V<>JKT6~2MAelXuDE1R?5k@&%mHy8Fdh`S5PZlv5!h{P_xIW( zT!kHfTE}IKf$@I2mZ!-l+@RW%do@mHN;`y)F*h+salY@DX5|}N*JY~3>8{sP9^*L# za=)IAdGlf}T$xydSAPiNB3`G|Qx$;diBY_q>9ZiX;A4Vq4Jo(m0KO z4|DYWvRic;Q{ct$uE*ESW;=#A=P|)Ox+VJMFMjb0|Ec*SH2~M@9_}v;m*M` qJ? zmnnC*n=rdrT6|hRzyXhuzm(@Hh=Y>bTD1Ab^J9+MXOi6IYc!ZeNCX>=yiejIIIW?)RE!%C=V&OQe5{#arL(c=s`q;GW;LzkK^^ z6kAGpqhAYRc?)E>iackSu1OKw{+aagGr}MJUvuA!hf5dg+*0u|?XK9>%O|fX- zuhcwmV}x;{9o}TSe@2gftZR}yQ~y4Hd`P{vJ${M~!+I6z)?e6m&W=xWrEET$mis?E z_STqj9g^$SN7v|QB|1cUT>gHy;v?jXmE}o-ch+*fRCQuy_u67O($CQ3|E2IAaqsh$ z{WfwhwfB(U^pto_*+%Z&|F?IxzjhT>821yzSfswd1)4&EDv4mCjTEV%U=>oqfFY6= zLzD*fgNe2_((uJ-DqyTZBgF8gL;)cI6F;em6<>g0#J32Dq#%$~5&dHH52(*?WuD2p zb7!A(?yX>ZyC<2vrkXT)w3}S>fNV!B5ingR2z2@_h3BF}HkOeEcJdJ)Eui z2b_n0UGm8sb(!`k%;i(^&EBNB;F%N)&Hv~4YU!Ekmnm^3)-(KBwq&pTOY;2*aq_xn zc9R#3&e|-0bLKU^4)gRkB=0N5hibm0@L-G_l6=Pcy=nbS8TaHT@J_ZTJGmX`p9h4Y zwc;SgJ$wGFlMW*P_%!*jat!#PM;-~=mfWWB>yV=(+~2Qv9t@Sc#D{Oyx;>Zg%YOsE zwSRN7{GGX6uROTSeZbl~WsBY+eHXl0_Axp){QkxF55J~vOR~i^IVa=;8BsMq@%Qnc zaviRhIn{0ABK&T~Iot*N1UcQH`Co4{I!XWzU{H;d_j{6mafpPSarKkjH<+ad=+joaaAMb~^7o?Ah|< zpzYSa7q6Fp5`T{OJHAamm|QLt2f?;op_r|4e?_D7^7nnbFU7=e)c#@SJYXOHjtjZ| zYotrTRpj!3Gx$&8Kp&J|BjyDb!wx960QNBTGs^$V<#u5=&H7@v>Z&v^He2-6SzPMQ9;~cE6eiuFtd5rgDb^`9{k7I;ue6~5S#yI(_ zxc*k<;NTNjt8vXb{^#`)3%;FemGAIsah<~w@0NinxW}d;_V&Z%tI2(dolm}A zE*s_7uKQsM_xOv~3JbQ=`0h^W*@$5k8~2(<@3JSw8tKdG-&DLkuWQS{mygkFFNkmd z?4gJDZ&jQj=7BO7ge+hWBMY-ubhj(lQN1_uGxkaX<1Nl(|1ilZ_W8NuH+lUM>rzkE z?}PO@`EGZm{8aovr%A4`gN*mA+hhG|qucF^+$5Vg#|zyr_;v1*+?^oZA9-WaXMuTh z8QOw_mwOMggJ$lS0r%u!T%fW0Q_{8W)7<`4aTdpHnQzfW==?3x=`Pq!$7`$^Q`@*# zHe8)`58>k}{!Ortz3B3k^5CA7{o3Bdrznl{TV+p#UthW@Vp_s}&ilSj@dNO5T=98s77x0o$tQik{6@!N1G~aE zQYF4`9X+p2->Kg(*Wh{|lO37MIhxntU%|r?&XH5%5%8CF!rcQYHkHrk82)&Jo+jhguSM64&D4E?x4F|9`x>+2QrM%JpI zJvPDJ6W*&Y@^Iwu*6TyU()@CV-uaVd&A(61Oss+MdOqo*5$~^>tlN9o&$#Ef z*!YzbqW81TSAX+T+6zjxJ+K-oUg}z z({Gj@M#l!PfLo~IBk9({x5H=XP6**8Ob*!X99*q-uP?kD%1 zV^@LyjC<)9bpG4~uFoC|VE^07p(Qqg&zKf|Gbe1?uOC=2=NC6cCuaN;liVcUe64&l zUr{`Er}mpXSNRaw0HI&SCEyg;0<|IsOQ^B#j+{i@`(w((U|*}ekLN$NPA>3mU0yI= z^?Am*p2g4B)9bc@;4z!T*>Zk|F(1B1**3@Pp6dQH9o^pZ%>>@3jQK(1PyB!0XV&@2 zr(1P?2ltG{I~2qJ#lV^yK3^}p&HkP25}=763YR9TSz&GInwG3TMXU4?7bXykIfd^y<`4WCrvE?u3jXGGSc3hQ%{ zyWR5r_hJtC!tgzMUDJ@AI*-eYw(8uXcgXNB1+|7VNWEAl$Q;w++|F z&wRAz{m9?ZcdOnv_DgIl^ds0W_z$0F;y_gfkb_nhY!@&mET*EfbJcZOFF@gWSo?T^ zVxU!*o1QF%Ry#9&zh0}zmwzkTau4>q#J!ofG&twE*2s5zw|HHSDY!4#E9`u&VRT;}aDVV(iiJZP~kb@BS@X0}|^K%NoTY8K`z}!C}Q^<{C>q_BD-f zT4c{v*fz#}uWyy_aVS6ILB+A&rkwxEUzULN;O!&cUs=3Q!hXVi&(2+-_ciA0weIxc zX05+v0F10tOf=pN1omAM*5+{`W6^EK>a52;w{6pJ2F`siYPmo5Nh05?x8{cU7QV+t zW1ap?`eo!XuK%(evAR~&Mc;v8>9>!v+kya%24p)&SrTl8*~g9fv+d>**wI$6ei z%>GyGVXgXJGE#zt(J@th`LicHkV&j?AYzV*Mb-bBphxzlD4BQl67r{Ljnb z@%V#>q6gvnw+(Q8_EX%feaynQSFjiR>=rDBEO2a$E~QP^^0jL#JA92|$*jNY`5t-S z(Yxh`AF@dU?+piR`zrK0{W#Re!T@p-}8G<|D!K2 z-@JMM^Bt~_y~unJe!pTDWIM5k+i9QW^YihLy%GB)X}^fqDW0`Jc3XY!znlIT`T8=# zx-su|M%~}j|0`q%4!sA1+;ZN}%f1%o&)}7JBF$S%)RBvf>v~)`7S~#*{c(o&XlI`! z-fK4x>&A84_Ha&rxNpXG`S(SGfy!xHtdze!2av-v^u8n35M8W&d#k-D%x=;id-?BK zkKd@gH1@gcQl3tJ zFT3$dF4s2gZOR_};5yzdVBD4#-zb>wmB+MBeS_9`O_ITiaqc(Y|9Cw1$AMxX$pAQO zakyHs+VARJXeiFiNcYvqft_&$*P8qXc&;uXRG+#V? z1MRnWs@8Xm{A9E_4`mEY;xFKw^0fAV{eXO$hw^id_dd#Li-8{3CvR=%&Yk;@kbT>J zfBbvI&~m}RdD@rnx7w=-TX#w>p-w2gV*4dtmH=u?NN;7<*vsfw2e1 X9vFLI?18Zd#vT}ZVC;c|tq1-K1W)Ts From 3a51876c4b3ddd73372cdacd233c5380c4d1398c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Volf?= Date: Sun, 14 Dec 2025 15:38:57 +0100 Subject: [PATCH 2/3] fix a small bug --- Dockerfile | 7 +- geohash.deno.ts => src/geohash.deno.ts | 0 mapycz.deno.ts => src/mapycz.deno.ts | 0 src/replication.deno.ts | 205 +++++++++ server.deno.ts => src/server.deno.ts | 91 ++-- tgbot.deno.ts => src/tg/bot.deno.ts | 580 ++++++------------------- src/tg/init.deno.ts | 262 +++++++++++ src/tg/utils.deno.ts | 106 +++++ src/utils.deno.ts | 4 + 9 files changed, 766 insertions(+), 489 deletions(-) rename geohash.deno.ts => src/geohash.deno.ts (100%) rename mapycz.deno.ts => src/mapycz.deno.ts (100%) create mode 100644 src/replication.deno.ts rename server.deno.ts => src/server.deno.ts (64%) rename tgbot.deno.ts => src/tg/bot.deno.ts (62%) create mode 100644 src/tg/init.deno.ts create mode 100644 src/tg/utils.deno.ts create mode 100644 src/utils.deno.ts diff --git a/Dockerfile b/Dockerfile index 1793320..ada511a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,12 +14,13 @@ COPY stuff/Inconsolata-Bold.otf /usr/share/fonts/truetype/inconsolata RUN apt update && apt install -y file procps figlet fortune cowsay pslist inkscape imagemagick --no-install-recommends && rm -rf /var/lib/apt/lists/* -COPY *.deno.ts ind*x.html tgbot.deno.ts ./ -RUN deno cache server.deno.ts +COPY ind*x.html ./ +COPY src src +RUN deno cache src/server.deno.ts COPY static static COPY --from=blog-builder /srv/jekyll/build/ ./static/blog ENV PATH "$PATH:/usr/games" COPY ./static/amogus.cow /usr/share/cowsay/cows -CMD ["sh", "-c", "deno run --unstable-cron --allow-all server.deno.ts 2>&1 | sed -u -e \"s/$TG_BOT_TOKEN//g\" >> static/persistent/log.txt"] +CMD ["sh", "-c", "deno run --unstable-cron --allow-all src/server.deno.ts 2>&1 | sed -u -e \"s/$TG_BOT_TOKEN//g\" >> static/persistent/log.txt"] diff --git a/geohash.deno.ts b/src/geohash.deno.ts similarity index 100% rename from geohash.deno.ts rename to src/geohash.deno.ts diff --git a/mapycz.deno.ts b/src/mapycz.deno.ts similarity index 100% rename from mapycz.deno.ts rename to src/mapycz.deno.ts diff --git a/src/replication.deno.ts b/src/replication.deno.ts new file mode 100644 index 0000000..e5c44fb --- /dev/null +++ b/src/replication.deno.ts @@ -0,0 +1,205 @@ +// deno-lint-ignore-file no-explicit-any +// The authors disclaim copyright to this source code (they are ashamed to +// admit they wrote it) + +import { handleTgUpdate } from "./tg/bot.deno.ts"; +import { DOMAIN, genRandomToken, tgCall } from "./tg/utils.deno.ts"; +import { RequestEvent } from "./utils.deno.ts"; + +const currentPeers = new Map( + (Deno.env.get("REPLICATION_ENDPOINTS")?.split(",") || []).map((peer) => [ + peer.trim(), + { myToken: genRandomToken(16), theirToken: genRandomToken(16) }, + ]), +); + +function listPeers() { + console.log("Current replication peers:"); + for (const [endpoint, tokens] of currentPeers.entries()) { + console.log( + `- ${endpoint}: myToken=${ + tokens.myToken.slice( + 0, + 4, + ) + }..., theirToken=${tokens.theirToken.slice(0, 4)}...`, + ); + } +} + +export async function handleReplication( + e: RequestEvent, +): Promise { + const url = new URL(e.request.url); + + if (url.pathname === "/replication/event") { + if (e.request.method.toUpperCase() !== "POST") { + return new Response("Method Not Allowed", { status: 405 }); + } + const body = await e.request.json(); + const { data, depth, token, src } = body; + if (!token || currentPeers.get(src)?.theirToken !== token) { + console.log( + `Unauthorized replication attempt from ${src} (${ + token.slice(0, 4) + }...)`, + ); + return new Response("Unauthorized", { status: 401 }); + } + await Promise.all([ + e.respondWith( + new Response("processing", { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }), + ), + processTgUpdate(data, depth), + ]); + return null; + } + + if (url.pathname === "/replication/register") { + const data = await e.request.json(); + const { myToken, yourToken, src, confirmOnly } = data; + console.log( + `Received replication registration${ + confirmOnly ? " (confirm-only)" : "" + } request from ${data.src}`, + ); + if ( + typeof myToken !== "string" || + typeof yourToken !== "string" || + typeof src !== "string" || + !myToken || + !yourToken || + !src + ) { + return new Response("Bad Request", { status: 400 }); + } + const peerData = currentPeers.get(src); + if (!peerData) { + return new Response("You are not my friend", { status: 403 }); + } + if (peerData.myToken === yourToken && peerData.theirToken === myToken) { + return new Response("ok", { status: 200 }); + } + + if (confirmOnly) { + return new Response("I did not ask for this", { status: 409 }); + } + + const resp = await fetch(`https://${src}/replication/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + myToken: yourToken, + yourToken: myToken, + src: DOMAIN, + confirmOnly: true, + }), + }); + if (!resp.ok) { + console.log( + `Peer ${src} requested registration, but didn't confirm:`, + await resp.text(), + ); + return new Response("You seem kinda sus", { status: 403 }); + } + + peerData.myToken = yourToken; + peerData.theirToken = myToken; + console.log(`Peer ${src} has updated their registration.`); + listPeers(); + return new Response("oh hi", { status: 200 }); + } + + return new Response("Not Found", { status: 404 }); +} + +export async function replicateData(data: any, depth: number) { + for (const [endpoint, peerData] of currentPeers.entries()) { + if (!peerData.myToken) continue; + try { + const resp = await fetch(`https://${endpoint}/replication/event`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data, + depth, + token: peerData.myToken, + src: DOMAIN, + }), + }); + if (resp.status === 401) { + peerData.myToken = genRandomToken(16); + peerData.theirToken = endpoint === DOMAIN + ? peerData.myToken + : genRandomToken(16); + console.log( + `Peer ${endpoint} rejected replication (401). Re-registering...`, + ); + const regResp = await fetch( + `https://${endpoint}/replication/register`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + myToken: peerData.myToken, + yourToken: peerData.theirToken, + src: DOMAIN, + }), + }, + ); + if (!regResp.ok) { + throw new Error( + `Re-registration to ${endpoint} failed: ${await regResp.text()}`, + ); + } + console.log(`Re-registration to ${endpoint} successful.`); + listPeers(); + // Retry replication once after re-registering + const retryResp = await fetch(`https://${endpoint}/replication/event`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data, + depth, + token: peerData.myToken, + src: DOMAIN, + }), + }); + if (!retryResp.ok) { + console.error( + `Replication to ${endpoint} after re-registration failed:`, + await retryResp.text(), + ); + } + } else if (!resp.ok) { + console.error(`Replication to ${endpoint} failed:`, await resp.text()); + } + } catch (e) { + console.error(`Replication to ${endpoint} failed:`, e); + } + } +} + +export async function processTgUpdate(data: any, depth: number) { + for await (const dato of handleTgUpdate(data)) { + if (depth >= 5) { + tgCall({ text: "🔥" }); + continue; + } + + await replicateData(dato, depth + 1); + } +} diff --git a/server.deno.ts b/src/server.deno.ts similarity index 64% rename from server.deno.ts rename to src/server.deno.ts index 13e6273..78f2bab 100644 --- a/server.deno.ts +++ b/src/server.deno.ts @@ -3,18 +3,19 @@ import { serveDir } from "https://deno.land/std@0.190.0/http/file_server.ts"; import { - handleRequest as handleTgRequest, - handleTgWeb, get_video, getSticekr, getSticekrCount, - RequestEvent, - init as tgBotInit, - webhookPath as tgWebhookPath, -} from "./tgbot.deno.ts"; + handleTgWeb, +} from "./tg/bot.deno.ts"; +import { init as tgBotInit } from "./tg/init.deno.ts"; +import { webhookPath as tgWebhookPath } from "./tg/utils.deno.ts"; +import { handleReplication, processTgUpdate } from "./replication.deno.ts"; +import { RequestEvent } from "./utils.deno.ts"; +import { webhookUrlToken } from "./tg/utils.deno.ts"; const indexContent = new TextDecoder().decode( - await Deno.readFile("index.html") + await Deno.readFile("index.html"), ); const indxContent = new TextDecoder().decode(await Deno.readFile("indx.html")); @@ -32,9 +33,11 @@ async function handleHttp(request: Request): Promise { const resp = await r; const end = performance.now(); console.log( - `${new Date().toISOString()} ${resp.status} ${request.method} ${ - request.url - } ${(end - start).toFixed(1)}ms` + `${ + new Date().toISOString() + } ${resp.status} ${request.method} ${request.url} ${ + (end - start).toFixed(1) + }ms`, ); resolve(resp); }, @@ -46,7 +49,12 @@ async function handleHttp(request: Request): Promise { await mockEvent.respondWith(response); } }) - .catch((err) => console.error(err)); + .catch((err) => { + console.error(err); + mockEvent.respondWith( + new Response("Internal Server Error", { status: 500 }), + ); + }); return await responsePromise; } @@ -54,23 +62,52 @@ async function handleHttp(request: Request): Promise { async function handleEvent(e: RequestEvent): Promise { const url = new URL(e.request.url); if (url.pathname === tgWebhookPath) { - await handleTgRequest(e); + if ( + e.request.method.toUpperCase() !== "POST" || + e.request.headers.get("X-Telegram-Bot-Api-Secret-Token") !== + webhookUrlToken + ) { + return new Response("You shall not pass", { + status: 401, + headers: { + "Content-Type": "text/plain", + }, + }); + } + + const data = await e.request.json(); + + await Promise.all([ + e.respondWith( + new Response("processing", { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }), + ), + processTgUpdate(data, 0), + ]); return null; } if (url.pathname === "/" || url.pathname === "/index.html") { return Math.random() < 0.01 ? new Response(indxContent, { - headers: { - "content-type": "text/html; charset=utf-8", - }, - status: 418, - }) + headers: { + "content-type": "text/html; charset=utf-8", + }, + status: 418, + }) : new Response(indexContent, { - headers: { - "content-type": "text/html; charset=utf-8", - }, - }); + headers: { + "content-type": "text/html; charset=utf-8", + }, + }); + } + + if (url.pathname.startsWith("/replication/")) { + return await handleReplication(e); } if (url.pathname === "/about") { @@ -92,7 +129,7 @@ async function handleEvent(e: RequestEvent): Promise { } if (url.pathname.startsWith("/api/videos/")) { - const index = parseInt(url.pathname.split('/').pop()!); + const index = parseInt(url.pathname.split("/").pop()!); const videoBytes = get_video(index); if (videoBytes === null) { @@ -105,7 +142,7 @@ async function handleEvent(e: RequestEvent): Promise { return new Response(videoBytes.buffer as ArrayBuffer, { headers: { "Content-Type": "video/mp4", - "Access-Control-Allow-Origin": "*" + "Access-Control-Allow-Origin": "*", }, }); } @@ -115,9 +152,9 @@ async function handleEvent(e: RequestEvent): Promise { headers: { "Content-Type": "application/json" }, }); } - + if (url.pathname.startsWith("/api/stickers/")) { - const index = parseInt(url.pathname.split('/').pop()!); + const index = parseInt(url.pathname.split("/").pop()!); const sticekrBytes = await getSticekr(index); if (sticekrBytes === null) { @@ -129,7 +166,7 @@ async function handleEvent(e: RequestEvent): Promise { return new Response(sticekrBytes.buffer as ArrayBuffer, { headers: { - "Content-Type": "image/webp" + "Content-Type": "image/webp", }, }); } @@ -158,4 +195,4 @@ async function handleEvent(e: RequestEvent): Promise { await tgBotInit(); -Deno.serve({ port: 8000 }, handleHttp); +Deno.serve({ port: parseInt(Deno.env.get("PORT") || "8000") }, handleHttp); diff --git a/tgbot.deno.ts b/src/tg/bot.deno.ts similarity index 62% rename from tgbot.deno.ts rename to src/tg/bot.deno.ts index e42067d..f26a4df 100644 --- a/tgbot.deno.ts +++ b/src/tg/bot.deno.ts @@ -3,216 +3,22 @@ // admit they wrote it) import unidecode from "npm:unidecode@1.1.0"; -import { geohash } from "./geohash.deno.ts"; +import { RequestEvent } from "../utils.deno.ts"; import { - getImageForPoint, - getUrlForPoint, - zoomForPoints, -} from "./mapycz.deno.ts"; - -const token = Deno.env.get("TG_BOT_TOKEN"); -const MAIN_CHAT_ID = parseInt(Deno.env.get("TG_MAIN_CHAT_ID")!); -const DOMAIN = Deno.env.get("DOMAIN")!; -const STICEKR_SET_NAME = Deno.env.get("STICKER_SET_NAME")!; -const STICEKR_SET_OWNER = parseInt(Deno.env.get("STICKER_SET_OWNER")!); -const PRINTER_TOKEN = Deno.env.get("PRINTER_TOKEN")!; - -export const webhookPath = "/tg-webhook"; - -const bootId = Math.random().toString(36).slice(2, 10); - -export type RequestEvent = { - request: Request; - respondWith(r: Response): Promise; -}; - -function genRandomToken(bytes: number) { - return btoa( - String.fromCharCode(...crypto.getRandomValues(new Uint8Array(bytes))) - ) - .replaceAll("/", "_") - .replaceAll("+", "-") - .replaceAll("=", ""); -} - -const webhookUrlToken = genRandomToken(96); - -// Rate limiter to prevent overwhelming the API -let lastRequestTime = 0; -const MIN_REQUEST_INTERVAL = 100; // Minimum 100ms between requests - -async function rateLimitedDelay() { - const now = Date.now(); - const timeSinceLastRequest = now - lastRequestTime; - - if (timeSinceLastRequest < MIN_REQUEST_INTERVAL) { - const delay = MIN_REQUEST_INTERVAL - timeSinceLastRequest; - await new Promise((resolve) => setTimeout(resolve, delay)); - } - - lastRequestTime = Date.now(); -} - -async function tgCall( - options: any, - endpoint = "sendMessage", - retryCount = 0 -): Promise { - if (endpoint == "sendMessage") options.chat_id ??= MAIN_CHAT_ID; - - const maxRetries = 5; - const baseDelay = 1000; // 1 second - - // Apply rate limiting before making the request - await rateLimitedDelay(); - - const req = await fetch(`https://api.telegram.org/bot${token}/${endpoint}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(options), - }); - - try { - const resp = await req.json(); - - // Handle rate limiting with exponential backoff - if (!resp.ok && resp.error_code === 429 && retryCount < maxRetries) { - const retryAfter = - resp.parameters?.retry_after || Math.pow(2, retryCount); - const delay = Math.min( - baseDelay * Math.pow(2, retryCount), - retryAfter * 1000 - ); - - console.log( - `Rate limited on ${endpoint}. Retrying in ${delay}ms (attempt ${ - retryCount + 1 - }/${maxRetries})` - ); - - await new Promise((resolve) => setTimeout(resolve, delay)); - return tgCall(options, endpoint, retryCount + 1); - } - - if (!resp.ok) { - console.log("Req to", endpoint, "with", options, "failed:", resp); - } - return resp; - } catch { - // Handle network errors with exponential backoff - if (retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount); - console.log( - `Network error on ${endpoint}. Retrying in ${delay}ms (attempt ${ - retryCount + 1 - }/${maxRetries})` - ); - - await new Promise((resolve) => setTimeout(resolve, delay)); - return tgCall(options, endpoint, retryCount + 1); - } - } - return req; -} - -async function domeny() { - const resp = await fetch( - "https://auctions-master.nic.cz/share/new_auctions.json" - ); - - let list = ((await resp.json()) as any[]) - .filter( - (a) => - a.auction_from.split("T")[0] === new Date().toISOString().split("T")[0] - ) - .map((x) => x.item_title as string); - list.sort(); - list.sort((a, b) => a.length - b.length); - list = list - .filter((x) => x.length <= 8 && !/^[0-9]{4,8}\.cz$/.test(x)) - .concat(list.slice(-20)); - console.log(list); - while (list.length > 0) { - const chunk = list.splice(0, 50); - const webArchiveLinks = await Promise.all( - chunk.map(async (x) => { - let avail = false; - try { - const r = await fetch( - `https://archive.org/wayback/available?url=${x}` - ); - const resp = await r.json(); - console.log(resp); - if (Object.keys(resp.archived_snapshots).length > 0) avail = true; - } catch (e) { - console.log(e); - } - const l = x.replaceAll(/[_*[\\\]()~`>#+=|{}.!/-]/g, ($) => `\\${$}`); - if (!avail) return `\`${l}\``; - return `[${l}](https://web.archive.org/web/${l})`; - }) - ); - processTgUpdate( - await tgCall({ - text: webArchiveLinks.join("\n"), - parse_mode: "MarkdownV2", - }) - ); - } - - if (previousMorningSticker && !previousMorningSticker.has_reaction) { - await tgCall({ - chat_id: MAIN_CHAT_ID, - text: "rip", - reply_to_message_id: previousMorningSticker.message_id, - }); - await tgCall( - { sticker: previousMorningSticker.sticker_file_id }, - "deleteStickerFromSet" - ); - } - - const { - result: { stickers: sticekrs }, - }: any = await tgCall( - { - name: STICEKR_SET_NAME, - }, - "getStickerSet" - ); - const { file_id: sticekr } = - sticekrs[Math.floor(Math.random() * sticekrs.length)]; - const stickerResponse = await tgCall({ - chat_id: MAIN_CHAT_ID, - sticker: sticekr, - reply_to_message_id: 97776, - }, "sendSticker" ); - - if (stickerResponse.ok) { - previousMorningSticker = { - message_id: stickerResponse.result.message_id, - sticker_file_id: sticekr, - has_reaction: false, - }; - } -} + BOT_TOKEN, + DOMAIN, + genRandomToken, + MAIN_CHAT_ID, + PRINTER_TOKEN, + STICEKR_SET_NAME, + STICEKR_SET_OWNER, + tgCall, +} from "./utils.deno.ts"; +import { checkStickerReaction, getTempDir } from "./init.deno.ts"; -let tempDir = ""; const contentTypes = new Map(); const runningProcesses = new Map(); -let previousMorningSticker: { - message_id: number; - sticker_file_id: string; - has_reaction: boolean; -} | null = null; - -const origins = [ - { lat: 50.1005803, lon: 14.3954325 }, -]; - // In-memory video list for kvh frontend const videoList: Uint8Array[] = []; @@ -226,27 +32,34 @@ export function get_video(index: number): Uint8Array | null { let sticekrs: any; export async function getSticekrCount(): Promise { if (!sticekrs) { - setTimeout(sus => sticekrs /= sus, 69420); - sticekrs = (await tgCall( - { name: STICEKR_SET_NAME, }, - "getStickerSet" - )).result.stickers; + setTimeout((sus) => (sticekrs /= sus), 69420); + sticekrs = (await tgCall({ name: STICEKR_SET_NAME }, "getStickerSet")) + .result.stickers; } return sticekrs.length; } export async function getSticekr(index: number): Promise { - if (!Number.isFinite(index) || index < 0 || index >= await getSticekrCount()) { + if ( + !Number.isFinite(index) || + index < 0 || + index >= (await getSticekrCount()) + ) { return null; } - if(sticekrs[index].file) { + if (sticekrs[index].file) { return sticekrs[index].file; } - const fileData = await tgCall({ file_id: sticekrs[index].file_id }, "getFile"); - const response = await fetch(`https://api.telegram.org/file/bot${token}/${fileData.result.file_path}`); - return sticekrs[index].file = new Uint8Array(await response.arrayBuffer()); + const fileData = await tgCall( + { file_id: sticekrs[index].file_id }, + "getFile", + ); + const response = await fetch( + `https://api.telegram.org/file/bot${BOT_TOKEN}/${fileData.result.file_path}`, + ); + return (sticekrs[index].file = new Uint8Array(await response.arrayBuffer())); } async function* handleVideoNote(message: any) { @@ -261,7 +74,8 @@ async function* handleVideoNote(message: any) { } // Download the video note - const videoUrl = `https://api.telegram.org/file/bot${token}/${fileData.result.file_path}`; + const videoUrl = + `https://api.telegram.org/file/bot${BOT_TOKEN}/${fileData.result.file_path}`; const response = await fetch(videoUrl); if (!response.ok) { console.error("Failed to download video note:", response.status); @@ -272,8 +86,9 @@ async function* handleVideoNote(message: any) { const videoBytes = new Uint8Array(await response.arrayBuffer()); videoList.push(videoBytes); - console.log(`Downloaded and stored video note. Total videos: ${videoList.length}`); - + console.log( + `Downloaded and stored video note. Total videos: ${videoList.length}`, + ); } catch (error: any) { console.error("Error handling video note:", error); yield await tgCall({ @@ -284,181 +99,13 @@ async function* handleVideoNote(message: any) { } } -async function postGeohash() { - const upcoming = new Date(); - upcoming.setHours(6); - upcoming.setMinutes(Math.random() * 60); - - const now = new Date(); - if (upcoming.getTime() < now.getTime()) { - upcoming.setDate(upcoming.getDate() + 1); - } - - await new Promise((resolve) => - setTimeout(resolve, upcoming.getTime() - now.getTime()) - ); - - for (const origin of origins) { - const geoHash = await geohash(new Date(), origin); - const a = zoomForPoints({ - MinY: Math.min(origin.lat, geoHash.lat), - MinX: Math.min(origin.lon, geoHash.lon), - MaxY: Math.max(origin.lat, geoHash.lat), - MaxX: Math.max(origin.lon, geoHash.lon), - }); - const meta = { - point: geoHash, - ...a, - }; - - const text = `[ ](${getImageForPoint( - meta - )})Today's geohash is at [${geoHash.lat - .toFixed(5) - .replace(".", "\\.")} ${geoHash.lon - .toFixed(5) - .replace(".", "\\.")}](${getUrlForPoint( - meta - )})\\.\nPlease refer to xkcd\\.com/426/ for further steps\\.`; - - await tgCall({ - text, - parse_mode: "MarkdownV2", - }); - } - - await domeny(); - - setTimeout(postGeohash, 1000 * 60 * 60 * 2); - - if (Math.random() < 0.03) { - setTimeout( - async () => - await tgCall({ - text: ( - await ( - await fetch( - "https://%73%6e%65%64%6c-%75%7a-%6b%75%62%69%6b-%70%6f%6e%6f%7a%6b%75.%67%69%74%68%75%62.%69%6f" - ) - ).text() - ) - .replace(/\r|<[^>]+>/g, "") - .replace(/\s{2,}/gm, "\n") - .trim(), - }), - Math.random() * 600000 + 600000 - ); - } -} - -export async function init() { - if ( - !token || - !DOMAIN || - isNaN(MAIN_CHAT_ID) || - !STICEKR_SET_NAME || - isNaN(STICEKR_SET_OWNER) - ) { - console.log( - `TG_BOT_TOKEN: ${token}, TG_MAIN_CHAT_ID: ${MAIN_CHAT_ID}, DOMAIN: ${DOMAIN}, STICEKR_SET_NAME: ${STICEKR_SET_NAME}, STICEKR_SET_OWNER: ${STICEKR_SET_OWNER}` - ); - throw new Error( - "TG_BOT_TOKEN, TG_MAIN_CHAT_ID, DOMAIN, STICEKR_SET_NAME or STICEKR_SET_OWNER is not set" - ); - } - - tempDir = await Deno.makeTempDir(); - console.log("Using temp dir", tempDir); - - await tgCall( - { - url: `https://${DOMAIN}${webhookPath}`, - secret_token: webhookUrlToken, - allowed_updates: [ - "message", - "callback_query", - "inline_query", - "edited_message", - "message_reaction", - ], - }, - "setWebhook" - ); - - setTimeout(async () => { - await tgCall( - { - photo: `https://${DOMAIN}/startup.jpg?q=${bootId}`, - chat_id: MAIN_CHAT_ID, - }, - "sendPhoto" - ); - }, 2000); - - Deno.cron("tuuuuuuuuuu", "0 11 * * 4#1", () => { - tgCall({ - text: "TÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚ", - }); - }); - - postGeohash(); -} - -export async function handleRequest(e: RequestEvent) { - if ( - e.request.method.toUpperCase() !== "POST" || - e.request.headers.get("X-Telegram-Bot-Api-Secret-Token") !== webhookUrlToken - ) { - await e.respondWith( - new Response("You shall not pass", { - status: 401, - headers: { - "Content-Type": "text/plain", - }, - }) - ); - return; - } - - const data = await e.request.json(); - - await Promise.all([ - e.respondWith( - new Response("processing", { - status: 200, - headers: { - "Content-Type": "text/plain", - }, - }) - ), - processTgUpdate(data), - ]); -} - -async function processTgUpdate(data: any) { - for await (const dato of handleTgUpdate(data)) { - for await (const data of handleTgUpdate(dato)) { - for await (const dato of handleTgUpdate(data)) { - for await (const data of handleTgUpdate(dato)) { - for await (const _ of handleTgUpdate(data)) { - tgCall({ text: "🔥" }); - } - } - } - } - } -} - -async function* handleTgUpdate(data: any) { +export async function* handleTgUpdate(data: any) { const { ok } = data; data.message ??= data.result; if ("callback_query" in data) return handleCallbackQuery(data); if ("inline_query" in data) return yield* handleInlineQuery(data); if ("message_reaction" in data) { - if (previousMorningSticker && - data.message_reaction.message_id === previousMorningSticker.message_id) { - previousMorningSticker.has_reaction = true; - } + checkStickerReaction(data.message_reaction.message_id); return; } if ("edited_message" in data) { @@ -467,7 +114,9 @@ async function* handleTgUpdate(data: any) { // Handle video notes (circular videos) if (data?.message?.video_note) { - console.log(`Found video note: ${data.message.video_note.length}s duration`); + console.log( + `Found video note: ${data.message.video_note.length}s duration`, + ); yield* handleVideoNote(data.message); } @@ -502,7 +151,7 @@ async function* handleTgUpdate(data: any) { }, ], }, - "setMessageReaction" + "setMessageReaction", ); break; } @@ -538,12 +187,11 @@ async function* handleTgUpdate(data: any) { yield await tgCall( { chat_id: data.message.chat.id, - video_note: - Math.random() < 0.5 - ? "DQACAgQAAxkDAAM8ZWhhSjCXOdVCv7a8SkikjCDwEH4AAiQSAAKXPEhTuYZAGfYG_KwzBA" - : "DQACAgQAAx0CYbOIYwABAZOgaSF5TvqBIZtJAkkgCyJa5lPTpvUAAmkaAAKkFxBRMvxu2BMBa4c2BA", + video_note: Math.random() < 0.5 + ? "DQACAgQAAxkDAAM8ZWhhSjCXOdVCv7a8SkikjCDwEH4AAiQSAAKXPEhTuYZAGfYG_KwzBA" + : "DQACAgQAAx0CYbOIYwABAZOgaSF5TvqBIZtJAkkgCyJa5lPTpvUAAmkaAAKkFxBRMvxu2BMBa4c2BA", }, - "sendVideoNote" + "sendVideoNote", ); } @@ -554,21 +202,21 @@ async function* handleTgUpdate(data: any) { yield await tgCall( { chat_id: data.message.chat.id, - video_note: "DQACAgQAAxkDAAIHW2kohCvq14qxhtH5p9QUgIYGb8glAAJPHAACYmBJUZ-MT60sG7t5NgQ", + video_note: + "DQACAgQAAxkDAAIHW2kohCvq14qxhtH5p9QUgIYGb8glAAJPHAACYmBJUZ-MT60sG7t5NgQ", }, - "sendVideoNote" + "sendVideoNote", ); } - if ( - text.toLowerCase().includes("fit") - ) { + if (text.toLowerCase().includes("fit")) { yield await tgCall( { chat_id: data.message.chat.id, - video_note: "DQACAgQAAx0CYbOIYwABAZoQaTb2QmdbNsgQ0pT0JOhLg6_GzNEAAnkfAAKUS7lRUArcMe2A3mA2BA", + video_note: + "DQACAgQAAx0CYbOIYwABAZoQaTb2QmdbNsgQ0pT0JOhLg6_GzNEAAnkfAAKUS7lRUArcMe2A3mA2BA", }, - "sendVideoNote" + "sendVideoNote", ); } @@ -579,21 +227,23 @@ async function* handleTgUpdate(data: any) { sticker: "CAACAgQAAxUAAWYaZDro9kEe0mLkwvNEkBKbmBS6AAKLFAACwXbgUt-2B1-aBYwpNAQ", }, - "sendSticker" + "sendSticker", ); } if (text.toLowerCase().includes("hrovno")) { await tgCall({ chat_id: data.message.chat.id, - text: `Pánové, toto je certifikované hrovno. Miluji hrovno. Co je hrovnové, to je suprové. Hrovnový moment.`, + text: + `Pánové, toto je certifikované hrovno. Miluji hrovno. Co je hrovnové, to je suprové. Hrovnový moment.`, }); } if (text.toLowerCase().includes("zig")) { await tgCall({ chat_id: data.message.chat.id, - text: `Pánové, toto je certifikované Zig. Miluji Zig. Co je Zigové, to je suprové. Zigový moment.`, + text: + `Pánové, toto je certifikované Zig. Miluji Zig. Co je Zigové, to je suprové. Zigový moment.`, }); } @@ -618,7 +268,8 @@ async function* handleTgUpdate(data: any) { const r1 = await tgCall({ chat_id: data.message.chat.id, reply_to_message_id: data.message.message_id, - text: `No, Richard, it's 'Linux', not 'GNU/Linux'. The most important contributions that the FSF made to Linux were the creation of the GPL and the GCC compiler. Those are fine and inspired products. GCC is a monumental achievement and has earned you, RMS, and the Free Software Foundation countless kudos and much appreciation. + text: + `No, Richard, it's 'Linux', not 'GNU/Linux'. The most important contributions that the FSF made to Linux were the creation of the GPL and the GCC compiler. Those are fine and inspired products. GCC is a monumental achievement and has earned you, RMS, and the Free Software Foundation countless kudos and much appreciation. Following are some reasons for you to mull over, including some already answered in your FAQ. @@ -632,7 +283,8 @@ One guy, Linus Torvalds, used GCC to make his operating system (yes, Linux is an yield await tgCall({ chat_id: data.message.chat.id, reply_to_message_id: r1.result.message_id, - text: `Next, even if we limit the GNU/Linux title to the GNU-based Linux distributions, we run into another obvious problem. XFree86 may well be more important to a particular Linux installation than the sum of all the GNU contributions. More properly, shouldn't the distribution be called XFree86/Linux? Or, at a minimum, XFree86/GNU/Linux? Of course, it would be rather arbitrary to draw the line there when many other fine contributions go unlisted. Yes, I know you've heard this one before. Get used to it. You'll keep hearing it until you can cleanly counter it. + text: + `Next, even if we limit the GNU/Linux title to the GNU-based Linux distributions, we run into another obvious problem. XFree86 may well be more important to a particular Linux installation than the sum of all the GNU contributions. More properly, shouldn't the distribution be called XFree86/Linux? Or, at a minimum, XFree86/GNU/Linux? Of course, it would be rather arbitrary to draw the line there when many other fine contributions go unlisted. Yes, I know you've heard this one before. Get used to it. You'll keep hearing it until you can cleanly counter it. You seem to like the lines-of-code metric. There are many lines of GNU code in a typical Linux distribution. You seem to suggest that (more LOC) == (more important). However, I submit to you that raw LOC numbers do not directly correlate with importance. I would suggest that clock cycles spent on code is a better metric. For example, if my system spends 90% of its time executing XFree86 code, XFree86 is probably the single most important collection of code on my system. Even if I loaded ten times as many lines of useless bloatware on my system and I never excuted that bloatware, it certainly isn't more important code than XFree86. Obviously, this metric isn't perfect either, but LOC really, really sucks. Please refrain from using it ever again in supporting any argument. @@ -658,11 +310,13 @@ Be grateful for your abilities and your incredible success and your considerable chat_id: data.message.chat.id, reply_to_message_id: data.message.message_id, parse_mode: "MarkdownV2", - text: `\`\`\`\n${JSON.stringify( - data.message.reply_to_message, - null, - 2 - ).replaceAll("\\", "\\\\")}\n\`\`\``, + text: `\`\`\`\n${ + JSON.stringify( + data.message.reply_to_message, + null, + 2, + ).replaceAll("\\", "\\\\") + }\n\`\`\``, }); } @@ -684,7 +338,9 @@ Be grateful for your abilities and your incredible success and your considerable "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ - message: `${data.message.from} říká: ${text.slice(trig.length).trim()}`, + message: `${data.message.from.first_name} říká: ${ + text.slice(trig.length).trim() + }`, image: "", token: PRINTER_TOKEN, }).toString(), @@ -742,7 +398,7 @@ Be grateful for your abilities and your incredible success and your considerable }, ], }, - "setMessageReaction" + "setMessageReaction", ); } @@ -770,7 +426,7 @@ Be grateful for your abilities and your incredible success and your considerable chat_id: data.message.chat.id, message_id: data.message.message_id, }, - "deleteMessage" + "deleteMessage", ); if (reply_id !== undefined) { yield await tgCall( @@ -780,7 +436,7 @@ Be grateful for your abilities and your incredible success and your considerable video: "BAACAgQAAxkDAANmZb5XjJUES6VCvJGtIRRrKMGwRpcAAq0SAAINl_BR5jVZOMRHxCI0BA", }, - "sendVideo" + "sendVideo", ); } } @@ -810,7 +466,8 @@ Be grateful for your abilities and your incredible success and your considerable ]; for (const { trigger, genitiv, popis, regex } of bannedWords) { - const disclaimer = `Upozornění: Tato zpráva obsahuje ${trigger}. Jsem si vědom tohoto prohřešku, ${popis} a tato zpáva nesmí být interpretována jako podpora ${genitiv}.`; + const disclaimer = + `Upozornění: Tato zpráva obsahuje ${trigger}. Jsem si vědom tohoto prohřešku, ${popis} a tato zpáva nesmí být interpretována jako podpora ${genitiv}.`; if (!text.includes(disclaimer)) continue; if ( @@ -823,11 +480,12 @@ Be grateful for your abilities and your incredible success and your considerable chat_id: data.message.chat.id, message_id: data.message.message_id, }, - "deleteMessage" + "deleteMessage", ); yield await tgCall({ chat_id: data.message.chat.id, - text: `Zjištěno porušení pravidel uživatelem ${data.message.from.first_name}, tento incident byl zaznamenán. Příště prosím přidejte do zpávy tento disclaimer:\n\n${disclaimer}`, + text: + `Zjištěno porušení pravidel uživatelem ${data.message.from.first_name}, tento incident byl zaznamenán. Příště prosím přidejte do zpávy tento disclaimer:\n\n${disclaimer}`, }); if (ok) { yield await tgCall({ @@ -842,7 +500,8 @@ Be grateful for your abilities and your incredible success and your considerable } else { yield await tgCall({ chat_id: data.message.from.id, - text: `Hej chápu že to je opruz, tady máš tu původní zprávu:\n\n${text}`, + text: + `Hej chápu že to je opruz, tady máš tu původní zprávu:\n\n${text}`, }); } break; @@ -896,17 +555,18 @@ Be grateful for your abilities and your incredible success and your considerable open.pop(); } - for (const cha of ( - open as ((typeof open)[number] | (typeof jail)[number])[] - ) - .concat(jail) - .sort((a, b) => b.pos - a.pos)) { - await tgCall({ + for ( + const cha of ( + open as ((typeof open)[number] | (typeof jail)[number])[] + ) + .concat(jail) + .sort((a, b) => b.pos - a.pos) + ) { + yield await tgCall({ chat_id: data.message.chat.id, - text: - "jail" in cha - ? `${cha.char}jail time for ${data.message.from.first_name}` - : { "(": ")", "[": "]", "{": "}" }[cha.char], + text: "jail" in cha + ? `${cha.char}jail time for ${data.message.from.first_name}` + : { "(": ")", "[": "]", "{": "}" }[cha.char], }); } } @@ -950,23 +610,27 @@ async function* handleLogo(data: any, text: string) { photo: `https://${DOMAIN}/persistent/logos/${fn}.png`, caption: `https://${DOMAIN}/persistent/logos/${fn}.svg`, }, - "sendPhoto" + "sendPhoto", ); } } async function* handleSh(data: any, cmd: string) { const id = genRandomToken(32); - await Deno.writeFile(`${tempDir}/${id}.sh`, new TextEncoder().encode(cmd), { - createNew: true, - }); + await Deno.writeFile( + `${getTempDir()}/${id}.sh`, + new TextEncoder().encode(cmd), + { + createNew: true, + }, + ); - const outFile = await Deno.open(`${tempDir}/${id}.out`, { + const outFile = await Deno.open(`${getTempDir()}/${id}.out`, { write: true, createNew: true, }); const command = new Deno.Command("bash", { - args: [`${tempDir}/${id}.sh`], + args: [`${getTempDir()}/${id}.sh`], stdin: "piped", stdout: "piped", stderr: "piped", @@ -996,7 +660,7 @@ async function* handleSh(data: any, cmd: string) { length, id, data.message.message_id, - raceResult.code + raceResult.code, ); return; } @@ -1007,7 +671,8 @@ async function* handleSh(data: any, cmd: string) { const progressMessageResponse = await tgCall({ reply_to_message_id: data.message.message_id, parse_mode: "MarkdownV2", - text: `[Command is taking too long](https://${DOMAIN}/tgweb/${id})\\. Set Content\\-Type with \`/settype ${id} text/plain\``, + text: + `[Command is taking too long](https://${DOMAIN}/tgweb/${id})\\. Set Content\\-Type with \`/settype ${id} text/plain\``, reply_markup: { inline_keyboard: [ [ @@ -1032,13 +697,13 @@ async function* handleSh(data: any, cmd: string) { inline_keyboard: [], }, }, - "editMessageReplyMarkup" + "editMessageReplyMarkup", ); yield* reportProcessResult( length, id, progressMessageResponse.result.message_id, - status.code + status.code, ); } @@ -1046,9 +711,9 @@ async function* reportProcessResult( length: number, id: string, reply_to_message_id: number, - exitCode: number + exitCode: number, ) { - const outPath = `${tempDir}/${id}.out`; + const outPath = `${getTempDir()}/${id}.out`; const fileProc = new Deno.Command("file", { args: ["-ib", outPath], stdout: "piped", @@ -1057,8 +722,8 @@ async function* reportProcessResult( // need to await the status to not create zombie processes const mime = decoder.decode(out.stdout); contentTypes.set(id, mime); - const isText = - mime.startsWith("text/") || mime.startsWith("application/json"); + const isText = mime.startsWith("text/") || + mime.startsWith("application/json"); let text; if (length === 0) text = `No output \\(exit code ${exitCode}\\)\\.`; else if (isText && length <= 5000) { @@ -1072,8 +737,7 @@ async function* reportProcessResult( text = `[Exit code ${exitCode}](https://${DOMAIN}/tgweb/${id})\n` + text; } } else { - text = - "[" + + text = "[" + (isText ? "Output too long" : "Binary output") + `](https://${DOMAIN}/tgweb/${id}) \\(exit code ${exitCode}, ${length} bytes\\)\\. Set Content\\-Type with \`/settype ${id} mime/type\``; } @@ -1109,19 +773,17 @@ export async function handleTgWeb(e: RequestEvent): Promise { }); } - const file = await Deno.open(`${tempDir}/${path}.out`); + const file = await Deno.open(`${getTempDir()}/${path}.out`); return new Response(file.readable, { headers: { "Content-Type": ct }, }); } - - let imageI = 0; async function* handleInlineQuery(data: any) { const { id: inline_query_id, query, from } = data.inline_query; console.log( - `Logo from ${from.first_name} ${from.last_name} (@${from.username}): ${query}` + `Logo from ${from.first_name} ${from.last_name} (@${from.username}): ${query}`, ); const fn = `inline_${imageI++}_${slugify(query)}_${new Date().toISOString()}`; if ((await generateLogos(query, fn)) === 0) { @@ -1141,13 +803,13 @@ async function* handleInlineQuery(data: any) { }, ], }, - "answerInlineQuery" + "answerInlineQuery", ); } } async function* sticekrThis( - orig_msg: any + orig_msg: any, ): AsyncGenerator { if (!orig_msg) return "wtf"; let file; @@ -1162,12 +824,12 @@ async function* sticekrThis( { file_id: file, }, - "getFile" + "getFile", ); if (!data.ok) return "telegrams a hoe: " + JSON.stringify(data); const resp2 = await fetch( - `https://api.telegram.org/file/bot${token}/${data.result.file_path}` + `https://api.telegram.org/file/bot${BOT_TOKEN}/${data.result.file_path}`, ); if (!resp2.ok) return "telegram cdn is a hoe: " + (await resp2.text()); @@ -1187,15 +849,15 @@ async function* sticekrThis( body.append("name", STICEKR_SET_NAME); body.append( "sticker", - JSON.stringify({ sticker: "attach://file", emoji_list: ["🤓"] }) + JSON.stringify({ sticker: "attach://file", emoji_list: ["🤓"] }), ); body.append("file", new Blob([sticekr], { type: "image/webp" }), "file.webp"); const resp3 = await fetch( - `https://api.telegram.org/bot${token}/addStickerToSet`, + `https://api.telegram.org/bot${BOT_TOKEN}/addStickerToSet`, { method: "POST", body, - } + }, ); if (!resp3.ok) return "skill issue: " + (await resp3.text()); @@ -1203,7 +865,7 @@ async function* sticekrThis( { name: STICEKR_SET_NAME, }, - "getStickerSet" + "getStickerSet", ); if (!data4.ok) { return "i ran out of error message ideas: " + JSON.stringify(data4); @@ -1216,7 +878,7 @@ async function* sticekrThis( chat_id: orig_msg.chat.id, sticker: sticekrId, }, - "sendSticker" + "sendSticker", ); yield resp5; diff --git a/src/tg/init.deno.ts b/src/tg/init.deno.ts new file mode 100644 index 0000000..b945f34 --- /dev/null +++ b/src/tg/init.deno.ts @@ -0,0 +1,262 @@ +// deno-lint-ignore-file no-explicit-any +// The authors disclaim copyright to this source code (they are ashamed to +// admit they wrote it) + +import { + BOT_TOKEN, + DOMAIN, + genRandomToken, + MAIN_CHAT_ID, + STICEKR_SET_NAME, + STICEKR_SET_OWNER, + tgCall, + webhookPath, + webhookUrlToken, +} from "./utils.deno.ts"; +import { geohash } from "../geohash.deno.ts"; +import { + getImageForPoint, + getUrlForPoint, + zoomForPoints, +} from "../mapycz.deno.ts"; +import { processTgUpdate } from "../replication.deno.ts"; + +let previousMorningSticker: { + message_id: number; + sticker_file_id: string; + has_reaction: boolean; +} | null = null; + +async function domeny() { + const resp = await fetch( + "https://auctions-master.nic.cz/share/new_auctions.json", + ); + + let list = ((await resp.json()) as any[]) + .filter( + (a) => + a.auction_from.split("T")[0] === new Date().toISOString().split("T")[0], + ) + .map((x) => x.item_title as string); + list.sort(); + list.sort((a, b) => a.length - b.length); + list = list + .filter((x) => x.length <= 8 && !/^[0-9]{4,8}\.cz$/.test(x)) + .concat(list.slice(-20)); + console.log(list); + while (list.length > 0) { + const chunk = list.splice(0, 50); + const webArchiveLinks = await Promise.all( + chunk.map(async (x) => { + let avail = false; + try { + const r = await fetch( + `https://archive.org/wayback/available?url=${x}`, + ); + const resp = await r.json(); + console.log(resp); + if (Object.keys(resp.archived_snapshots).length > 0) avail = true; + } catch (e) { + console.log(e); + } + const l = x.replaceAll(/[_*[\\\]()~`>#+=|{}.!/-]/g, ($) => `\\${$}`); + if (!avail) return `\`${l}\``; + return `[${l}](https://web.archive.org/web/${l})`; + }), + ); + processTgUpdate( + await tgCall({ + text: webArchiveLinks.join("\n"), + parse_mode: "MarkdownV2", + }), + 0, + ); + } + + if (previousMorningSticker && !previousMorningSticker.has_reaction) { + await tgCall({ + chat_id: MAIN_CHAT_ID, + text: "rip", + reply_to_message_id: previousMorningSticker.message_id, + }); + await tgCall( + { sticker: previousMorningSticker.sticker_file_id }, + "deleteStickerFromSet", + ); + } + + const { + result: { stickers: sticekrs }, + }: any = await tgCall( + { + name: STICEKR_SET_NAME, + }, + "getStickerSet", + ); + const { file_id: sticekr } = + sticekrs[Math.floor(Math.random() * sticekrs.length)]; + const stickerResponse = await tgCall( + { + chat_id: MAIN_CHAT_ID, + sticker: sticekr, + reply_to_message_id: 97776, + }, + "sendSticker", + ); + + if (stickerResponse.ok) { + previousMorningSticker = { + message_id: stickerResponse.result.message_id, + sticker_file_id: sticekr, + has_reaction: false, + }; + } +} + +export function checkStickerReaction(stickerMessageId: number) { + if ( + previousMorningSticker && + previousMorningSticker.message_id === stickerMessageId + ) { + previousMorningSticker.has_reaction = true; + } +} + +const hashOrigins = [{ lat: 50.1005803, lon: 14.3954325 }]; + +async function postGeohash() { + const upcoming = new Date(); + upcoming.setHours(6); + upcoming.setMinutes(Math.random() * 60); + + const now = new Date(); + if (upcoming.getTime() < now.getTime()) { + upcoming.setDate(upcoming.getDate() + 1); + } + + await new Promise((resolve) => + setTimeout(resolve, upcoming.getTime() - now.getTime()) + ); + + for (const origin of hashOrigins) { + const geoHash = await geohash(new Date(), origin); + const a = zoomForPoints({ + MinY: Math.min(origin.lat, geoHash.lat), + MinX: Math.min(origin.lon, geoHash.lon), + MaxY: Math.max(origin.lat, geoHash.lat), + MaxX: Math.max(origin.lon, geoHash.lon), + }); + const meta = { + point: geoHash, + ...a, + }; + + const text = `[ ](${ + getImageForPoint( + meta, + ) + })Today's geohash is at [${ + geoHash.lat + .toFixed(5) + .replace(".", "\\.") + } ${ + geoHash.lon + .toFixed(5) + .replace(".", "\\.") + }](${ + getUrlForPoint( + meta, + ) + })\\.\nPlease refer to xkcd\\.com/426/ for further steps\\.`; + + await tgCall({ + text, + parse_mode: "MarkdownV2", + }); + } + + await domeny(); + + setTimeout(postGeohash, 1000 * 60 * 60 * 2); + + if (Math.random() < 0.03) { + setTimeout( + async () => + await tgCall({ + text: ( + await ( + await fetch( + "https://%73%6e%65%64%6c-%75%7a-%6b%75%62%69%6b-%70%6f%6e%6f%7a%6b%75.%67%69%74%68%75%62.%69%6f", + ) + ).text() + ) + .replace(/\r|<[^>]+>/g, "") + .replace(/\s{2,}/gm, "\n") + .trim(), + }), + Math.random() * 600000 + 600000, + ); + } +} + +const bootId = genRandomToken(16); + +let tempDir = ""; + +export function getTempDir() { + return tempDir; +} + +export async function init() { + if ( + !BOT_TOKEN || + !DOMAIN || + isNaN(MAIN_CHAT_ID) || + !STICEKR_SET_NAME || + isNaN(STICEKR_SET_OWNER) + ) { + console.log( + `TG_BOT_TOKEN: ${BOT_TOKEN}, TG_MAIN_CHAT_ID: ${MAIN_CHAT_ID}, DOMAIN: ${DOMAIN}, STICEKR_SET_NAME: ${STICEKR_SET_NAME}, STICEKR_SET_OWNER: ${STICEKR_SET_OWNER}`, + ); + throw new Error( + "TG_BOT_TOKEN, TG_MAIN_CHAT_ID, DOMAIN, STICEKR_SET_NAME or STICEKR_SET_OWNER is not set", + ); + } + + tempDir = await Deno.makeTempDir(); + console.log("Using temp dir", tempDir); + + await tgCall( + { + url: `https://${DOMAIN}${webhookPath}`, + secret_token: webhookUrlToken, + allowed_updates: [ + "message", + "callback_query", + "inline_query", + "edited_message", + "message_reaction", + ], + }, + "setWebhook", + ); + + setTimeout(async () => { + await tgCall( + { + photo: `https://${DOMAIN}/startup.jpg?q=${bootId}`, + chat_id: MAIN_CHAT_ID, + }, + "sendPhoto", + ); + }, 2000); + + Deno.cron("tuuuuuuuuuu", "0 11 * * 4#1", () => { + tgCall({ + text: + "TÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚÚ", + }); + }); + + postGeohash(); +} diff --git a/src/tg/utils.deno.ts b/src/tg/utils.deno.ts new file mode 100644 index 0000000..9128635 --- /dev/null +++ b/src/tg/utils.deno.ts @@ -0,0 +1,106 @@ +// deno-lint-ignore-file no-explicit-any +// The authors disclaim copyright to this source code (they are ashamed to +// admit they wrote it) + +export const BOT_TOKEN = Deno.env.get("TG_BOT_TOKEN"); +export const MAIN_CHAT_ID = parseInt(Deno.env.get("TG_MAIN_CHAT_ID")!); +export const DOMAIN = Deno.env.get("DOMAIN")!; +export const STICEKR_SET_NAME = Deno.env.get("STICKER_SET_NAME")!; +export const STICEKR_SET_OWNER = parseInt(Deno.env.get("STICKER_SET_OWNER")!); +export const PRINTER_TOKEN = Deno.env.get("PRINTER_TOKEN")!; + +export const webhookPath = "/tg-webhook"; + +export function genRandomToken(bytes: number) { + return btoa( + String.fromCharCode(...crypto.getRandomValues(new Uint8Array(bytes))), + ) + .replaceAll("/", "_") + .replaceAll("+", "-") + .replaceAll("=", ""); +} + +export const webhookUrlToken = genRandomToken(96); + +// Rate limiter to prevent overwhelming the API +let lastRequestTime = 0; +const MIN_REQUEST_INTERVAL = 100; // Minimum 100ms between requests + +async function rateLimitedDelay() { + const now = Date.now(); + const timeSinceLastRequest = now - lastRequestTime; + + if (timeSinceLastRequest < MIN_REQUEST_INTERVAL) { + const delay = MIN_REQUEST_INTERVAL - timeSinceLastRequest; + await new Promise((resolve) => setTimeout(resolve, delay)); + } + + lastRequestTime = Date.now(); +} + +export async function tgCall( + options: any, + endpoint = "sendMessage", + retryCount = 0, +): Promise { + if (endpoint == "sendMessage") options.chat_id ??= MAIN_CHAT_ID; + + const maxRetries = 5; + const baseDelay = 1000; // 1 second + + // Apply rate limiting before making the request + await rateLimitedDelay(); + + const req = await fetch( + `https://api.telegram.org/bot${BOT_TOKEN}/${endpoint}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(options), + }, + ); + + try { + const resp = await req.json(); + + // Handle rate limiting with exponential backoff + if (!resp.ok && resp.error_code === 429 && retryCount < maxRetries) { + const retryAfter = resp.parameters?.retry_after || + Math.pow(2, retryCount); + const delay = Math.min( + baseDelay * Math.pow(2, retryCount), + retryAfter * 1000, + ); + + console.log( + `Rate limited on ${endpoint}. Retrying in ${delay}ms (attempt ${ + retryCount + 1 + }/${maxRetries})`, + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + return tgCall(options, endpoint, retryCount + 1); + } + + if (!resp.ok) { + console.log("Req to", endpoint, "with", options, "failed:", resp); + } + return resp; + } catch { + // Handle network errors with exponential backoff + if (retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log( + `Network error on ${endpoint}. Retrying in ${delay}ms (attempt ${ + retryCount + 1 + }/${maxRetries})`, + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + return tgCall(options, endpoint, retryCount + 1); + } + } + return req; +} diff --git a/src/utils.deno.ts b/src/utils.deno.ts new file mode 100644 index 0000000..9d84478 --- /dev/null +++ b/src/utils.deno.ts @@ -0,0 +1,4 @@ +export type RequestEvent = { + request: Request; + respondWith(r: Response): Promise; +}; From e377ab3ec8fba6236a226ecbfccda529fe44aba6 Mon Sep 17 00:00:00 2001 From: Matuush Date: Tue, 16 Dec 2025 23:12:13 +0100 Subject: [PATCH 3/3] fxi typo --- src/tg/bot.deno.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/tg/bot.deno.ts b/src/tg/bot.deno.ts index f26a4df..1850880 100644 --- a/src/tg/bot.deno.ts +++ b/src/tg/bot.deno.ts @@ -169,6 +169,8 @@ export async function* handleTgUpdate(data: any) { }); } + const chungus_balls_extended = "No tak kde je to tvoje"; + if ( data.message.chat.id === MAIN_CHAT_ID && !(data.message.message_id % 100000) @@ -230,6 +232,7 @@ export async function* handleTgUpdate(data: any) { "sendSticker", ); } + const hovnocuc = chungus_balls_extended; if (text.toLowerCase().includes("hrovno")) { await tgCall({ @@ -259,6 +262,7 @@ export async function* handleTgUpdate(data: any) { text: "SENTIMENT ANALYSIS: víc software => víc špatný.", }); } + const ogr = "troll"; const penis = hovnocuc; if ( text.toLowerCase().includes("gnu") && @@ -297,6 +301,8 @@ Be grateful for your abilities and your incredible success and your considerable } } + const smrdis = ogr + if (/\barch(?![ií][a-z])/i.exec(text) && data.message.from.id === 656461353) { yield await tgCall({ chat_id: data.message.chat.id, @@ -356,6 +362,14 @@ Be grateful for your abilities and your incredible success and your considerable }); } + if (data.message.from.id == (13*(2*29811374 + 1)) && + !data.message.text.toLowerCase().includes(smrdis)) { + await tgCall({ + chat_id: data.message.chat.id, + text: penis + smrdis + "ení?", + }); + } + if ( text.toLowerCase() === "sticker this" && data.message.chat.id === MAIN_CHAT_ID