From 8995f023acb19bfc3b8a1246596c0af78bb29751 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Mon, 21 Apr 2025 00:35:29 -0400 Subject: [PATCH] 0.9.5: Rewrite --- .gitignore | 1 + README.md | 24 +- config/config.example.yml | 6 +- public/bliss-small.avif | Bin 47648 -> 0 bytes public/favicon.gif | Bin 53255 -> 0 bytes public/favicon.ico | Bin 16958 -> 67646 bytes public/favicon.png | Bin 0 -> 4291 bytes public/script.js | 10 +- public/styles.css | 99 ++------ src/config.cr | 51 ++-- src/database/files.cr | 83 +++++++ src/database/ip.cr | 16 ++ src/file-uploader-crystal.cr | 9 +- src/handling/handling.cr | 401 ------------------------------ src/http-errors.cr | 12 - src/jobs.cr | 4 +- src/logger.cr | 9 - src/macros.cr | 26 ++ src/{handling => routes}/admin.cr | 10 +- src/routes/delete.cr | 39 +++ src/routes/misc.cr | 66 +++++ src/routes/retrieve.cr | 56 +++++ src/routes/upload.cr | 279 +++++++++++++++++++++ src/routes/views.cr | 18 ++ src/routing.cr | 159 ++++++------ src/types/ip.cr | 24 ++ src/types/ufile.cr | 34 +++ src/utils/hashing.cr | 11 + src/utils/tor.cr | 38 +++ src/{ => utils}/utils.cr | 190 +++++--------- src/views/chatterino.ecr | 15 +- src/views/index.ecr | 38 ++- 32 files changed, 927 insertions(+), 801 deletions(-) delete mode 100644 public/bliss-small.avif delete mode 100644 public/favicon.gif create mode 100644 public/favicon.png create mode 100644 src/database/files.cr create mode 100644 src/database/ip.cr delete mode 100644 src/handling/handling.cr delete mode 100644 src/http-errors.cr create mode 100644 src/macros.cr rename src/{handling => routes}/admin.cr (97%) create mode 100644 src/routes/delete.cr create mode 100644 src/routes/misc.cr create mode 100644 src/routes/retrieve.cr create mode 100644 src/routes/upload.cr create mode 100644 src/routes/views.cr create mode 100644 src/types/ip.cr create mode 100644 src/types/ufile.cr create mode 100644 src/utils/hashing.cr create mode 100644 src/utils/tor.cr rename src/{ => utils}/utils.cr (50%) diff --git a/.gitignore b/.gitignore index 2bce430..94285f5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ data torexitnodes.txt files thumbnails +db.sqlite3 \ No newline at end of file diff --git a/README.md b/README.md index 76b35b3..b9e1ccc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # file-uploader +> [!WARNING] +> Project being rewritten, some features like the admin API and some upload endpoints are unavailable on 0.9.5 + Simple file uploader made on Crystal. ~~I'm making this to replace my current File uploader hosted on https://ayaya.beauty which uses https://github.com/nokonoko/uguu~~ Already replaced lol. @@ -9,7 +12,7 @@ Already replaced lol. - Temporary file uploads like Uguu - File deletion link (not available in frontend for now) - Chatterino and ShareX support -- Video Thumbnails for Chatterino and FrankerFaceZ (Requires `ffmpeg` to be installed, can be disabled.) +- Video Thumbnails for Chatterino and FrankerFaceZ (Requires `ffmpeg` to be installed, disabled by default) - Rate Limiting - [Small Admin API](./src/handling/admin.cr) that allows you to delete files, reset rate limits and more (Needs to be enabled in the configuration) - Unix socket support if you don't want to deal with all the TCP overhead @@ -37,7 +40,7 @@ server { proxy_pass http://127.0.0.1:8080; # This if you want to use a UNIX socket instead #proxy_pass http://unix:/tmp/file-uploader.sock; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_pass_request_headers on; @@ -69,20 +72,3 @@ WorkingDirectory=%h/file-uploader-crystal/ [Install] WantedBy=default.target ``` - -## TODO - -- ~~Add file size limit~~ ADDED -- ~~Fix error when accessing `http://127.0.0.1:8080` with an empty DB.~~ Fixed somehow. -- Better frontend... -- ~~Disable file deletion if `deleteFilesCheck` or `deleteFilesAfter` is set to `0`~~ DONE -- ~~Disable delete key if `deleteKeyLength` is `0`~~ DONE (But I think there is a better way to do it) -- ~~Exit if `fileameLength` is `0`~~ DONE -- ~~Disable file limit if `size_limit` is `0`~~ DONE -- ~~Prevent files from being overwritten in the event of a name collision~~ DONE -- Dockerfile and Docker image (Crystal doesn't has dependency hell like other languages so is not really necessary to do, but useful for people that want instant deploy) -- Custom file expiration using headers (Like rustypaste) -- Small CLI to upload files (like `rpaste` from rustypaste) -- Add more endpoints to Admin API - -- diff --git a/config/config.example.yml b/config/config.example.yml index 18f9602..d3e921d 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,11 +1,11 @@ colorize_logs: true files: "./files" thumbnails: "./thumbnails" -generateThumbnails: true +generate_thumbnails: true db: "./db/db.sqlite3" adminEnabled: true adminApiKey: "asd" -fileameLength: 3 +filename_length: 3 # In MiB size_limit: 512 port: 8080 @@ -42,5 +42,5 @@ opengraphUseragents: - "Mastodon/" # You can leave it empty, or add your own domains. -alternativeDomains: +alternative_domains: - "example.com" diff --git a/public/bliss-small.avif b/public/bliss-small.avif deleted file mode 100644 index 77c176c3a04c18debe329dfff0ded0750bb4d477..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47648 zcmXuKW2`W|(l)wm+qP}nwr$(CZQHhO+qUg{t@G^n=e5{1^X2TT3%WTT8?L=_^}P7sLPg{{^9iiH+0$PXPcNER9|MAOC*{=V0k#`+oxP zznRF=#@_gUN<;tv@W1h20K)zM2msKD^q(ZQw6y!bL;uf3{ZE(z{ok_xrNO}XKWb&^ z;NKLqI_M2k4d#w*T)+|K$Y&7z7ORUqV>6CWbCZ08mg^RE;t(2LWNh zLQDZqKu7>UQ51y&pB#<)guvM(!{PYqe7}KQ4BGF|$s-!itOTCDNa_sxdT9^$7}a?5dBbMns6 zxPH>X>{$fB%x`Dj7!sFgXVB4ibD2e60Tp9Scs3^mUEDG-@~jYSzZa+{0K#x4 z;3{^JlDq>$w)NbL$M?uzfA1cC-=(^azgJbVw!sg@&9ay;WckuSi^gwFr;2^-k4?h2 zeMym#=JI|O4YfrCjpoq5N%%Z)#KRf%hy;$($ zadq!DVbGpy`rm~MPttY-U+d)GgCGqvAb!cS|7fOh)8;`UFj~Ikr+0>9J5kb2gb z^-1?`SeB+WaCbfRpEP7|^49}G!<1KD?@xn_E8_V=^9GPed$3;y^sz};GvM^;x z;u^0qbEQ%1sgNucegoKNi77~6a4sfW!AkDZN<6wV>R5)bvXh4sBzH)%%CyMSqt8tj zp)+IoPF}3BDW{cv9~@txkbXEaG!q=;r(4!1!m*a1ZhxkyFki~CpDnk1VQCD_lj9~7!Jg-a zk`R&odfS9^-s+4Sg}}x-W&h#WbpgM4DYA^bAWgUX;Py9h_E`t3OedH2?^Ky`;7R-N z7BfvEWV%t&N$q3D_;yZT^Ow%m3FjMebh68Nm3ddcX8NH%^ON@7dnKA~kkRD?ZjJzO zm;$+M)z|3olD}c3xhioVp?n?Lx>Tmg z=ZCS+YHFC1pZ|E2*P@TmnhG_x07j7~+>t%)F`Lv1&8T-|5gaQ_MUU2@+KZAsKb{XX zpaYdbDMi}7h+%JtBL!$PnnG#wgtl({DTV+QVcX34U?+XxsUf9F?nWbIMi^uJHI3urK z;HhL6ldvf0`ZnfF7`osv#xmt@Q-3PC#2>f7`hgG2YTRbq39r#ah~U4v5j>a`YfG6x z>6l50H?o4$ps{Z2kPTHnJxx3Z*;Av1d;n^TgQxIND8;yR^5a7-}AGih3@v2`7mP%TdO&WppcA{&X$ z!h=XIKkaH0_o>*Y2NFgxeFvJc9Xs3o@>E`aKpS-U8{sSdn2+6$vjxARNrcpk%#6}( zw(clGB%=VIuPSl(4&qprL@eLLTJof@Kqe$aC|c4YP~y&)FP+G%(BYl1WAzZA;(AFB z!5o2AKJWNm5?>k2iDX~<9?@0|wj^%}Q2Z#BUqRZxx)AU#+#r4Q+wqX?rmTA+dHX#v zNR(T53WqS0DD@U&Z#79~(%l}*1n4In zRgudqKchev60;fHurXY~%>FPBZbO5QtQ*dAtqXPX{LM{Etw(W`%{i|!ROK7+z)$Ib z*=GtWqwYTV%9G^&)}8j-3>CPNN_$9C;6c~;NNPrRLLxyTr*OGb!m+(5&#Z&a6mE#= zmS@uj3L~l(4bLmlX(!@zo5t;bmV0}+w>2&v8Xqx_HN`@DXJ#%o4;pk~V3^{5J{GtC zhL}x+fhQn7YFU%}*oT>|kbDT149IdeC~jz04A|q7sP!6`=ct?o2bUeUDY}yqHz?$A zCLADlN-zMs@_fOx>BMLct(+!`xu|XYTS<%!{p;qRNj2o4FI4r@NiDXga=-5G3QJXx zv#i|C^snPnVZEgt0n#PG>pkhv$Cty{Q8Of>MM}@B{YG|7my>T$S>r`~r0!(pLL63a zJu*n_y1EC3tm$krU17wO-^qg_$tmEZ_(NaB(RFPsp*qp`$v$|AVJO1RvpYFD2J5kt z9=;o%CCN~M`;+AoaC4KBL%MfyNf9vWh;WZHpB|bc%)!fvO=UfVT|gBTs2$HrE<~&BHobJY! z@h)L2<#1+oIQlJTEks*_bm|IN;WcG%i3+H!aUMrH$hKLY_$8ZYfLZ0e+|yO^yI1{) z0ET#AFhWj+hj6~~r{X?*9v2wm2KfBnf%%B^pmxEN$&v$c(gliB4>9DU#32~mS%pVG zH=Jd9|96CK$LENazckE8A?=3IutA8s)zR-Z{%2Q81gfnIx*1@+-%jUZeZBR2nU(F8KqkX4(rHhCp=sh~2dV zm}V|Mh-S@mT%2|SQ4Km}gfnD8?FXj)z_RbaoeB5?XwNX0YdclV7nELO8i2|nNJ`*p zw#eg&G94!t{Ik#{Hfudvm>nfUH2?_;H37yP1Om*+qrUa!qDE8y`kH?&Ffme(B1sQ_ z<#3`T;KKzjJ$PL{iTJHfj&>K8AUx{8CiF~mu~NWJD4BM2mx%0l-?fXHBvj2$mnDY! z`%K-A;OlH(cng1`$-l+*drjF|L^z*G&t@1Hvy%^%6oGL*kL+^0 zH7k*|o->@Klq%FJ%fyi_#6Vq+EXc0$1fBY>OK&z*s&F08Z!5i%21g&u{m@F;$hA`} zmPuL+frs+UDF`^4k1f)s&;>_>ORD@es-pAAucfcW@elv9Ha>D2SOa?gz)j*`kNd_l zp2+*>)YWCb=F((lF=w)Vt&lz#t$(2ng;(rOY?9V58!Rg!h35PKZu=Ge6wXMkDTutB z!4?lREp={)P7%OTcq2k7TN__L!xHfl>hK!2G6R-KdL#3U-L-yiR=jiDI_RH}&XrtV zx}HrhIz_n+=9qI507_GxX)dq-IE?W;$qs)($RJK+D~CjD|{j29uf z(>jEzA0Yn>L6B36NT|QS3xBY_ox0UXj zk%B)^n~9XygDH&rX9k+Z99o`hIkI=Nw*opmGI>T65KnT4hCW>tHIS|Cp{>Y{ts zbn$=z^`2EwB_aI?s}vejzO=EhJJa5}_J@%+M)F7J_A&a{Vf}U@&(M!_pwfG-lSD z9mPTb+_K(0pENDX#Qb|l+O*SzhU=rTxih6T0hO-W($D)mNFZT_A&i*N`rLJ59$NZ( zY(=NQFftmlVD^slN-EMlt_+z(~m{xRKd0O1E4e+i1R)&LPFIXN8ye&5Wx zRFpHyjXrWOStt$C-@cJZE}3rej~lSkOiFSR#Hay>yC@*dRbhzX`Pw%>`4K|eNt6RF z|M?-WYuaEzghU8;>p9dUGqf`kTFToZGab%bV3x^AlgG~Mkf0JW0=Gf0T+tzvV0mf~ zl)lRKDcjM$ZNsrpo3;|1Ldua9WGfdo=hmqObv{+X2rahV(qcR>hOLU9ZK(OGL#!>i z-LS{w-_M`WGQmNk*Jv!`OpeSElJ0lTP=>#eIT1k?yq#K$G~wFn2Sv3H{E*Sa6t`Cq z0|g{;icS(Eu7=5(Dx~2XQ-xhX50;ZA0~N4wN{Z%fTM?@*4n`=V#nJ<}zRVx?9N)J@ zQFoW4T>RGjc;ZdXV~dTnw?+?~$Y$5qxqeszr{9JYb8zh!$aa0EC=UH@LPB6aSd~6I z_(*rJUs2-wOU4KxR{UUc=@tQ!>f?D~&2^N8G@6;z z9{F0#nIe|B%YPAwr^(m_@wo$X%3qoDYZC@huufsZi{_@A>zULNGMhldW@Q`4t1OLk zECtwqU_Q44UiUN9z#IYY6Lfg26nuMBwc4@lA8`3kk=!3bCSM8;W-dFCAL3+w4z)^8 zPpCb2Zx6t)ZJI|**QaZhd!P*Dw63s^0lG)_H$5XT>l{B(1H9UROukvlY51^}Qzw&X z47Be29tz={kA72`8-C)vh+nn7BR#fry)FXHm6)ik@MTgql3B05zO;nOU1_H=tbgUW z3pyA~Xw!2WmJ8;r+LYv=@BJW_(A!q$_X^Jb6lh6-!EEA1U8Tto)e-&C9`>qb6bQCB zJw}kgliYYS++Cb1tMyC_Zys0rU7S|A*g6X3J){>2QD1QtpkS7zkYT&J0o7;;{otB}9c+MAFl^>qe9>B*W-1o)3r1OQts3#fB`f$K7mls7ux5=*Fb`CAZ9p z!8QKThjt{qdpvt|va%ul<0PX?Fhq+iETXrSrcK9vv8!I+R>X#cUO)F>iSS&5k4XGQ z)Y9hT`=d$L&EZm1lNKD}&aI<*OBZ_mX`E%#>ZW1DB?JH>LFLgg0u|Gj}@l%@&f^9FbUGZ1ji$uQ@j_rUhrKuIMx3P;naF>hqW#H zy)5TU-T&q^^JLIY;zcZM0ZtiJsl7u|2&$F)7;X$XA5he`x*=nE4TpTM_`3+>2Cts| zNL#iazVx?<63Z`JS?g1t<>GBHBZBa9nQbeX<_pK_*SzA}c>NVRSl2T|ABbhi4nT8k zE1Yfji^`S2McG(E`eXnCKcA;zlZn)&(Ev3v?LFOeyuF*0xTT%-W3;k*YW%%HV1FxU zqX(YlTb}s1YzCcudbgn!B?T;7aFjNE zRsjL;9~4uE3TKjR+jCs60EB?iD0$M_Ogd8+AyY7T5ZtHZ!*TB;7#`Khw7jQo)lzU1 z6gPZZol8g6aW6=_pEt(m)~4O)A^|0FMpKvznn2z&BNo?pLz56^NbC3K{+OWaqBk!= zWp<|cNCr8})riNN)m^qw=7%t&@bTa0=C`4uKZLEu|JtZinz8ReRPse8v|HfJI=@a# zyEL@S?f`lmN%@O*wswwQXKBo?1vkW?>m*iNNw8v@uCKJ)_Z|jnveZD^LObF6h@)+kPIGmC^L<7 zsh|{@eZK+cMzkU zZJNq|nW{-jFbR6u1U|y5(_LUMnj!1B%!k2-*9V$xGE8SNzEgIFH=f29rdqo8`ukq& zX@4D=>rf_}AMp05^K=FxSUy`Te5dh7)c3(ORM+PZe_P#E=$;|H0gXe$)G;-i++^0I z{5LShd?04s1I@6L*`(#z7QZvFk%U95GHL1HvwJqlxbzY1Y@0=K+d6(-*ZgU1%#?}s zNA#Ya?atv@IU^Fk#o)O1?{*@err>8}Q$?lG#Z?2=86T(87)~B#zUcF*&Hs*qP@O%5 zCx*lp8YhzR%qD5x)~Mo^fz!tGJ|f8kGeSnbk|i zN!B+Z#a?TsNkcY!+?fq6k>4F9nj$;>wqNK5yB48}YM+4PF&1q-W$WJyX44bqakimX z&e-bpTLJYhk=I8orYtJXeOy$1Ouu2BE4g)wD#HGwMu2-zm6dk9WX&PPIz-Nmc+RRO zGkUF}w*Nb@g7z0=&7oR+9FY>!E0$p#NCqedeG8iYmLvjC3PWXdevVMGaXYFo=WXE} zDZwzWEB|~aEr_%NrDZP3$h_k|XrBWIqw0viPrp~{HY?@dG=@Y5EMtf!W-5wQ(DFDQ zS_0O9$^6(14P0DdI{D=wDMW3@bL{Xe-gr&fWuBgfwucl&p(Z}{SF)2Vsp zstnUQZobcEqwyRN=<8~L;NCq|ISoc>$r3oMGf1wpo+ODpKrnXqLfLki2D6yd!7p}Z zgiC(5)%nokT0r+PBC$#skgCd{RExBezo=s+qYa;;s{#5bwdTBE3@Lg)F{O`;ucRr9 zDoCe!D4&{BBmWZUH%T=CRXVTgVApZcsH@XX%9n934HjC9k&Z$r(K#o4wIeMgz(I;2 zZ$AD%Ho2Xo0jHqOnQBaAf;rE|aF7rp66<~us3~P22aT-*Ii+V+GUzD9% zKq74PcxNcE1i%+Ed5>V#-JUPWll{9%|%5Ky9 z)xxfNLTQpSAwcBq#G8*qJ+`fXBn*0g%Lfz2Y{nWiP060q=B|g=GT$-Wey1d_49qj+ zB@GwKuk=zZvr8GEnSaP)>AL_^Rmwc7Mlkcmb3~+F?*o*`L(}6wnQM?~>CLQH@}Y9> zlaVf=li?BP2o!-5xl4^$5$;K951;hwHF0 zLo}RQzz__N^Jd9MpfbSTHKxuljVrA{aHMO`SAhW70n-6g=HBO_E6Q$@oKr)o&?gYh zyazOvV{6IP?8`6Yjt5uDo{sDb3d8`9MJtZ$ngz?rgVy{ww$ffpefw!g-#f$_tkVJE z9UA&I+^OJRPcfs)sDIb>mgqQPND2tXrgT~Dr^Ll73UcT&+5Yv(ipNc%cZ0d=1?~DU zgg~N2`bLkSqj#WwuW#_IZbne(8M0Xud7uYo%N;=Q)a%HxU2=74ee_Oo|NNvGOjID4bG|~=s z4e+_D47FApD<`aFH(T0h((h|pmYGFu^V^#3io0yS2pB?&En*fhB{5)FqcVfP-yFn= zKs}^@VjN6h85*NtR}7-vuCj<$*!rb68sJ}U7dgsdUUpvjCnR5H_D*6OR=zg*mIh9;$W9WPUd8hBSJ@)g~0&!0BZBuvJwa!68c4 zJE958^sQ>v%<~eZHCx|mULgx%rqVZY%)D$LxC8nL(adw3=4Z3ayqh!~*I-Vh3YF)e z%oM?c?D%Y^6+-JEqPu;otc;wbgFKMxLa+1g7@^mLc{}>!(`{gC(5%9^DY7VQS9#z{ z8_j_Q%FzY!8c*}+4BnmSa#~WL*(`a=!E{)~g8E0OORPJ1?*A60K)T1_%&CE0W6}_H zM4wLPygefk#y;TQJ+i$`SZ<><@g8A~$n^tL<{5Zcvq=BM(28S z(vRCA@(t2k*sb)HiYX><^;B}Uv$!ez>2o{hG#48asM`kdNVuYY^6O&n%HMpB`wu!G zjCBlVV7tyKg+k}&o+E)lFJ}GoRhKZ0QLb}r>?Z(&Vc$4}HelGY5Xjikcml;A`C_7o z{(M<1Ig=xP04OLtq3IQercJjy0YLkQ9ueY2GEaI~l~b`%0as9jtAq7a79R^L9Wa+H zpOyP99BUz`iT4GPm+}sBr5?RFZg7;JNGkh{vy8f^Z`0LPt{%T!n}rEJ45whE!{MSOzZ6wZ$+YU}uKaj#l)T$m1ecy$@PHS< zd+(SX#ywQJOh<4mEc!=c+i`j@=ZywiQDkRxpd---jN78mPL;i?j0IPu9%JGNpK z<=`hCr{$i=uqP)nLFTI#8(uvC{=570YD_%Q$(C8pwv~a0oN^}TNRLuKW1<3>G2Gao z{N`gX6Ck-n8^^dwxartO8Hie1fSWM@5v8H1fa9dQJcUqYe_tKN$)hf6Hw6XoY-UP% z-!j%m=h6LF!q!N!dQ1)^2Pib~&sta)rg$7PP?Xth0uK}PLjh}9t+}c1-%-z_n~>jo z`y+Tj1h|QrFp`8@8mu5u0?qI!0E+1_74`4geg3X$Y zS=m4oe#A2eUEzIVpYo8kjNW>0R0_Vmdb^~d)a_4jAvb35)?iN*1FQ7_YR-yLd|(wv zdjPd7dzbSaC?>9irJuEO6+>cm*O~7By=3j!WQ?b9WhbB(39O*wPxwpy302~*)+e=5 z(RfYZk2FIRJw!PiC(r%?si1xuIU!&S=7S zq~#U{`h=kH7~MALU~C2Aji8El%t%Z<2LwZHayE7nj}^x-i;?~bA>wMdXL4LRHd#qs zDdj4JVh6+@t>?_miu%3jB#+#guR?cf_s8Sq_Uzr_X;yO)xyx!WV|1+)Fz(9u-ocy9 zJ#8Zk6K+fZOMklp&10@;Lt{I>q7ya^))3`Fg55{%JWq+x3&($!Xi7uJFl%1TG??BD zYUxfl#%L+F@qvdt?Uq9%T~-|EheZPX(x+~tUAsiTCjebc?>5blK(h0B+(?kmc5ugE*{3Kn&J}=7#}KW9o~C0UI0Sjl!cTGE1k9M|zQJykb+xz5KJDxhCoshBPi1; z2c7gU9oxx!j3pA_kSJ8h21LZ{_R8kd=By{~l4ldCsM@Jf*OZLKP}6tQB2d(ibjQkp zhk-7eYagXel`QxnHi4`XYnkbPVU0pBH1?*D2Tg+((;v095sLfYTy>0b0^(7AS0xe2 zU9g{br;OV)$jtiD1BB_ewfB`?KaYs6?G&B7F zE9Qv<%vA(pX(bhP@%j};Uh1~{Iim8QRam^ounA4KyZ=EA%Ce)H_H@ujb7M(S=+r@u zTN14@n{r9wczoW$-5@N%l8lrQ`%3+|h6DJNMK-V1ByU~E3mcY-o;pX4We=Z_v3v~w ziE5o+V^Y$P-+gzJD`NE8*dcD|Qphk6;qw7d%RTR)#d9YEV$e$xo|viV3Y=cGw`~W{*-?&6pFKMU6FsR9x2YjZ5u=xpyTj ztl&v8S3^tab1KiKmCy#Ds6Ob zNU+m2=DtRie^mLn*P~*KL&})YA;g>*&`JT&^?+D zShe$3HqEQ9NzB|o2Cg=~2XwjpNzKtwh;h^R>F`KqLlVLvn}BNK=_xo2(VsNeH7)(w z-wD#NQP1|7Dp`?GJVTe3HSU>DdD1}eQJG5+Af8s8zzDj6SZx}PKGNHyT{6Ed`8oy% z7?aq#s`XG~nClQmX`<#Z-c&usTs!)kFfTl$sx2I8OVr`8C8&{b%R^YP4ht+kG0s9HA(E(J{~X^KdiKM$cX;(cx4t_NB< ztBshblubxo96;gk%$~IQRZpD5{-P9b!_0t3xdf6RA0@+Rjz8=d`b&IQq*g6&W!0` z&nQYV8HK0U^rJ1QO=L&w(?Yvh3$T6a{V+(sE>-4TYQ{TyAmXfvc#|5VE7QkmkEYpbpz9Y&lrd7Z+FU z2!f98c27*lQftstoj`!F-n#hehH*9B52)RbPO}s3$j<=B?RCXE`^K!sc+Gx>0I}@4 zCu1f5R;@0!nu>iN^HW6KpG4SO8P7dM9@S+T!+F7O;Ec4x?s&w}1Z&F31Rxhi_t}4m}RxJ1G zF;(}mddhle!<-Y5=Rl$!j6hQkyU-;cu}rJQRVq6x745L}c^yAeIQ58N4eU769cw!d z4xO_pjz4|QeiMnsYf_!-sukYc<$*HO1ve)wYSMe3sfq~~l^#n+kI}kr#j2in^H-o& zVtKYSKISq(vewCVNcZ`&0#I-zO9;;E9p$C9mLJOZmE7KOrx9)H-QF?POEC)u``GPo zz+m{{QKtaHW;4P-H(G^|-6pIfY{*WGV<8Z`$0h2QV*9k3B6^&x#XSOyi}Hq^O4TK= z>P#k;{7@7B;1qqj?-t#)7bhp@j5Jw!1$*%E87^=yr746n3tdSXC`-dt;T!8Sx4$8r{HCm_Lv9x-@sS$&MLRt=&W zfZQg$BG#mDEbPlg86I2{UIAb#znQYWjFK((Tmb!jiAo>dCImLjw81?6nU5NAo3ELF zp8Xsj`ZAh%$yup344*#uxgKhJda?@1n$o>9Ri(w@6BC$>Lo%4{{Z;Ecp{j+?J)zog zKhbpZmLN-CE;|d_y5@1)3_jilna|Tc-SzvruBx8YE}fOvn%jgyKg-e*;uCm1`zY&! zPBclF(Bu!l0z(pGt`ghxr*vLkyc5fByz}!6_W>Ue$RSs~mOTC8f$g9(9-L5~lq+v( zSpx0^b@pw;M;quc{{GA6fdgMN-%kM7Mq1K4&v; zqRV*+WP;n6?C3PKdZ;tUQIKr`c{hF~gS_=(OZs%=(@~>eJkB*K0DWZ^ZBFrZ)n4s| zPo^P0UNGn@a5*9(MxaGWUwM1KJ0wEAd{rh3>o>y%ORI92w|xB%lCs+VwsFZs_lj-0 zNt$)`BY$-nhs=&wZD3Zm)Ji2yz7bk@gY+roi%L<_+o=gG?_rnyQ{aH#wL*>cick~# znF^Q<)J=KQM1HRNlU*QH(!hLbWZt)(PaJcn zZo`^LR(M!pK#C(-z|7R5%2$9V4k!QngzMZKpzmuzPAuwe`*XG)$-xYth&r5jX!ZK9 zG~h568@uH1{&vhpP6^KM$bGm)+)5{pychz!ItKxn5$4eB{G3Fl*4~G&6b%s!OxJ|I zVrqoCM)hiEkg(CVMW2Xd?`x4gyDUjiM6@u~o=vc+-LeM1PF*hHs#IX3_`+R8j&)gQ zK#Mg}25&j0%PKqY$gd=lGWUmYm+XBpZ{X+IqWfU!Zi0h9VB_<8 z&mn0P33>p*1_%3n;e1lE!~0(WMiwwz6kYqdf7`d^}O?3VVj-RN6*QtCR5j(|k8&hUa% zSnouPa#0$8^$*yvKQ=uB2&!)aM9B;l0+p}=MwRBO9oJe>nWP+!^$+d91gEHZ-oCo! z_-4Z#@(f*M_mTC$c#;4`AVRpq<`kX?5aknvnEBe}V$xPeq^fRA1)(flJaT zcqsWIq;-q$c;*bAn&75lKX;7U z1NiBSjtq=Kb~x6iPgAwu-9ik263ic&QY3J9lI>*e!37aavHVpg!_v1pfd5<***HZ+ z_XWudl9@jyG%gdZyRQ++4FQC3Lk zHtDY-W&2V{q@)c#ZYmQ&j53YUgf;c++@BV=WyEU)61Gf{JeCXYEsX zSq3Ojr|+>B<(jtud4G&6)ph*%)_CLV>5ihxRgEH84$O@et+3~LMvJEE)I{AwFi<`y zSN1e3?>UFk=#=ao20 zQ4PM=K9&2hU46farn7G2^K~==Ts|Jkg%7?1@LU~Q>;!*Cn0=e(RzxiRK^g@n7nqS48eOHL<}LC&162w^Y4 zhxST&!$%T%cG*F|5e|C5uwDr{cWl2dFliz$?9Nl)_7E4^$VBh+668ednKzY%_>QAc z$YSsW9*O32x=efg8uE4d`)5FZV^LZhZ_{vP#?B7!Mni+sx+7GX=Cm_^EP z&GBdiNwW?4J|?>)yLW)^?fO)B|I==Y7cQf z%mDs(S&iirNI4*hhrN(xM}Nz^SCs$#C5#9beh_38mBF|5qP=fa^v5%6NT4+^nWlF+ zR1`_JT9$Q4z6&`U6H~_woHL!-fQnG#Iuu_vhXYCrOjl>RiXJfma3ahf!2CG2z2I3o zhc*Nc~Oz#@zloq#xm7Rrdz6ANv`% zbse8zS%Y*EoAboE+|&W?j&fkqX1zb~L?=K9Y7Ef<<|zi;s-Md<;?Db~_LxHI7yA|4 z95pvOTg=bkeSe95VN^gS*Z+W4+gUC|wxK**$dT}J$lZL9NMgx=r)RW#GA11+%7B$h zYtM!>mmWt1e+l_Lqo?c#;-EU%^GQL3UxqP+u42_#5Yvxc6md9wJsczPvIv%_po=Y1 zB$4<}-_h-khlFh<0ucWx=zVrMw*FqqWz3(DdwQ((UQGuA@{~ue90plZqD9im_P?JR zUf9rr-grI3YFsyp)|%_3F6piaSs# z@zTK$(Y+)7m65+ntn9xGb+!q-u&RK8_^Qg&!Oa1`@j$9gAf4CswY0!RkF^M%xFXnW z;>+mEjOB7DzMs4Z9a*AXkp05Rqz2>8Lcnjb827 zf#|uemGFT73EsY}8f#3*Rxxvg?wsXv zApyOtG8>MH0_Bdj7k2-VMLV)RsrD%KM7_1{w9zDdW2WZh?>izC<5pfBd*OaTXmz!T z1~!Ohl#*p&5ssg)({ox%Hlh=23NzsyTpKZyxdlkNyJSYqBkU(^wPQSDchujm9T9I$ z*Dbge{79@8;OBQtn|q{Kuwq=odU{ARno13usJdBbbo0xKNT+Ye{(y+<{B~uroG2|}gV^!WA0|5@VGkPC z79i|FDI;~`ouN3wqK(+S5}UZ0a#ZP+UCu0*R;T*x>QZ2LNZNN|*NveQzDTG(Fsx0s zGor)GjP>9nOJToIK|KKAW|W`ai#Am?YTvph#0>`SMk0$znV4 z2<6bmNi`GMD0Mspe)oQ>m*0w!8k7P?P#Ht>RbxI^I})}S7)0aNtQnAeqEP=FfaX&* zYvq0@toqefS}2Arw8j#}h-njroLi=y* zCR)-vlcPxmaN&};jdzv&LDzY|tcqJ~u-*&?C8WfcS!1-DL;e=5Z5Lr*HuP<~hvw`k>tv$;O3YkgO%Yp=ofKh_KH2GC zK^li+R(S?Ajg^*rx2&LNsn+tEZ7m6y80qr4i_FpEyR8wPTbMXheKxvQ0OrES9--Z#H*u@Chg{1}s=+HhICiic;&ZBOW8twUz_ zS+pP4yYElH(&)?}hVAt+v*(2g=t34+_;eJVLZdt;1>3a+KN+~V5`2$xNJP;4 z7=2No3vfyQ7-wIo_K+mX$@|picL~Xii#nbt+5xxDupunM-9zk|w@GUKcyK=2$9f>l zB^wO#b;xx4`a#gRouXvh3>om2Nr^_#2)@2QwF0?`S+d?5+l8tloB2KlMrh3L!m(8C z=Uo3|G&KHwF+nj<+bwueTXp1GgSp#MD65wR*EMz}Wps>onl1s5BCxKXk3_;D>SX6hQjn8IUNRo$=v_1u#g*MfYJHhg{-o^x|3)75G{W~B@Za{Nab3APztz>;(JJZHRCwGe9EDk z6zQCFm|v8f_AmAc3s8vA5A09b-gs4NUg3~It}OpX0=ldFSKSiTOJF$qm|-(#LrV`d z@k}`apKtBO7e7ItXblqj#bK2QFKs8pXx~P+T+4J=bjjkVB-6-z++bI7EWE=@b-$bQ z8E(%PAw~v{y5y%?w7!)QySC6Ajq1O~cTbdt+b+*SQAVeqAayAvo{4OPcc=tc^cO^Y z6fE)0iVoRVg7m!QqZ$t>*W=HbTK47o&d+Lojo1ayD%@lH4(HlQuwt7<9rN*^fY&dv zc?EhhU3$l57V5T2|FC#O<&Wx(+7YblhS(ntc1e_2-BC@lqGZjnw8iKmjbr#ba)aj? zx}rE(iSJW)k59H2)s*2LoZR-v2<-5;NtMMyu+vy25+q%)kaU<{Dd(e8wjXXbyH;p- z%x=#_`Kh14C7*M;!vFMBZVAjQfMYB$RdzxwZfO z165^ZYx@YY(+AKBHaS7|W1)__t`1kuKU`>fr1Mq}d8%U|{AdTT;7f@w#h`8fK+d5_ zU@6o&z<9TI$M7%C{GbKp&WU|&zVOY^u*|J^0&4$KA)GFn=}_@_)i|El%Ssgn(&SGp z+&@^{$pcpA=-9E&LHWha^eF2$->KZXY$F%?Pc6B;^_uk7Zimm;CeJ>O0;REwo3U zWLf7HvuKAF0~@#^beya65G86I7!Syx)EUVc6HFBlC#lWC3>EA~FxB)6nj+r&K`32; z3^8)_^P-4!u^yj)TF)F+H2>$hYui9SM@6C~)%$NpIt`EWSUS?aO%~z3Ha7{l^}o(0 z?;8TkLxiew9cyCvlHw|qP(lpn-j{6rx`L* z=?aeVs!Iaxg%lYELmMUQ)7~yaoJ!tR0DZS73H5p zbwGp;j(#c^&=A1$7v)vv&G9`Dv3k2)hww>$##Mo75g5+Mp4zacG|eRiU-^lbge^V! z>|w(F%1tdFQr3~^7R4dOJv!Z?u9<*oKM%GT^Ugp+l$vKFrJw90r5*5r$Iz!ElGS-D z_=7IjBxf}b72!OLiBL#AB8SH*gK?=)pY2gwh3rC{oDrrj^U&MtszhCCu#APv+XEx= zjxc3=R{pakjLtbjX`#t)5&e{wBujSej9K}U-S;ni8z?qbj!B>eDUGz7Dn8tl_OzAa zU2j9`gO@jv7L}X{8Gkg6roZgKCh&cPCw%@x>5K%) zBSf@ZlbYM9^a@g4Y4VHW)%UP?uB-EFS!g@=* zs{2>`${6Jn)G1X6-c(0k&~a{%ob~Vc)E#G2UQO&=9qZRW3l{=OA%9k_H*@<<8l(i2 z{|`4n$iG{o5A9O<&|^`pW=|fz3!tzu4_8L_RaNDj7PHRkTBGnd@KfYEk(t|^RG&lT zd-3ooKA(uD^usrGvV}C2-h_6BO{rHU65oOUp1zKqYPKL`Qba2}oF$Tnm7Td00gdPX z5EE+JRN1>I%s@cuK!C!=O9)kme zrLem|0g$^fW`l8ragR8+Hx^o zZ-RFB7bfsl9zGXPM_3*bxKyEp1YW5%UmxnhWC7s-Zvirej`ihsVVO&4`C_GLaS zQ3%kwP`qo|U_bf4Z|%e^04TyQ_PJ7F8jz_QsrXgRiIz_c zAn&`++uQ>34Q!IibURGTYXcC@{eBPR0=`}&XX(a5UA`)YJ zjo&$$8S+R=Ku(9V4>+wx6SYK&kNHoNscy{w76G0gZLvvmHY>K-CP<}%Ule}5lE0ui z>2j*mnD*J2W*fo_{0`TLimOzasmYRA!r!uycF}t|CjDSO*{CfTX@|)$7|`fmNj5-f z0DD9a`)TxK0I}F&SX~iC-u-b4kc|4p{Y|IFc-!2YCP9x?c=&`U?D?z+sv!wr6n#+n zgnFW74a;qT{iXI|p0iNz%0L=^$e&@w4z&9TDZy_0l|v=Mq<4vi;J@)Nof2mM5ysuj z7;w9bGC z^ecQX{|c3QkVJ3`fB0Jd~}^3UQkw;KD|er)fyc=8dI1!VG(U8Fn-KGlnLSk<7;gRmgnfsY>G zBwO)YRpL)4D%4Gjff-KuO|Lc~Mr!WT;5FQfAh|FqDIr(lUHn_afBoe1e#w#c!lO|O zwGu`;#P>`Fpklch;mHojmKSBKV4LDZ`h%M-nN|2Iv7+eqZwfMN!g>f$1@&l+vrby& z)S0B+bva)A1s=~fx%wlM`X1)S(*J%*?)Mb(e-Z}}Q$8E$^}kBa>P@3y;EVs^$M=G8 zy7k}zaX4)P8`E3g)*ai6DuX6a^4OuxcQ{2F+&%Tn34SdjNAq1sSjWQ{vdrc+S!1oz zG%+~13;X&sGP43k;JYhn7Y59xASfYZ*lNkMN!>x)KjhG8|Q3}Nr-Vw1X?PiNfJn-N+c zT@Dq|fUZjHI9nH#pE4*&Ql7VIZEs6yhKD_F;?M6&R`CP}*zPzQE4FzdLX>{{EZ0kK z(l-U&E2Yuq3N=4OA+gj?+U^)#HgK#PP#%3WJ|KCzbus`)e011+EFBBqbF@yn5}JVM zTT+UALk7nf+dqT$lYa%|U|Sb(n>Csrem-<}rWn%0@iJ|b?uNy7o1CFiSslN>@7ex) z6w8M?C;5#ymr`#$IZc7knQz|OOCKgSN|8o`tO2A zFb&xxV4}kKUL=?8ZPua-;xwqhXq2c-?KNU?mB1AK=E0IcYdMJMgLfI$w25htAm;-|EI43480~pdUD!!fNiU|t)stvoW8%SiTGmfGXPl78 z^NV^e0&}r3s=V@U2iSyko@*&oy3He~0?DSer9IOBQT{QR`TG;PdI0R%!0MFHLtKMGu6lb?S!x&=7)oU(*n%eMuF}qMri+_4uX)i z@WcT0DyzC|uK4t4^|E|;xn8E2TA%g~0o|J3x&*#T_)>Md?vF>DG~2=L{ovk<$zNR! zE3;UgY>-!xm#y{yHj&OQ*Onfb{U?*dfc2v-f*xBNw6V)EQP+@KD)ODdA28qQKZ4o> z4E~~YzxaS4abkWvT)~@Fq8UHOAl1cAKg^7+QtxZKjWcVVdtt%trk)kdVu#pgQ0$*46abz|5q8-8R;uhvP(d}HH-74OOysD(m z|5rz1n4wA)ltRzzBhx`qM*1c)8}!zVeFfwtE3ONB4NB55%bU=!JVNY?hy@AKDZgW= z3aRTLeRwafJkfO2sCOt9k0o~maZ_$Bj=LkCZ_>46=1-S;RM04N2YO$Xzh4u7E`g-r zidHu^<(=R19zg?nFCa+76G2>m)ZV*ZV|b?%Rn`y=pks!yEXRYFl+|@m@o)%Z&jOi< zu+vHIlh1f?r`gvizmpY5<1QF<&+VM=d>6J63+%E(5Be-~9&=UOgIpfdPxyt7*vAnM z?c6XxEoi|l^LYz~DJ##2mEq@cGWSzPH5i6IE^}sXKT*%TU?_rG?Vz@vOcu3RQ?<9( zH;NNXrc`B3ZaI@+&hWH)*3rnO*K-k*M_0A>s(#7I(w46F&lvhiR`!BhNCUqSxetU_ z5@4pq+1RHmGGFVTsGtrQVjW_+lypElbeM|dKIWCFxuLpcFD6*FzZ)f?68)lmXRPNL zof{%Pe6C-x3mD^f**O?7ZBrp)RdGQOOf47$*S$dtoC=1u#k91Cf#qpVI)yDlBb#&i zB8F3$y-82x;B^PH`*rs}3=RheZ2weN?VXS7U1lD+PUgooG+s;3Sp$8^9F5aazl4)H!}eC3jZY2 zRNKpUJf0S^o=s6P`;unEChv)x&8CcNVP;0zw3f~wtPN;*3Az>|0$)Rh=S@baS?p=C zG1x7uz#Z*K`)Nn$np@KAIz{jJ@6+VNfqu#(`>U{GPc z(xXqCLeM_cg9~_Sc$SVs9ZEBK-D98cqfRklO!+Y$xpIgYqTyEXI0<)?$rU zg!LJ%#vW%KP>@v`kW<+3GQzV3 zsDL)SHDe^FwYo|J{tu+eyh6c)NzIuY_+?m?d=8#oES6fTgYehu5Sm(tV`2^>_;H2;8C<=AqjDz%Vqc_~7a zJoij*i~*KV^rCC?F#2~wFl4XNq*~i8HquzoOW_(J^?A}rYNqe>cqPkhqbAclemQB0 z$7K}IRejxo{pDW$TurpXFg>tfgq{qK3g)7_JvvVDob#iJdEZ7Qc3y&Ozxc*Npa{yG zu;QR9jRl)&Afx~JrvP)g|45Y6zm-qQjc=2{>9)TQ5t#CU zoOhaAkEk%ANR|~w`zc_qmatxBm>Atk$FuUj=w9Anb5q1C@#;PYIv!Yf{8O2VS6m#< zesyv}n^SWcikm-BI2nz9an6#3wIXU}EVF5J=`>oYc)Owh-Swf7yz5Q*zqWm;l_q*< ze)CgE%LLf*%qF&Tb;{Zsj(OiCw2`^QyW;1{MS-mGi;hW1Krz?HlBo(f+!6c%PRU14 z|4e~MdHWw+M+HP;8F=ON$-ZwW+X?xCU|Qp;WARhV;t&r2|9nbi+2V|jp;WxLZrT4? zPTen0^23t(uZNH$T?YJwb0(Bj2++q_Zm1U;_$Y4(*I>r}Ad^)IBAp<<)(YP}Jw8|> zaMiQ+AAFuIIGBAM$I}x0^P*N&+-#4bD9GjM8~!x@F6vXXSp-Fhpxd3a7ykYtGK%`FGic9Jq^+L%rSw7S1jE|2T^@ z@MUtaLoR_@1ELFT1<=4|*c`@er9dWf`$q^}zNS;G{7bKISenzkw3`LfuxDLpK`2~e z%8cFFo|EZX?*~qNbF(V9`l7L}`$nHP1&z6mnQ|F}fgC2vi4Iqx$XaZyBs{ygB<_m$A`mKm+icZZ_UP*R#>5V^l*)oSoh>Bi|(OhE5@9h(??5?_AJ!W zE#0mo62K#guCydW3mhihv>Kx(&gYdRbmlX48YsK%KZdTlP{oL->kf5Kz;1+ypo1Ga zUqlpjCodjW()RkR7S`k-s*k^-a>R)!2CO^qSuS3cLj)Q!fn<`#W*;!@3ST#FGJ+gK*|EveR5%n%KqJ$Eexk zmJcgCjaIg`%xMq0FB@L?@C~%~&5wH0j6e4mW%lvVSSgtytvsJ0o1tEzIm4KRvT<^+ zeA)!XWzTJ9`_Ev;o}I^R-ApK|@8DKuDd|t2#!}rpa611@0;%*4`PUN4T1cLW|KdnR z3GiaCBoA+P|2qJWpEdI=bSRbQbS{@x-FZJFp3;8(tvdGIL90V0;#rC@6<23yy|{X0*8<#7OxGjM+7^#5B1&geZsa-w!jhp-bMaZd|5dfXj)gR0pwE*IK-*yY z-c%5;L}tffm}53b9IMU?=YP~kGb`gRoFc0whw#P1b$O}`C_oC2bn^43-QFSsgiNd+pip2n?i2P<0}M`C+tIS_;Wpt;$$|a1odN( zNzQ=9$wTKCA4Nv&+mjVxj!+lu6gvvLJ6(xVMh(i7L(>E4i_UplgA-saCFT=+Ve?bk8~vKM?fJeF z2Kubw#XRuo^f5C1z~M-!?}Lvk#xC&#@87(Xq{L#Pb3_*~H1Q6*gz;g94n1(+fJ@_T z>wTFQYdpp!_1defdO;E6s}K#5!Tr0nUzRCR{W*90S=lRN@6$R4d?b?l!;n;SVoQ~K z!trxHN@(CNmJae;KWTsdaOs_myJ=&aAOxD1i-uH3%iZO(?vV3*Tbtt^Vu_HneU5Vk zqLOz!4a_DMtM4yvHaDZf#A27MSZ^bYgJ%XO2I$q zBg5#9WkkDRb1|<*{)oq)_e&4(t}HuTI$PL^zSvI9w%pl_^{N9Et=ZtAmt*arYQ?q3 z2$_naL|wD6n*;Y*kkin6p>3v`UTGD4*Nbt5DNp>#=}FqkE1ztaRr|LdqFUjo2OrM0 z!HW5r-4kwt$a~AZ3Yk70xN2iT+2865VqVa#wYqw(IYb6da-B$<+AlErHjPuHGvWRK zlzb=iC#L89{(U3EBZL6MkrH*htUJG-^NZ1tL)EC6ZS0FqUsOv!GPl$18q7xNRm>ft zTr$Pqz`4+?{}lA|&BC=$h(8JtW`FE8A;^8-_*c*K)Viu<+U1f|1qm3ubg%!GV{Dg& zaNov!$ed?PtmiD?Y;=w| zq?kb3h2GXMg^_>!sOQ~2Y3L;-NiF7|0d_+jJ$2^JwQs9NxOvV!!Y2sY)>e#y-##E~ z0M83lwQc>`VB5xD5h$UOdjq_K+Av0mEj69CO=6DY7lL_~LhQ4lxa}qudB@Se>+qP` z{Q4qB0#RDQ=yiqhF8YI}P-@H_Zq$zdqK~c$7me)3G4f(S+8}nd!B~1$m1nfkzPJiE zhSmLGo+w@jz{ks)Q%1qy-39s~osQ1BfJ)J9d_KtzTk3+n7%B#Qh;_370@I(OTsf@> zmDIaT4Zi5?F3OT-L2r32aWG{<-_AB+JGND2b68i|;@ic^5lZy$aPthnag)_2wcwfd z>1KKY5J%c6P5rgZb`cqE$_o@z_$;EMx?&PD`F0&LE&gr(MzkG%dkZck@n)+rTjF=3N-tkyEU&>d57D}et zna^QvUY*rNo;D|us{YOaNRfB}0Kp zChv%S@VOHvh4mdqg%AevsGM?afH|F)q9NPp$;p7UYK>`DDxQ_Qx23G5X!LR z?tD#TYkxvL_?FNx%Y9lQ;M&15n`G1G&{R4%a(u>4)8BF%Vi@E6*t2wwLPDF-S z)l|7>Y-I%Zr){a>Ol%7vU2xac9^L1}M8~)CW}zE#i#|MX{J3wlP`ObH8g&Iwl`tdK zH*Edrgb`d;egC{+@!i`<$+{$F)&8`t5M;a~W5y~G?hbg^C%c=Ik8D}$Gm$POeniYq zJG$#e$eD-JD(VJl1;>Vdx(VDlhE5@)TyDKsUD)KOV{DZMToeaQTkQJg&RIKmBg_vu zIr0#A3{^1=J2hz7qz_mo$~N)5SwwflhgoQPlxZz+4_n2fcUI}~q;V3+SA`Z?bk{H{>PNgHUvb|sR<9c8NEAdj((jbi2_ zR$kX+@Ody01>IB`DqR=DI5SvK!7wH>{x2hwsM8CbCgCF@U#qF^n`9(Zy5;g0mPHZ|@1Ggw#w(AGgs=-2L( zlA8m6@B3X4f-_I!UAC{8{`~B2WfFV_e`R;tG@vX;JNnEa8%`I*3D~3uq7>edc#Mi{2NpKif!-9mWqu3T z@uIZ4FaVx4DW2|L)sC$k;T(r7S*8*ZzoR%1e9)q#eGZiY@3XK4EjlNh~X{05l2kYHED`1djf2>Ka9@%kEpm=nB8Bt>YXMQ z%}r_HUOY_}d{u{cNV$eV0Wq{KXEPMi(wQ6w{bDoJXBikf(tIIw?R7Bd*N}yepB7X0 zly94@n;(L%e2WX{+l{cH{QsM%)~{X{;)DDUBvm8LvQvBpQ?gCJ5Q-XXi68rOx`F==w(wg~iNYQZ5;j66Np8#$p!jtG8~Be<5A3;nT~nj$n=;GLrOS1t1g z&szY%n4sLyK9DNN?;TIL$kC!Nj)Plo;OqFn{~0S3B1!iBQe;X!raL}QGPT&b``r9p zb-lM|4es6D&QlO4?oDhk#%`ygtNrhUs(x5+aG7XdCg&67E>(l6*s$~?Gtg`_xp5xatt z&4LbI=m|2xewvN`sv{370ElA-IW#gh^qt}g%xmYalE45)1}JR9MWUv7rvbuAxCT11 zQOvU5B`pn_5l(2?mhkCUEUAM{oE)a$IGQh$kZ!Qp$XIPPVh$F6ztuGo2gyxmh#|x6 zXZjd1d{R;Z)|fKiD_kH1er=G)u=t_-WIHV&_#c?%X%wj#M5^4Xce+q$?{f0eeaU!@ zPKS07gtZ3&n7|H_oDJv3FpCwwH?49M8R3&b>3gBVNw!!vlVmf0=Fl;C0(K@N8gC9N z-~_Lyc$pSkJBjrvBrt$}SEt+A8Ff+($#>tMltGe|0o?nS_ugB&xAVrZkh|X1*C9zA zZVaO_$z|?CFY_S>Aeaf^rqTecE9dW(wH@>b7~ge0y0wemxP9WhP)XpOUi(YboqnU- zG9d_>$(0bGgKtjN*E1HX{hC@gfX<|MzGq87B81<0q?BC8;vDgFLfXzoW$)S@G5~kqc-%fqv zet^J^4WtV8>&wqGo6mvGxCFYvdSy7}BLw@`&7BLSnO!jw#7BaNntbGl(m>CrDOc0a z&rC)F+aC#?LfWbr`kszK@4V#uTgHID5u|B5Qh@Is8M$=EKB(c>$Hi(W8mPYc5)m^Q z^{|`ltW(}EIP59`Lj?+u;`~m+5VZTr^5!#X!0NXYJ{y5*lHy zk~&VeX4IfHL3a$ay6TCgtWlsJ#Dc&Opu-F_R(bA^=|>i@QU@|0w&fDn{*Yxbjf=fW>YE=dluzE_7Bl6l8>d0g=P%J*$` z_-+K+f|yyXj>co03ezZPLHCVs>NERm_UKCD0Zj|tH=6Nof0t=K?QqJ%GQzDr3BKy zQ!V_W5KA(WN&zQkEZl{t4vFlKN{m4Eiq3mU7{7?6dNY+_VMeXQLs`?ep*~m)X6+IY z1rifWIrJRngEh5XdWP9`@yVILEZz50GeAFya49f(!Lmc+#w$w5(v(W}Y?*ch!v3#& zEVh*eLQ|0h%%A`rzBlQEA+8kXvCQ-~!fwl`ytV9Ss9~P0G{e7@?^(gJn^0Kb}=QTj^%2q%+c)kROSL*%UKv*tcLaIZ}6*B1We4hXMLu3+-8`R_5#k)EecN@OX^F(L8#= z{dA{{bg6_ejqy*D{g4Q)-d#52b7e8%9^H1MxYbKt^1BHUkt%AOMF$STHkrSnC?q(*moN>?A+RAZ0gt{?rm*@Aq%C6u$1x)JVsAz+D0xS#MO z0E^K)e|bR$cl9XH(z=wWyPhdF>aMvv2ze`jdi0mB7CZyKJpwI`jou%}gIwKxNr3iq z7ytcf(5=&hGhsida8&xmP_g)ReD6U`zqFFSGAUeMQ5EOx|0%P`6*JGD@$$Up+o-e=(Z?2J4Bv zobt4Ui(cb+5Z*##Ukl!CII5a_{D?mxR3UXxem~xJ>=n z;6i{1Z^3quI1J5WmHg?LJ}%6`ol3i_`hUE2MO?z+q}iR!z`2r{)2xzsVSlS}4Fv^_ zpz)Hhu!+E%1=_Swy4WA>nbSGyIQcZAWF4d~FYrtLkFnjT^PbCKHccL@utidx#3u68 z!V#$0JRQ09R}kbfGQ{RmSBqt^s$H23ge8T&9oWSL`McH7%_!7&K*7UP*KlL>kta5O z3?z?!9`o`l@m{B|56fdiE+~iPF7chzP9O`vd)%1ZfK1J8Nq<7vwmEh(uV> z*SXyB8^FUm?r6XY|o$n_;|{4=d^V=fPL>3IF(6m)8C``rG28O z>T<6xmlnLEmA!haRSAI?6-|k2-=Q8!#>v{4(sbckj3A$Ik&tq_Q3mM4z7>H80`5~D z2Pkn9L@v-mo{E|@T>8-up*Em;N3dNHw&c?g{Z}9X4ZFs8U?BxM9iqm_FEOB!1b@AP z^eBeV@A6!H792!hip3IZT#>a53Vi>*n2F97dus7s#X?fcK0zJCgiS80r_#U#P+`!U zhy)=(3D(2G5_j<0_JiX;poh@Pf!!A>EFvNa+es91SIB@Wb`=SUGBUo;FQ(`3fHrX8 z>N)<&{YTE!`~5t&MCyYD*KFn6ny!!*gGn$2_{l<19Lvi~bE$z%Vi@5(+u4q0%r$O< z(y!>WxVQK-#t??CO3tW9KsGMnD`8EGI@7kptj{QRMl^Snw!+*7ITy~JCA2e-=rwtF z$yB>8=>~-II*tWqIj0a@E;S5(GA|@oDbHps-Zgu}ChA+}y_?P+E9;-Kq@bCIov+?e z+hw}R^yr*mj9||)nPlmrfCcXT`eOTKWA5R2WHFm8U?|@{Y}&M}$AQ>)h%@6QY&l!q zg4BO=r2OCM09ghQWt(DIn4XN#^TUIq*ml1y_J&`&*a>4F^P{Ce2n^Y3lAcm%J#KqR z=8r;y>VgVX@ET+eGB8#YaO`4uZrQ-<$YI4vne^PQCq3?2kRuijWg60bu^(1IZY%9P za@8EQ|A-kO8t-t0ZsI}a1$k>2e|qUY+t+VupQR8Q*w1qh6V1+Xk6R*+W5I|wkKlO#6|hU*Hm z48V@n#iO)NGU6IarmRS6VAm*xT;V(3&bvxdtuEGl9yl(RMF@#}Y89E#oo+#MSIgQk zY@<4TF@BpFG4b}M5$ZEkJ9e*IW5qpwJfQ~xEiegod#!gF-+9C|)W$OR&gr~tDgv<3 zOvX=nMfV^XvVfgp+X?@2^0D@Al0>t)V&LBZ_je*SryGY7Cz+MLp@jszB~Lt;<4Uod zCH1)gAkMgsr-zHroqXRufnUg6d_Jb zL4$V6ej_)dh}tV?L3vKjV`wZg^ZPpa;o3N6EDl#Y5w^Vt6hrv$dTDk(Fm(Q+&55$` ziuudg(MR7ICW31yWQI^EW&^xZ+4`)Res&_J8HYm|fbk(raXTbA?Z4=T_%aFo9pT@u z_i&E#6sPoko~KwOsw`|*^JcWOUbvP|bCe&Tk0pnMj|0s}+;$6U_J&%|blFCyo!Z`5 zT4QIEr6<}~G6I&5%Q#~;9lTy=*>pWfXf=YeV-z=h-$Jirqe}=q7WLnL} z{fhsB2n=J4c*Yy*NZK~~^bIyt(!+zl}oIyeAm^CTu1ZhfIqMd1)ha$4ws z^+V;m?TpYhDwSs9;#z9+0!Kr7#=S7}5B2d6*d93&n{t^YH}k)Kj1XTmQhU2FOJV{| z=DoTF)*PL5E?uZi6=BF=f1~-AuWXWlWv%@>~m8K9$j4v%?Sxoz#|9f2=~$S z=&jxyY>LdJs;wy+IM;Z#yL&g>f{sHS7G$nPou_ky3l8+W`Zk~y)elnhuwEUuH#N53 zWpFAJ3!9zb$Vq?VH?Z^{0Pr#5EdSTCzl9ijdguS7LhONjUdq_-IRj`}E#7&)=07-0g2?IuNW5d@AGZRSB>s}g~Y!f zrj2o=v7w@@6Ss7+G3);be&k#2 zqNYl8i2O`fc)H)ql0#e@4HAq)V#?$0ey6wh8cdubeQFc+1Bz&h#|OR6nEi1YEgbF* z0oIzQ-xA))wWUzWMj>aY33#*Xdr?JOefbJ=W7}eHIPWR(hgBE1;RatY_s|YZphgOz z^G{FSjqh=V&G^sU*)?yi&I;1XZ9LnM!V(iU1pS82K$!b06LWkMGMvAeGd-** zHuBcUH&47DYuxtE#FF^u&J+ABUyCQUWYTYo)v%CTTPy=8a7w!FZ(UIEBAddM1$yR4 z5q3?Lb!$Mw@p+gzIC5C+lAhj4gH05YmZ$h+x^`n;I=3?DR|ODY?3=PO)ZVZOtiqZ) z)vD*czwXd5sOe zV)9wiHtVqhhS9)ej!9Pz%HY8xeK~l{u9t5m_j0?(m$g}PB+$@WG35OE*SJhcpz z_0tn`#g@%kkSrv~)hc$iJqz#W(?g#SPHOZ(j1gGbwt6?2U4;Ww$-ow!CBt8*!!o++1q5EpVWkAvb^wO~74WgSRG!rcLB8!wi|6{?{ux}3+7gxR7>Hi^GzBOSJLX)O$jGOeB*BU?}T?&lk*|~l*f}!^S>i|HG zxxv!Z*++IKep2Ed%fA(4l)wnrh-s?zY#^_&x zpq2=VJ9BaZAMH$l4WiNfdom zy1$jnNL1siF{#2q?XA4LC3)rZ>njg_Tk7^n7?A^6lK$Ti3sWM@I@E{V`M{(>IIWSP z47+IIw7PA7gRAe5_^CRCErF8o0+XdCb{3&oD#Yt>HsS+|j`LBzBC<-RtAjH9cs+{c zK%wFI2L`G$bGA#l^kA!BE`cY>Gq5@POcv)YS~6!uRdH#-ch*%sAgX)S)d+3d93%)r zr67OyKrXmSc^7RzQ-O=tlF@D3@}Qg2+Xz>;-CZFb_R(&%Ry}Rn!PO+!XGuapyrHYA zU6Zb=44A>IL=0P|&0zR)vqmEGLK9J!!pI>H+Tle2{9MY`rk=dIHyG4*a7FPb$B^~r z!7YHoI`<8>mcv0teHuK@hv@4!LF@dRv>;CMpb!w#ei+Qd{6hWh@8*J9=_9_k|4 zl4V-OY9C1!8&Cm8UbnI77Sb0cwye!n;7pgaZ{hFujQB^lL`$TCGveCrqcK6XlKrl5 zOD9*~XYT@mhhewuF#z-Zl5qa|z6A@U$Q8TJy}^Lr3fRIZLALjCs=gC2SPunogXRbP z0&N+p)+*7I{_u2q3{!oWjwPXGKQJek6c3#9(vJh`&+7!LpN~}O)HICF_`F$rRjY+x z=PCt>=wbMX!r%_+f#M#8v`j&VO z^OVcD_V`9d0d24d@L|Rm+l0iRIXN&5sPc9)(FUahYyRY4{!w$OZaZLiWzwc5fOBZhklx%R>o?3exS z%7y#%w@e_a?TNjE21E6r4~^ASka2f{Lljt8_G{0Suzs+y>Vz+ZeICqcn=RDtLhz~w zsZH9`<;`c9howa9Jq>29pTNfoJK@aQoFxlnzu_Yd@wedixMIji?ERX#&#`;?_PnQ| zbU!hT!?A;$fa9P@0S|H-Z7J>tDprg}usuuuIGLbROe>93{EAq{d&`QhlGwWOPIRt7 zu(7zP*ACl5vPnu%qA84d?Phf12LN~2)W(Em>skbS9BMcl$C;bY{ANSD7ZV9DXI2U_ z=uE%MZITZxF}@W4#@!+YjcQP)pbPlUaJ7HqYbhv*ewrN-MQF&3kA>0YW);nc7t2wK z3PmlwJ#o711MQIW(wPTWjY=v_k?4Cz7I~xl8*{eqb+zNAP|_&~`<}?Vee#0u#=(K+ zsW1XrMu@A=kmmbA#wjVCB@C-6B;%zhMm9a&j6p+*8ZD;_omJn61MUGBiE)#`PnG{c z&4L#@;j*x|8E`c=k0RlgLOJS3#pVS>{=D2EixixN>vL0DXY^D6^|;lVJ1>NuTkQ3g#CN))mF50uiF^-!ej%&!23 z$L*LV1A4PGG@0x3bx}}UUG7eC4-G4f)Uh|Ka~JX5A}M@r6-1+RB%hvJ^)gJQ2G;V& z>k!C}V&FuG0E#>6Ufn9zM*QoeB)!>a*fLvsuRHvtV6ved270=g5GDG!B((5Rd=0_O z>zf6#iNc*EAeNG4%RHVcU~^oQbNv3_SLdWOe`O4F=$vK)4YJFwifdYK+<|rrv0!M6 z!4;6g@8?s(pE^q?1huXpUi1}V0>j0A6VWfu+mb}&5C0oKQrHwBAs+;}mw)!KI`-JI z02O~_Vd2D)+wZ6l5e*OMjV80}Zv1WkycArF{pSyMohVZSQ*svQ>f3 zBwJ*iEVAe|PUMq{%=)kw29?UA2-hiZ)z_e%d}H!yAyV>i4Z_tUVeM0T!Ko&n`6K{5 zX_>;{ccoy?VqAZ6nfH2P5HeC%O?_6Dow+m4dENwNLSV%(g@}T~z`&Xqs(Jt>n7KtUXJ- z!bvilZWva6ixYhs1Yxwvb|p_m?$+K|&%7pa>byJ>t*SX}GV!k%_$Ap!l@*sSona(! z&$v$FeX3T7Spz$$B+a2+$lmi^*h5hF)|6L=XBOG=sgC7x=~cn?ka{A2j_-y9b}XDJ z{D|8TrEQFQNhwi~9K1FDN7!Gx&nQ5us?0?D$a1Yp5QlUc{w~QEDW54`0Y}gwH%T}} z5$d%AUWtZm)SBK>N0l~`jD4HYq5~fl9DS#cY6ZbB6(^EZt(F2xZ(kz20HwB3&A)h- zB>MLq^js)MR{&<&dT|C%&}6{6gdTl%4KpPsR^u^mH^4UTL6Ph^!Gs*1p%G?GDkc}q zQ#Nq)3dYWkvNAHx7^J%ENUR#L@?lU=^L*Smobnh)+aLmlt9OB^oxpGo?>LkiHsC%t zCVMbfoX|h4dCU{KXC)UipH*;gK!5c9CF8mZkie~DGB=`@TlSd~-k?rt;}(e^^-ES*<9Raz zQLN*x`RYD&qU3K3cv0%gxmSIL=`U~e4K{Zhp~AYYr>T@jP^g>!<_J}B2PBA3Br1Pj zhyk#z$BTfiA7N*@XtHhefEkMHEI5V<^vxqVpcC+~wnOl^+o~pV3`pYdtAw7vk^^8K zgdNx>ijjER`Qhe*r5RD}QRQLPh)J@p_o;f11`|vYfSL|G28FC|q-w7lW4ufAAvlvg zu0)4A-3p26-Hnw!z&AZ^AzCqEeNgB|2#+AuuuvXUv_SqU9Z-r*pCeO&h92B~zm?Ov zGiO-b1Uo7Fzu*(Q!YE%#(4D5|H9ber?%V&x*eIiF=N;fF=%v7DT=?zIs>mT$eZeBo z3ZO_}o&_^;VAl^yp$ev*OD*>R;hL$Z*Nh_B*@lV6|3fO{Ow)*w@xEhWful7*^D44Y z0_IWrJSB2nL(A9!t$=2DO^-Y&WT+%ynO1u3y6*>i=;Mei?9>Na``TAdHdxD_ zo97}(Gkk7{-reyWtk0Nx0#oV3o&lMt>LjL3w%2L=~$qas=bLg)l?4!r1mg5RR5f@)*jX z+8mEuaneBN3fkW&qOrSh($n=D?D%4+?jZ^%0?v(z9WR9=P82rarqjcMb-}-Ru>>0Gk zFst=sP%ee6?mbJW^!q`O+$vLxH6;Q3_=pH5+B?LN3--Bpty&2W5EC&kCLqFBagiDk z7L*yC(IE@5LKyiw0Soq4T97yx^GAc_GryS1tB+ef-q67!(^wX|oLHZiV0(FYi1(a_ z4v3w5hPIY7_@?e0d0g(@KnM7o17L1-E$3#Ul~+HGR^Z8%ai6IY%XonGsdevKQ`D#q zdq_y1BIGs~@$j9dZvk~VipKG4(9JD#6a&7E=+B`)?*XK+rUeFd0Pgg+6YSiBgsIqS zbX+)YwAh{g#(Yg}Vo#?_fE>!-mKL(=qw`95U^3$s3EnV@1;Frz*-*Y0{h> z8xQLpcGD|2`dfr%uwF-=jU$=OFSM|JDSmRc>B5z(!#tDL*0ET`UNEMa0`7L<Uh?SjDh9o;yy0Fl5FGVk;__=p$a>!VR0JW z82uCRl#Ll5jv$xq_UGs^^o_%mm|WE&Qt7jwen>wA?Or5VnHhPx7!~fi_ZN=*XI@p` z)Kj7!$9z!65xX&xO=qd-iuQxtJoP*cQrW#Xg8JSvk;F)j25Rp4uVHrU3FMwhzaga7 zam^LB$`UU3FAvY{MpGH?-n@ly@!d!jd9zbXsjgO@E7CP3x!5@0w--_{2+BIO6|y2x zw!u)5MGfKVY|1#o^8E>)y)ky~wfRnYSJzwh>ZJuu=$(U+PbG+k8l3mCGv`8Qmhps7(OxJ+QUZ-)4-pA3gN5Jk_?X14 zmR#op1ruHM;l57JEs^>D1x)_zY8g|X~KM(@hICZZ z7y6T1+I`z!l&C)_4X~N&kD5H{+0aGO9*A46NaCYV`{e2?Lla%F=qnO+ zXhgPuXi-ys?CykhQ_q|aWw!R`*|p1tYr=(hY%zl0>jt6SM6F(*ica^0>o5Ffk2pyA zdJmqAHwYWpx~tqX^?*lu?}HsRW_ekqO}Xp}- zsXe?zro;L%hr}Bq61*Lp)}H)iHq(HDb)eh~TvWQu@3aVqcx-JQcMP}27X+Z+sG{QJ zbYGPZ!wVv%2w<44?*IGevH#wzM(J2pF6&;%Y7nnYf{NAk#As}@ZRkwU+pNj1>f~wM zz_$HRAx*91iDoKFgG@|O&~^YqG79OPuHN#ymEp7jDU;j%SgkH$RF1fT;UDRKn$vWW zMayBzh2ybo^`81IzQAKF)SQUxa3u&I-$zl@1)z3Llk4w*{Iodn{O5vnlWHa8e=Wtf z{4j9K!)N0Tgyu(q&;gk&dBE7MfqJNB_>K%G=%}F&+TyV+yYmZX3q%EwfhXpz8p!25 z@oVLGugWuMx4qKqGaG-jtLj`TUuVqS4EbjTzvuv%OMm3%HQj)+?&7$)jZ63awF;G= zPPvP^l%M6@N~QL39XvQ_eg=C!hr9-P()8xfltNd18fd99Uo~BCG`hno9zp%+tKNYC zUq!fLD8SIE>$=;bPIKI8fd_hWN{z8N&H&@;bjW60svy|oK=*3uzV(;x@hkl1T}Hzx zRPX*C{ic5~bA+uliFd?wG9d=<5cI9D9rYHs=f8fF3yDdP`VxQFjq8QwpuwZ$*TS@t_HbV}j8PL{rXbN$lyo%DaWNoz8g45-YE+1x2^j z%%0yK&F(oA7`U%j;MUG~O(1Rl00~SVqCeSyHip$fZ{L%QZnKe>xShkZQsNF!&W39_to|?SKt0D*l0~tPpsm+ zK2W#Q(Y-UQ+1_Ujjy--;YbeJu;pH!YV7aw7#XE7>u*RL~<(PBeX4w*h*Y)aDyPG%w zL&A~~Df27iNygDvH2h5xK{M{3=y-H7o2K$6YRZx!yR=1Q#S1tZjJ*0YCRntL?vDo_ zkT#4sM#Xv=iu8qS?7w&R#K%8^y~(U==W=4t zBz93>4u1$@_4*gRP*A2r6mGDr(lN2vso%U<=~sfos;bJV*8`{yVHrS_>i9DQtgPjj z5XKfskaw7iUx$vqWEc4gyY!FE|z| z`rrFPQYtrlGUbiIyMA^XDk$*a4Ef-jwky#_`BjPOB~|~pGI$Mi61@QLv%U9nz+5FXph^|!HO5A{kc~Lfu1gD4v&cH=_1<+tuZC+r;k5ec_Z~;<-{ft7iPf5 zM=FU96Qy#f>4Toa;WoH;tRfPWausm&NeuIQ5WW7N-aoo#$-Hpe^<3aS)0EjEF3gP_ zNPfqSxBGb!JOwp10#*~pvMXlzYb(pX$@T^6F2sPiAa~;j73GXDi-2`zaUmIdSIf~M zQyk0%>JQQOjBWjB(<2OL`E}Ve$cHe~&L%}nVn2E9lQ$=t@q-|5e}hCia%qAUnbr z7Q5Wwd@ygscDvu`w?Dw<$^;A5nTPd@{5K$8 zuFp73HzXUp*XCcXXK48Ofj_KpV`shr3^RQlGfEfpY!{Qbt$!$IZ*@uRkz_`n#vECE zF%q%0h}hs)hlQC-W0+7V_-wLqgA-0mSbUQ>@z@sw?1*Dn^OrE|uMASQaRuwqD8yL@ z3&;h(57(rftRCT9m2=#W=3hpJ7)O+DfxLYW`+gPX#H6ia=plEqEdl`4CvqQjkk5!j zzJu+I#?|;+_f}*aG@L4>ds~19OQhKjc6cx-6_*Q05+5IdwX2|+G72nL zD>Q24FZxL0CGFH08Ga~5^4S-hW?e zAT!1k4fBn|d^llIF$a5%?;ITrbea>_giI<3k*%R0Dp@Ms2~x8*MBzCtZ_$%UCk;fb z%&Nbjh$^~2L5Cb}hF3Lr#VpAppsD-NnR=jkm#rFS4&0Xn^egNT>9}UlpRt zq}u)rAZWF4NT#I`AXNj)EcP(Gl3DX|4iH=C=f~~_^FcgKCb1q7Ja*6JC{RnALvnkF`>Kw9Lhx!* z_7yATb|Z0+MoqG(PIrH5a8shIqDc7(^)R$9e{S-^E1=a=TttJp!ain}z2KC~iiBc?IFF&i%*Z|HmjGtyB9NvNb$C1R7(E15w~dnHF}ptHuZ_ zDQ~T|D55AWVMr&P*p%#`BQQEs{GmYlixolBbIwxq3tSn)+%S3O_T*k=iLj(wly30r z`Prp~h@S+vOMH^nz!t_ZKN>Q!xxiV~^c%FW;0)M+Lb~0+n_cW* zF{se%x4Up_)u=c+%-zn8^?RNgoO=0))tDfnql&K=W#nD^>LmexzHPYd$ESFr-;Yp? z(2b=Fv-XN?mI^U`A%_aMd4qbA!SP%9wNK^9sbH5U4nQ5HtmEQ4Nj|upYE!w92GO@b zEGbt5>IvrrA0}_>#L5}p*mRZhq^qOCqOg`Yyo9XdMFxOrBT|Ps@LWZ`x^N$nQFm_3 zdo7qF@oGu0>UD275(;2>ryh4hpe36WShO0Ctmv9=GxZ6Yz!>le1qjZrzK0e`H7&%a zH7NI@qAA@(ma=I;*rt@YW{pP&(&3$G4*HcKHo{rHo96@3qCB~uc4k4eZo^__&}IZ@@R@dhAPpo2pjkQYF>_AheKWGGPM1T^OLiGOjI7(buq z+B1{Z&|ikIlHXUI48Cp{Ce4nHroj7BAim=OYOu`eXwB*3P>UYuAR%oLFNLWexmoxDjUn~`Uq+tM}Kht9^E%7EF_12PTfa%X^LW;Q% zfBMiYrHQx>kv(YSbMw)7tDcI1qJF#xU8rZ`2@g2!l;eKx7dE^5u!m0GcOSv{Qu=AL z?+M#!P07#2YIP}W_3+n(Ev>SEh(bzcV}8sG>@}jPBjkWs=H-x`IR+jRYiS2;`*_qC zQYOwDpm}blDKZ~O73u)X7>Gxr(|pwV?iPsXq!-rp1Ns0ExaHD2PVSiFtMv%`Rgh<- zmqiZ%J0tk7B)|>d_SRz;0N95DlNe&?MSAQREU4k{$N(kyxVxV7y~QKw+BUUp@Co?& z$Y7qfrhCyEQQkS*-PSh%rw!jE6oQn4H;&RF)w(Hb2E*poY!|++@$pUZB)zXp*TALY z805kj4pstJ$~5@o*L&%V3Y_*%tV}%Bn%!jbD8@cQ#RHEsLVS8OteOA;2tiuGn?}#c zI5%UGCx`XoI>}?F?w4XMs13s{B;jr)cOLwuX5OhkDXS`+3@}IMdW%@&qW3vVAM_)L zsa(&#x)8tY6LIQC!hShQf~ZIO4yMX z#H8qNCtQwl+%h^~EI9=;C=23B%ab($@_*%t6Mlt(DF28gh}YeeYVe8?QV{Z-8XBYV zWpg9B@_!*_y_E$~ZnnxocUu`yxkKqcuR_zSUb>0Ee0aEwX#OQr^NriWy4K0f8FB4w zKypUc=#0=j5PmglUnNl9 zHl?R5#iQw!uBSNhlBNUQg2zi^i1)$iP6Y`dT)l$8L>aQ-FnUb;ty_aK`#ajSJ2WYZ zIiPH$Oy}EKnV$=sFGPUIqLDQDEj~(+w`_iral+DVdb1*vo~JQGt%acOwW=3VsWNkv zWz?UwtJeYoKwHnqx-|MjTpK3*)W&Thf&65&6g)4e46Ie4?0O}kZ#$R~A=qHNGoO(Y zV^ZDV+5Z%k;*-pemTi8;6wx1cpYWv}ItM7*=G)G1 zGKV*JuZ$@oFQI`hieEgp%IfIa%M~+a3AD?&BRL}BshcZbx__)DbHd|cP?$p(91J%5 z`64KM9_(;fE_tGAk3lJ`Ok||j9;6bY(xp2Oas<&O=|X|=u2t(Z9yIT#4-u<8%GokL<4Vdn3|`cC#&^&PrYjr2|jma==CG%=3n z6@wPLIp*t2trQM9S!kgrYm?LnId55{fp!B>aLZ7_6g&6&)L^Yg8uMAQs<6eUuhxu{ zhGd-~=U&vEMUP7!zFG_{^gEMV*gCk_Hbw7K2${UQ{mAPY*F#T@4Zko@f~?;z)@mfa zJ~;+T=ZSKV)%o&cEjm-5L;S3H#k$Ch4=)X#@P8_JMHTq@=lEfGi8{*Zo{{ls&RYn z*Opn{-wkV81X}TMTBBLinBmT6*BK`r06Eq&Jr8MHI*U7~WAb5jRdjuEMLqr<#+nK_ z>fG=SH14hiGMue?K~Mm$LiAgIUD9tcPKx}pXnS_>d1&sj^B*BY( zHhCls)XqP43EXHv56olCT{^@5yl762Wx<%DgHI<0)iNujeoMDk>RN}a zPDPQy74o%&rQYIbDcSNS8ew%^=dL2%SLVLwW0}qmS4#dX$n?=S4p^$5ahtq z=O7AIZQMx?Bk8xShD7mg-=hPv;@G%Tl6O+-&Xl3c(+}BdFQ^`?Vs~Itk&uM&v*)YB zm7JD4dscTnIFjDQHR|>uWoaixcfGW!!!7*eQ=k0^BEJRMN2xulhYnCS)CY^fOlZ%< zqJN{4a(1T}`hf`Cv@_3&uiYtXyynVgF)Ib36X(5)CnnhJrqqR<%G^54FS9de5ukG{ z8a@O5-xV8skOujMxYZO*Hww8uPmL1NdJ%F zZ@1@hpwKw@>DlcwmvHA4dV6euoM#xq4OGnc3vmZu(X8|}J3)_Nc>bs8-SQUhC-N6h z&Em^GI$@teA&m1-+>QpY-kpsx?H8v%p-_yXhO-jjlBNWu`B_6KV$f~K$ME7Vn3pG; z@4k!y|4lyF*lb}Z(1E*IZ@|kq6emR2yA;9SibK1e54_ZXQG9J73La4iCnn@~9azXg z+d}W$kpG!{`S&vIr$3^v1(KMz);#|A3*%d%9)&QKcfZVHh+MrXWxCt=D1A32A^onh z>xtyRzd3pWZ(W$A%;5BhVK_1YpdWCVw_M<-?Jq`{Q%R3saK900H4|baO6hDTn@$lq z|FL5{+aXOAp_3E#?q`yrBJvaR9G-~Wc?f|}ck3AYkl8Ji_P;?AZkke3N}qwChhg)| zA_!N!Z7UU-djvc}!8U8gfzNZe5uvR!>qArHKZ-M-gX*;1P2KpJz=9-Y?gW`#?tA}t z9?NhH@T7AzzM{3F05zk8l%?D5+ug=^MxM@C&59i?7*2ic_st zQr^&ljdbwAjx+dIaI!`^$$RbV-{UdW>)Q%wrUyXY-Uc)7dE>QXb!gmgm3GhcHvD=_ zz4=P72N|-s4+AssCeUkqIzp97o0-zmZ~OD>Y*=Ez`-V;3zPbW5V=4cDc1F@=wTCjo z{61M33gkdVW4|o5p4AN1Oxaz@Tw?S)^d=L|Z+xdiE}I?j5`m({L+u{}>9dE0L&DG2 z-$nsTh`zJW7Ba)sikQHIdz*okw%i>zF1~-$$?%WE673ytT`I!5GfnVJ~VSh;)SQ_pKAel%& z6q$GhzIK%-&j`;{JqJDv3jxL@xw}U}b=9XFz%6Q zf5z6U=1HaKIuhO+F`RZj2a8U10@FVEM#upX@kAv9JvgQyU?rcj^ec4SxJhba>gM-< zxMYyAbz7p@wos%iN!&rHr5*3dp5L1#J*AO^BbUCj1aw_i_m)N^C~5_*24e&rfAc){ z`9=WnAcxugz_uW00a=RRB9x zzT+eyCrx)0kNt3^UI3Fufb7K8jeruZE$A`qa3{Rfi2jwI+FTx}fxty~gUhG=K!xRP zxkqxZt9db^KLB#gc`Zxj5Hf9Ydgm=wz|g(U+-$TUxOur$E6@dAUrStjiJr!lLQ@aV z8dmR04g?Ls!dPD|tmO!!7xcOccwD2&T`#lv2!x4MY7A2@+_z;j`L;G)KaK$Lsh9I!)vYt;MPC-@A_L3vgH!zQ~ zwtzq;9Kw=~KZHN}*UH)+y0A}~nLKk5-idx6%9Q@)M&|&|5HF zz%+ky5f#BuqB)rX=KhYtX}rJtFoBZ~l}w-zTTr?2MFBq{q5By7J)R|S)At+KDfRyv z`V9NP;fxeu)ADF~)osG#l|N$sX*m`i2gjlvcn(Rz(=7FnRrpkwBuK(Q?5?Vij29YK zLU6Tb+=BtEJM?CVS2~ew%B=fPp|RE?U>bJOjCmdt-NntJnerCF@Y?#NlsoATFJ>|b)rye`szl>~2ZZ~B$&!KN(N7ELrNt{Vv(ReJbgZ)WFCZ*%vSZ&}I;B?LpCPO6vHvWvB)!YOfJ59@D!L6e(KNbkpA@qh?I|{E7%K4(kOsKLf1#OEiU?;%y8=0Pa7uqtoNx4Pw_wPLC+aF66sF zUVes?=Ju`RuWr_N=M|HUcF(3{x49Xf-PNzlD zISX1S?vbQ%%TqSUa46L2%g423xZ$pvGJ0mfI6-%>`FhkaU1loIcbOoDH$?n<-RsOg zJl40gmk3FbF{XE&+7wROV$x^l>B#MfS$GhZ;VDxlzq>V?>G{OU6X4RD46 zS*>yww}1c5{PVp7wZ7n_>~$Wn)=v=f%p3th_4P^3b)f9XI7%~amqU-~CsQoKLl!?; zL9Q+9xV4+{?I+u$;XNqBpAuXffsUrr>uN4-9Dl~9w}<%#iuWds+wgQ}ZWr@4W;0Xk zx^xG|2-=hfzAvRY=K1SXnBjo0%T9I)OWlIoProThu(F@T-NbQ7>=Qh*(8nxh8gwwX zbMIyxqI{P<=2q35@t(BF-zP7%rPyGBGN$U(rIpTUl8a*qVkz@A>8~Apq%d4>trnUW zH|D?&I%f>LF=xpxBaWRgYnzs5h|i}9#FC(+QZ5U+nku7x7cXHS_4rBTzn8Q%Hhf9N-p0GO@vwD69C9o}$9G$Xp0_s<_<5 z_E`gMq(*;@NykCNn4x;J9a#&?T)WH2 zO2?i7%&b>;p@2!vLKX%YIL51HnXzq%G8!+7!vkV` zQT3ZX#gDhEph5w5ImL}#bu4#Zk21r^5N)?$RBG(VfAO+6ey)ymX%IBVafM|+;A_L0BRosi(>ZtN{SWKaPbFS*81>=p- z20vqA%hJ8YPrHkL@(I81V&fy&ezadmM;w<>(vY>WN6tf##D zYp(`loSr3Rjpz}D0j|3 z1}f=RYA9xy4JNr}$*)af&f^ovy)ko!5m#LvCe(FLCECa?+%E#!ggacmJ-*$J(8b^` zFnZN!+-KXHGXTk$`SA+TE#MD7c)58!czsUuGH=*nSOUmZ$tvP`Am5W!YqT(ck8A?{-aDl zsp$6FjhPn5AWeJ^P=4-H_mJyN(t@h`fPKY$%>UPjQj)y)=6#+I#9NflspoCbjkzol zrQ%#t*Mm4}_eYsHPF6}JW*hlvC3B@HY^5~H0s`kq45yXLKJa5vM0}p_48I3M!%ehf zoufSQ8j=5dSqz4Kyv`1aXS6!i)PCPJsG@bk;g8ja99rn+5MUo*M>6b$IVaw)R;t5@ zFyW=X)A;g=m&khu+@uKQH4u#*wZJuG%Vta4xwO?j0`gY+ zO3V2?pJmT1%7;P)BW@$ThJx}}G~dcj!|LoTbyE)WJxV*HAIPyUa(IC$uOgVeY)>1Z zZhWRPAWJEZ&!b){GTS}?_{YTSb#98@Q6SFRM)Kwj{Gl$66FV_<5?#{{p(mB*xui|F z?E&JN9zsLzj0v2`=_T~#?yXP3G%H%$T}O135od43uh4T# zXjBTpc-wLRAe)|L=jW*8@2uRRhip$1!Ru4F%`84;|19l2Y9dZkLYIf6TgQ7R%qLU? z=4o;9;sf?-Erb{=ZrcH)Qbg&V`^ror5n@c<&xb~3`S)e+g^W;o1j~{V9Qww2hxg9w ziU4_`gUOj#xpN~1FS{K5jyZQtS{oyQO@9G?*hnc30?)uAo0L1ZIs=u1074Y7Bw_7| zv`$u}8U=Q^{@aeCc)noRB|>{W+|>iN0|_1I+_dduQ6r{;N*;Tl8%U7_?oz5!a9aav zD2st}k2xV&U$)8_?!zb_UHw=!D1MKMe%&~Ls%+pL!86J4W>g)lrSo7?XL7zu||r?m{R31~EJ~C9MuO zzxx{2Ns-I*L(=->0vi>N2DF?mmo;~MQJaWd|7z3aI|UElr>tle?~rr@X%MNRuYBzX zQr}iz7V*K9dN7jblNoG8P2A~fBin^jNeanFPHMcSWczYPE3%U}&Uj@1q(l1)J0@$7 z7?=b?$@hljdcW5fUixjb9CB$TGv3IooYsGLcd4(psua7|M&!fh*5!6WcP2NBMqx`&4wWjDbJ5BQC*dA=Uh@%ZMcif$oWwp8H*)AOu6)S38MT zv~F?jL_@Yt84D#I*mowh;~1G>wiorRe-(Gl0#xRjtfXVS=gFyW^i)K9{RmG49R)D- z3D=~!DcrOsac`jDdnHjLXmQ<17#?GNgNX50tp}(YQTJLu%HDFy6y9Ii52hW5wh55) z0#hZS8;DZq?(Ux`C(D?NBof^YLEw)ZlQ?vjrg2&K{RMs$XW>ZS0mB2ME|4q1b0SA1 zgH@km0bwhCyhRYGqn*K}nE7NgtzD*oZk+t7#@OXK1+9^h`aIY>X}o>3pxdDK(6AKd zhLdbeSva^q)>b1AmV+zp+Cn8wV=KA5+t*cGrL{duIS>voSoyseyhs*v~gaW|# zqUEe@8gIKSr3j@PZzhH=H0&HDElv7dmnBp532G2&&4>uFqhjf)$NH;5%}>Wuw zp1T~pQfVq=x0D-Uy?^5|!Gj$oSG^Z7X)TH$!ea++BWaDkaALys9 z`V7<%5EQ4Ye^;gMQ|nh)q+5>#ogdsi-4Owsz}33VFEK38?sx@*0{9y?>M;_duaB_c z=j3LLVI9k>&nin71Q7T1uPHL1J^=tV>72{besO5*xZYIn&XRe6gobIaiZoheBy)m} z9pMa?F%q_8^MEWc-rT;A{~{&>w9Vq#ml|xpou2HU!fE@|;CL7gc>jnJ_&@f2F3b`R zB#C*=6N3O2M<_9oX~oQmV=b0eMCB&oBpqv?<(k5^gr6Z7si*76nB@zvJDu1SF_p56 z$?#J8<}ARN_S=@c-A8&X8{xS$hXGrFQEHRmP&N$<3LS~H*;dcsIVm&{QF{_90>HqL zDZ$PRZKv^^#iKZK`u+IP3AJ!#(d|I{Rn+!{0Ck+`PtP^&Kig-78&JqRmg+`-FC4Rc zH5|vSO!b-Ayv~)m|H6U9NN^?jmP&d{i+CTVH%IU)5U?xz(0B*Bw5xZHD%C#oo?|(4 zO@8UJ6lI!=$*gc|jE>kyYEpz#1TlCs(^Y$6ASh(#njxO)h=YS3w`w%&y^3$)nVi8z zHRm>`3J(Rm(5sox+M`TDV>{p6{GKi2q}BV%$=G~t%4kOT5J81}s0PAQ?Kax!v2$#Z zqS6>9jIY29>}ir+;CyMH@iWKU!2jZ7%&M7__}K&6>+1m8AtGjr*11*mFn}To#dVu& zEj87YM&b1YtbPtEqJg+W)mka39*JxSD*W6d9x#o(48_+wK?nU~BvoVX(NFqd3mkpi zh?v@|5J7aygu4P8VoXnd#m00^8>A`{S7o=P(9#)bmaIQxP1io?=`^BAl4|{l*=S2t zq0nDu^RRNBK9$8z4`7GhxdCkL6Gqc@YJJ7C`^oP9GG$@JL9nUOlhvcYAX(#=lXtXkE>e(Ctqm|d!vtI&wyfvJu2K1sv!hP2MQLNsk zOAu&W&OV%LU@-C~lZM4i5UX$2Qdeud9?F>n@}1E~>E9{}Alv6MR*R}lxC)jCIVJ80 zcrHVgta$=erlJ)o_hPK%ovdnl_I>bNAe435Q<-DOfp2K+l3OZ-1H$JIU^iMrfh(8~ zGIG6nDc&^Z<3*U#7=MP#TS~-raOI1LkW|lH-(4Lq+)kzA?-zc4pM|2km9k5U)BKZB znxX3x^v;G&8v1$Uv>V?+`zFPeIFx#-C61ckSX@33w9nLv{m0qs-hO4nPlj5l>wGY; zUV_Z%>^Q<(Sv#j%vx4Ly;A(fe&W>f0Vk(oYM06`4qsIV!^a+-V9gkvLnz@2IWMdPMOzn(X z@qwol-Y^QoR-lN4WN^xOW1EU#P38mzG^k7c=!r>@0tjf(>I8be@u>|+*Ktc^d_SIV zi|R;zguP3t%>nN{OH372JkTVoXWIICz?`JEyBVSSZRDp8vS711+(G{adG?cwNo;5H zvy0CUExU$$(MnBh+x3w>zSZnQLgD!kjuN~o76;o}CT(eH(oSltZB+_fvV{UWh_G`O zBAzl^JTpCx&&(YRpHUAmgQ<}5(>VH{jTO3>u(WG&FPk6HSKUhl$*KXuAPLUkj&uUr zD)(>#T01wir=og(=Dh8qzGkLf&w5pN*ywJDYd z31aZ6q*tHkwX&3sbw5h1{mhDLts>A>rQ{gAYQY|{ZdDkfK=V;Mg8V+i+O|8k=vw&P z;u5NVA}|u-*mI$rT95c8eI$05hM~8gC-h2pk${3rp};&HF5u86wP%aK6XIRT#xS*p zBFL#&ks$NO1S3D*SukEyxy9=JW1QL~ewZct^$S9R7BCe(ne7H)pa~ggz#bPW?aL#H z0KW@ZiWv4EcsA)A4JrbjbJs9tB26Y6FRtO94-VO`$pD!~uCL-7=Ef}h6^*}| zfAjwxw7&+Uyldy(Ess>0>@+MlCZ9&OLfwQ);kTJT=eezEul$7@+4&zMCcSo zPP0tR(VhiWAcmfu<%SkB^IYYwj^C)|X|!~dp&+uq*j~`3LVneGPwAhnmHDGxhtq9b zc1r(HS>{%Ss3(!ProEd*M$nK{xlN+krEP#L)8*`$7W5a`7=9>)sD??1>>(nY3~9Vc zs8=YLAJ+S}QLf5|nncjF4LPS79`JQ4r0W2#(K&p6pC~m{fyL!ohwLm;VR@e8l7~p7 z)UB8i4=M3_zPo2_ThVJs+F(cPQnc{?_!Z5o2jNzAI4oEpd(6IZg6&VcfAop;KHkT0 zjeP82b{TS+R14yvMOlGxS19jokxh_SG-d!iG2KAljj>fPb*1zI=%3d4w~Og%L^dmk zFZib50Ik%Ir0qiOW{ANmGll#+<-6Ff4=C1i^er5cVDJsTN<-RdAiG)S%FQ}x)~xr{ z0WdV7WtYBa+abN3X#ngb7|s$jy_<`}C_Vz++vuK2%F^B>w@VNk2H-T#W_neTYoQYt zuiC^Ke+fC#LHPX-m2AG}ET}1_D-dr3U0szp47IHgH2|YzhPa$)*2LAl#`KOyZFhf) z6Xk1?SN(QJmy_b$TBr7}x6zx!;w6}KF(}`8FV~@(wdG>6#mn=OuJls#Al%D%(a8zm zjs$KACr+M(v&Y<;wtv<}STRnxTQG?Miep(6p7EfW;ApPMnu*9X9X|7gS$rYs&(9NR zxKa2`PUGiyK~@UCmNp-f)kvhlT$$(HngMMHW+}`mIS6qkxUDUi8yKw25ZoADcPLeZ$fQjEi0X*pM@5@izv>h+|ygD)1H zpdn_X<@n~PM8^NC3N)GLBZnP9pP`jzSQ(iU-50Xz9^Stm1Yg&*up$Dry4~wgT<5Dt zUi7aE2-|-zHuJ&2Z`0krFlR0I;|lna0NZM!>e%JktD3}u_qT}q%*8VB#oz+N-nL%cy7{K&@y#VGU4-|u*XS@h(}yo`BNrNhy2a<(8w2_0x-U#rYuA}i=K zr&1c@=YZX#QBMGT0I+n{P6dPoi?_Y9yIDs8H1s~sE~*IaGy?>es;x!+cMN;}m1aQuGn<#WQccU`!6rK96gLq%6p zRljEJlA<&3;NFX8N_x)M-c&SI7YOeR+)}9qN7SnCCZ68vQD3UBP9jE_60l58s~XYN|Y2Q+oH#ePu^;&xQKOj~@5-+-j?lo|fj`zIElM zqOq&Fs<-p3MtQTjvaqeD_^!HV_(tcojtiI0o$YL>JXc-x{ddnUoGu<67{1(C`FMQn zO3Uf`vizR&bvJu%s;^$XJu+5PlzsE+_2&=oU1_emaj`+A>{WNRe)nYZYWJ;MibhpO zb9-%BU1?rtL&d|9{(jYf`cmtQ$(Pg5r_R?%FPtgsYLeZ&*l@M=)RmT#l|{$zYqj?V zRS$2EZ17<{y|1;mB7gsM>PSj_XJh4^@#kI5vKCq4gm!3b{K@q9)A=IN^)`89d4Xo+ z!P)AfvyDwxT57smYG$ThT|0Z`a%084hm-M9krQLLUwr>!9i4t4e*6Ai$uA~nPUPm- zm*(BMJMrkz^GIIEm6qxkPoE5p+`n<5L2w}BN^^BbU3shQ`1ttq-Er~fTiULjJ0m)L z#NuMuHV!= zo1C4Uo}T`GQmwrH!|dDnckkZ3e)Z<%3+sRD1^b3=WV3j_ZZ0$k2mk=}J-3PnKxDBz^~HLzGQ0P^1e3Q6KBCEl4MIH}HXxdi&a{7cdJwcGwE-Ge$Z32510AU@CZo4R&S#>_`&E+8>$nCN2JOh zE3CFANgc_L76P()42-M$cY8~MJq@iBks`L8m!7R%+6+=tqTfXj0ky4>Cpe8q!@G^* zm;w{TN}^a|1L9@>92|XI4tp*?&cxjrdGzAolzFxb?)I&o-H7!z9-jo;tf0S1#@6iS z0Rkh~0pHL_s}vE~b@T$Fl(9hn)y`Woild!9}8ENekjqi+0_X! zc35LJ77>ZLX{x>eeLVM7S zwV$ibP z-PYMwDF)(g-k7p>B1c64i5J2{$CIsOmC*Z%739wiu zU|pFk;t&HMBnim`o#XLb5fY-GUgbPZ86gAF|+7bqb{oIviS zWIJl?jQBiob5FQC516==h;kdfCU`x7janCtLH@mb|N6fc%a_poqDT}E{e+xLO^~xC zdge}_0Il{vY{K=Q zp1g9}H!646=qBojK1FqCZ3(#QNp2=HthT50;+iUG^0u4bx-Go1?mTGwt4lbT zlhS>+_t@Ych-08hpJSoVng_y9%E{F!Fk&vk*c8b_CZ#J=&tUYMr0m3=UsJy~&dgN! zVh(-?@pfC1KjpjYaK>~{yy?4kia9&_;2Gw!+X@g+@r;4D)N7}SI3Um$7+}EZ?==fU z5_B~yqJlpmNy=DxuTal?_E6aQSV3IB)!I&B4#YBs43c}+jZK-@Z=-~y2$Mk7C6Fak zWVg~j320Y^GH~K$G1z%!rFL_j(W`nskvRiSd%gJr@yG$RbZY8^-f~OF55#Qmw{|w1 zhixDwf-76S+Keyf#TB3|xvzjX8%s=juJ5KMf`z(L-Am73)uX4sz0zxoN!i;lxlfT2 z*X}y&U4^^`N*iT;n8_Td7MjxdNK2T}8E-n*n0D?ER-~{0IN3awbNMN}-28T?*BfgB zp6vK$GuZ)UPB%>7_2v&L#w@7+Fx~OPIn^mL*u!l6qs29-kFi>E=&ChO$T9ULWo^H> z^l{ZGL^P9sEAm2^jo0k!yvyvjvgIBCZ{X-C)2`gyjidx0`?f7YDa>r3gjv@df{4T1 zCSw8-inv$e?A2rGzF8T4>T(I#c#`a0+Sj1JYmbtk)zOH9Bs;qr+KwS)DjHGayQl9$bQ9Ck+2Msc+jrYI zKy!UNdMAJoP5Vvpz)wfx0gpNk5foYPb6Q|Mu`8{NC2N&QwtkwD63qOA{!XTUGuSR& ztL<(MFs=TE7LykOL`<7?kyh=LqPDdF-dtnAFgP@Ch5b4E+-X1IYnk_@ z?#3pqyWh_o)7b~B-P(*^Ra&jPUpZ`45d37hNB1=Pn8dE60)Z&RkHppQSCqPjHH@>Y zb60#MTeS*et4M1zcOV1ZqOCcigDEzTlE~ZM=Nb4i<%2PHbG8EW$_Y8xd>m$NvjHRr zz-Z4;m+JuwWSnG^-B%>KumRN}!T)us;kzMXlj5+WU<^OTSI!2rwa`|MD^R#y-xAz% zk1g|a#umj95;Xx0epG}u&D~9DL-7GiWeYJ_@D?-I9-l5Qa2#uIh#JnDQakUq@!-Ay z&hmowO({vg^5ci71Tbe$b${X%Zb{6$&IM%y0>E#68aeCkW+Yx`UzEzo7(9u$Hp|O+ zEKbTQ`AH-D3TH9m=Q;>&IJe&pIBNT@7WD;y7OtU-JeyrUO`MC?ep-krB z#133ZQB4F=?1xlFD9@BegW2q{${<%G5cDV{;;E&+g__92N6^Z8U zgfUi#BL;HY^_{YzP5dok4I&h^=+F!C%3lw~^jRKE`8o1v;e!0Rr68VW2-;3m^`Ws7 zSEds>m4L`tG)cDs=|K45ZT_AvU^)=kzd$@?iSoFP>x|$ErjL9IPdBvkHTp#}c+mh0 z3q}zH)3#oIn0Wfk%SjtU=T7M6()YSh0pjXH1;DDyN$JSWUzvhBzL&fcsa_B3r6t+$ z6My$;;c+l$D3?DfY>O!y65JL+6c{7cs=G&?;PvaR01pAfzV*bFimE{Qlokke+Usp@ z_(n}uA#LNg^(&`s5JKvajhlxAeWx1OgSjuY__=YU(eS%mMjt4oaOl~;<|b-?)t)ko z@XCAR+ZPN$Y-~#nt)wmot+`Q0+iZXoo;MTo=_$QW9@ZY$vlU))uNgkA??k$vv)fs@oEI+a`q-7*&Gd@|$pgtzztxJugX zgv^4Q!Vb3^zA-rGw?xE6Z6*UnfGu*eI*S~lNvTccX+wV)RTzL%Trj>bMV$!;7~FGV z+%aEFGzA;U-o?4=p5vYG*b&ckU6PJGg=)118CiHR13irgwn&@rc({UCIqXumJpyXS zenmhfe@k>NYv1ca{c|EXvT)o^eOf<3IzbUGyNwC=UWFVr3GA&l8-AD4-iVAS^ro7) z)8gVKFH8!{lt)3DoIb!>gNEdZ0&0 zar55*7D-B+3^o^mVdUsYT8$;C7(A)i$cM8LwhlFgivco)Mt`JkR37$l+^c=Fij zC|ji5M~VamKx(bplg7Z|2lXW+>W=ups z#yepZJ$_mg-00A(rs<{6Q{V|FWdd?~1b$z8T!x{Q^WxF_C}mvhs=r4Z?@ zf(t^Yf-gJnOOAnh>}rcfa%%6CIjxI+iP{q_Jf$}=ZS{B$8f?3M&nmxsS{zva6nPJG z=NlJ_J|@f5h4@kQqc_gUSn&GH^8+!2HN~?YTcaTP1flD(%n~cgq%qYHezK;WoiX-) za2&E-mWqIyv($z92cGuj8eB+C%(vZFNEr-*cTWm9)S{mTES%gzvm5{=P)Ay=V0>>W z=>|_*t~Hl4&4CFioL)*;FSSmZ=;ZwcTo)|>0HW&#EI&7AFeHge4rHtbQGC_4jY>-q z9(D~Kmv3?|{sfgpGccIOdU>TPD0pRl3CD zsNR~D9C?p*N9vSF_e=~YV_B3G7g}tm9TLlX1*~{{3Pd>%L=g|I{~$*QLd_OAIU-dl z1{H<5_w`Pi+T)KV-YtE45N+h5h$#)|=Nd_{I5N3-W}u$ADzl|Q15&X}5XR~>Nx|`v zYO|%}O{}Ab8XJX5oEx#o-&6CR8|LjfR`+6F(1rMPrfaDu=HePngnXm z{?eRRNSyNj_7>fJkeW`jV;bOiVZ()c=lS&pXF-5i-zzls1#Al-*bYiq6ITyxwUhw# z?lq`l>t&p;UEAj}`X5eLkDM3#a61L51S)oRBvE!%3yT$O-eA(>hQ(g(w)t z2MIB`k=Ns!mCn1f34hQ9h~CP5Ijfjs`|SHdL$-ThBgNjzlkN!>hf`H?0s6S(YMQt> zXNx~ZXOu$k9JZJpFLUUC?QR-&*($Q+tEafxKH6kE!v^jfrpGLRuu#_BF`#v)+~)Py zC*M}8=xuMve%>bZ{iM$H@*J{-hbhtZiD4GUdY^y_9tMgd0O!J{D)ll_`9}cYCrt!&+gue$QdOhI@2h91_>TXu-SuzGZ&b z&M=4U2JjziU=0MARdk{vi}d)9D_i{ycvF++xc4QezNH`Hv2A5a`7;N?_=55aqVo@) zY#D`3FS#;F5rRyvHSkBl?1^~tFr z{^^^#8OIlw`)kIrh=&R2ZbeiMDgiMFBZYJlx=T|hMX^n(il|?xW>6zz7*2bSZ6gFC z$}*5iOg0*O6j|e=wZ@K6PfS23^7u-G*!q$|@xg)(n54UChiiFLBW>IR9=bt>q_KMW zNjTrzdh&Lad&Ji6iCFefD{Yjg812eK5!^`T+13VBkmam!58F(W6!Z&TrHJN!l}$aHko&z)IK&2dMNoZ+2Fz{mo4 z*WD_GsUM3&CVU(fvwB8I;}W(;ih#2y2JrdECHW!kPL6?jdZcBVeXtCgufV^yMQ}P+ z)4O7+OnukTh}hj`Mq?LU4Zi(oWbHkOzA1J|+<}Ord&0_4#vyI42Fsy2GrGD-DQCeU z1BT;}GbyqCmYUly*%+q~KJ@V8+c2{LIKkDIe2e#ys82O-kF}#6NnDZ-PXP%IcuYr1 zDLQXMx^#c!Y7LW)2>#pK2mndvZQlYqZ{zahV4)M*ku(uYZp?;VX4Wv$fd1@KXv5VO z1u%Gm3`?9c3T2+~x7!#BtxKceKndP%6GlT?B>-ioIvM<2f4U5H1NrH_xNeD%1;5Zu zWW}hZrcuAB+}^8A0X-1++DB&NBhmU>2WB#7eki+$3w=TWVq*VtTir2PY~=px8n8$@ zz*Z`SmN*>7$OXBKa;^@VX5>=~+SDJ2+HrZV7mqZ-65pl?fMsA%8&#KA3~}qAJRCQ% z__htKOWs&}30iKQeS#Qwt?_oSJoC5@yp6b0L0V00O1A6DIhYk?k! zQ7kQ~#f7n{jqf?-O0RFapi|Lc5DMzP#=#=@N73n6;2q%wJ&Ci#sqHFp6&)tpt2Fyb zNt6j9@TmTX7TQZ@YFj?IJ#BN`?{~mRWqZS~q&y{kQAm!V4y0~crxo%qH7`E6y; z7TVUyjR5B2FYP$?LXuB3!8`CuU^f{g4_4d~;*Q2Wb70pHF7 zd#SU*71Uou4u*I0Cv1P*BHcRvlprz*gTrB+Df_w5w}D*h$%la~>ww3n%6I2a@PBFu zt*4Df{PJ5YI*YyD6^$CbZ#Qvfb$qq4)CiF)blGo=r1El7J3@~A)5q<8?Z(r)BxT2+ zTpHxz*RCejI~bc>Vqi1=YS)pB9hOzkIVj$~1W zxbcZuD5JwZGpdqTB3a;)biM=K@x+IwiIh`slK(jDhzY|8BUtxOZE9;elS24CyJ5A- zf|uh8hAk?f5FDi6caVJNQ4JVCE&woL^b3vkRG(U`557k8wOhso;dXySb!MY*sQ}Bk zf<~`&-u_tNZ*El~;~_9#8{{v2!e3H@Z1?SGwsM9+6F(G#nQ{oix#Gr_%MHfz9825^ zmga5oakvLr|T9bZYr{JTB53hF*5%aVo{ng_O_-ymT zA2X~0XHXvDJ>%YdXffqv+kKX+pP6OjjtlAJjjhFvHW0c-Btm6We8 zS(JM4VZ956mY8?x;NFf92666jD3#N^#ti3h{ZhgS>s1{1tE6%A9>+B|C|dvG+WW;j zzD1|~TIOg}4D3n~P^Cz?v%W?~Bo9g08#=9gJO$IrtkX6J@J?XzFvJEaam+pWui-n< zDaj^pinoE%i+*r(2?-T^@~8mj19p)3Cb7w(~=w@`lsiHmOZ9_#cR}`?bu&3 z8_lII%8XrkD;aR>Z?k)?oCLZow}E;8vxjbhD`_d2KeTubc7=MN%03{MGopL zNEQ3IQ+uGG{-o(fKN2_3cSn-QWh-U#)t}J;AkVaS2Vw-QfSOymepjDfYUC9LA5z!C z6bT#9pJg^Q^!-z2ko>dETN6ulGM9NU30&w7x|=-KN~paj~meB~=zFrmw6JKz*`YSi9WRz}#|0 zcg|HL7g(A||0w5xpSP;G`uMD$Pe*OCifk4h}<3Dz2c zl|7SnBp?I7{j+`RZh76AUL5-T)vbbniK!guvZygIlqK|+vxRJJBgMlTuCw3 z&oR5e7gEN-&R_G%je8mFFS0W+g%cqo}+qI#Ginjh>>tmCRB{OGsU#=E2%y-eR$#-8DmAR3aTBdxg zKx~X7y=movux8{I{KvF?nbkwq@21J8?(T@!OIktb+Qj`wQ($v_v`hcNZLd;6pWISJ z-k9N!E2}wHlR`gRcQQx5k_r%q3@b)MHYfURwH^RP%*9)0B^||#+%?y}q`P!p?p@93`vJ@^(AY`HkN-+`#3w~MC z;z}{-0{=?-`jVmRsld8pV)W&fSsb4C`V)k0Q0P^6#gx(-+vc?87Z+xE1kUoB_YSsf zkc=yeBeldp>$yjfico+&!WE*e9Og?6E6;;kwxK6_IYQwImDk9(|MhO$2|_ z1%K<58w*Oi@g;#FvO=VR-%_swmu9K*jq|wPO4R@%Mm@9MWarW&g`@C`0g3<)gVaiJ zfn#~NsRzcSm=ngA`ldf&pJubVytv^*pbt&yMI`J-U;o;w;C7C6Vqfu#-g_s_%uI{N zegHB~d&W%T%#*~U$#lx98`Y5YD~}hGMN5X}e_H{i$sfjH`h<{o(>E8m$z{|8HX29w zOocwn1o`3ydAkIuEnwN2^YZKCFXrzxzBe8{cx(uE@$^DZtT9#YE-MV8_L2FqM;4Zf z!#y!>@|AE-2M4D&oOa(hvmsp<2g(P&4;fgaW!tgmIHQ|w8cWI`cWdRPOdlu@KzeC` z=lS5v?mR?N+}B%Pr~iLFUJ!T4Sp>Kdn)?EpcRqgPIV|^qf30c-u1JO-XlHW@AczHi znFs*IpaV%D2c9rs-BILZM`JuH0K}M3tan94QP_FBXK{ktOqXQXftP1}dQAdcqA6EX z+nbPO__RIaJN&G(!K;Q68CQR%Psp&ACSF6zxh(yHkPg)kBB6m1j-)G7R&SFkaQ+p% zND5}2i|m`$%8O|mD+jm!(l8OiEPA<8wvSl2@Nz`B+@pxFiy-3E=<4-G6{rM*#2w@g z$~JDkOeu(qzQ}m`#`wqQbi=8mKMHoAeo>;#s16B;A>$IOv)Ip9Up1;lVF{0a%>C`X z^)ut5UG1tFUiQxCs_yX}?*sI`Uf;Wv@?x95?M!*8*UO(^KsWKZLT;`X?>o-IiGj?` z!d)|RezK(^17_8F1Ozz>!^Dq5yu#eF_Bu&dEHT|i6LQRAEU%TyX-2PU836@z;-^QlM53e+*$!{i8+=v?vGCy zg$EX#j^ju4OT450Q~&xVmCINCT^IFF-LR-X|H)ta2foiPe$DjDUx8-X+=*Z7?qp)k z&gnfd=K3n^7kX~H_rtgj(z^wJkG-B||qpQYR0Tnrpa5-rcMmcVvLGP3<_ z4@7FeOQGtrvx%b5t&LY!(kGhFdS9_e zbP9#}69k|yfpJ0<T^O>#?)fWEI(swtA{Qwk&1-nugz(1(DcK#cI2YY3QEr zW7p%r0;)+ngPyqXqq@622W&FaQ&nRlQBid-$>V{wk@fh}-#^axC2)K#;xwrO+A8?W zqkQ8gdvR0hQu0NcK@SZSnrnl*|H{k-HTt6ux;d38uI#=zg_q7*CCYP~X7AMYdM-+D zuakc3FphI~P^W$F(lg_|l~Sqg(6MLty{4wZtxp8SFHJd0pw2bJoGz$-DPFQ^vN%8D z<9d_(#-Cm1LQnd4mh{W6rbd@HYfOWV2C8*}Yn6x~_---}VcAgowcY%GD*mxs^jY!4 z|BK>|k*!L7Q1t&@ac=2n#UFlFJm~OtY6Ficxr@=;R}Htg6r*Q}Hlq1XShV&DCE{wHxRSaqX=()KVl_|Iw65~sJnBQL&y3D( z4xLYgK+SM?@4?hj^LB5yIHuhC=^NKwo)@rgAkcKKxbu)G0K9Q^@#1NJYW!{)18RG9 z@wC73-1j;)+M$6DQP-kxUl4B{`+3oACi|Ng_%{dVH)WTvh+oIZ3?Gc$_!#uf65lhQ zqLdv`yO#W#2DAE2rHlr#w#MjuhB`I8?D8|#=Of;i5(6+1K`Vm3D4q-XtT?UwPsMe4 zO!s>5_dKRYn5b=o;b0-3_$8Ovx;&<5-d(MjJEd^Hz*&0VlZ$U?6| z%3b$2RZ}>a<5DGW^8-HT@Wh1Dy1tR_bv^fe>|3r*@sBeT6JV>Jw5nB+xxP-pNOQ22 z+pL-_gR|+nGrlJAVz0}KU$kgEpE=V}5r>XT^<}I580p_u2Q~%!XL~5FGAWSqLy0hiGgJ@b7Qa2XJEJSKqHs&YVYB zhEF`2Jcgiwam!(j?7U4@q)BhP!(mfH$?nG598)g=USk>4kZ1)yC{Xlb@-$RDp*^qb zzw_9israxij|IZXOo9+%@cBiEE!o0JIm~TeTqwzmej1x*!rmM}j0FUR4gF$tIoYX` z$L^;ScR147u7JP*nA8w*ch(-y#uDB_flD#kQ)qqEOs+yxW~?tJpSo5y1}CuT|L&Q#?YJq9u`u~yDjiBoiGoh`^ZufLM#9M@}q z+QWi%`k8la;?(KDEjP_jY863$t={&`37-I=35dSNU<>zdzf)iV)Aq$nZELZW2Sb{K zzhihp9CD5Edz>h4>srrwwZ)YLW>IA1be0^{$2W!R=AtO@$KzXL-7&M2PYD4Md|&Pg zQJCr-R%2c#qw;{7A6Ai`YUMHHNJUC#TcH**WMEFuPe!>({V^UM&M9=>_#_uzodoUJ zVTu}*z!jcORhTVgCLSwviy|@$)srl@ zXtgFe<&aH8#F4AKJgeLlUHCNJs}r~=W{xU_C(VM?W_d@*4b1P6guWp7*QvmN!$HoG z6>=pQjzw}0EER~TCkUD1GOf}Y0js!Sw9`){&bQBxF|?l>tM*NF#B~eAf}19~BIIgb zg#>|Z@rh=Fw_+A1CG_cUCQu(2dcL7qSgFH3CmV=~%u)4vV1vE`#X)PBm9sP!Wt1!O zMi?OxJ#kmjC*SB9;o`Jem--}4?v4lFue7-0$0~|+eL4@T<9>moP5`+( zNOEt>Vg&PNoGrupXYbEyR)mY-?Rj!lRvs9JERM)>G&t`58yV4Rs|w#6-91h^P=kGt zvkGfo_>nBQTcpH*Bgaa6?ts@H#5~dSa0r=ReL?;504gDMg96Bx0(=gJd)j(k>!(_U z{2$cuJr}H)6xQ8S6R-LVw>w_=KnA72W@vKbJ#-KY9v1)4QYyjL9csHLYig7CFGpgs ztPaO1^}2XA;5WN9Qb{OP^yVUT@d6KR!kfquWRcvAI49W#KDwr7|L z)KSNjE*R0bk!;A2h<+J5I)TA3o6=DKN_m4<9~Yd;_wK1hlea*Z+33wFw8|p=R5Jmx z(+tKr;cpndUJK>sA;lpg=k{@Sw*70|(aoIaFU6Y4{xZ^8@*CU-i-X;lj!l3D!n-#G zU~WL|yT0jDaPU*3lMF1I12H5XI&HwPQ<9)}yHk_`futRc{zN27FWotdswswA{< zwpL<691ihDZU1&mjMYg)?;i3VLMk8{~L7b{YX?KpIg5qx6I zresp0=fh2{&U?!V5O!L%|4KuZms9BYz-v|H(#mM^Tw7^VGtazkfHJnemYgfc@7;sW zt$U^SAof7I?RT~NH=NuYks@Hs*luuJ&0hXa=-1CByO9}TXi0pI^ZqUzM})XoAo9gB z04BM`y-X=FlP64=5#<4*pkx0UMTX4fLXdjq&cuZ&i?TKOE5Ye|W6x9RErRR(CkgkU|;EleymilWf#Jd#D2HsZ=+f3Xk z^4+(w0iN3S$H&zO*WIcia!Inu>Nv@Wxb7tb-bq#DSXRi`qswM~MUb7BJeB5B>NBSA z1Z`R{Ec}+06%HKb#Yjaa2@4^UIV-)SO4u=I8pr0MCPc)tJIWf|Z^oF4jj(eI@zFoL zW@TLjxTRl({wBti>&8UmHSk02xxOV}9^aQ7$O{eFz5WSLTWsSD- zI0Nq!37bS1!yOXSa!5NfsDnXuwyCNmteUBTWwz#7)212Gl|m!euZR6pQ_DoxX zRN0pMbTmrg6iHF^;hk-uWTz6)a&#boR3VIcE4GLFTrOolBv*g)WVCJv3`$vgsN^xI zO0H*&7HR~OJck-j^RD}k9a{ja9+zU16>Wse&l;yL4~dIE&w`x+;`4I zenVyoTu>f=Oqtb96f@U;lx_Lej}LfscFN>@Fh@!mJBs^fDOgiW6y1ER(x#$~*pv zDm9Iuwey^l7!Fbc@P6J%AnNPx*#A;lc%n*}fO919`j5k7Iwvqxa{Lr)Q;8%+tH}>OPPQw_p<{iP>;O00Bfud&hqNQXyhd~ zQ9BpjL#2VpeHLNM>RWy-i9LIYv=9C`@WD4Gb@EVP4L32*oBA=nbRPb92u=D@Esiv%F8$`Qy4+#F5382Gp?~|h6c_Np>w~g zeblE`2jeY`ikttbHCQyibWPbFrXcg>8}08ReG4KHntM+RQn&y7j z#EiFld<(wm+ZDN^Kktt9 zF~!zKnjdBP;o+-X>%I`u9;`LtJeGjUKrY^epk9fLAj;&XO;+a<-IJec+HD3rt=MoH zRZC!nk(P$rTD#ep3jq@s18&w1Xf;N{zkjb}@^}t$ui=^p8z+4Wi{ZH8jh&4rnk@Ev zW8c^cPD;gKWGwOXyNa&0)cy6v*7C2HS_m?*lp|LG0XTD?v{)Z%%s-x+Ig)*21O`n_ zNBEcLimeb4PToJf&o)`M#}@81LHP+{K(Xz9oZP<%O6KmU=Tblv_x_Q7+d=2u z;+>HTe5Y~qs9jJb`aNgH)W!kBQb2+q${Tm*6CL1n4{E?ePOIvt;~{4FY9HCjVq~Rm zba>6je2kw(W?kl?OeXC1LSb|hFG|d^pgJ@6Xx%%toqq#p7X>wMFi`UvsO>oKN@w;0 zoLeMn(lio?!DbXXMcv;qT^#`LZo8!3~ zV7i!;z076T6-(5kQz?6nmA@WU9jfdRTvYIJ2VcOA7Ls>7oBQTx#Ctw%3|(UTF5Ssa zY&o;yr+m`dBaKpon|5r-Vd{oZSGF%)#lR#*30$z zIekp&115dlg#5AsjQcX?MM25IDwE#K#7oZ9gv4}$q1CGzN4=hvqPA6X zZ4MXW$w-hR2>AzxCy&88@|An2)vShsXcz;Yn3=I6OMj{PeA`eKWYC_IaezGhO%9M9 z)cWb;W~ll?Yfmn!A-3|Zw?58~#2hX~oHY-!D5F-A-vnb?^uZw0Z9>LoVeHz+ycqvq zmrXB+y^A-vgi?yE9~P|cI{a$C_9pW~1|#Bs}41sq?sV^4SIMck*1a&@0;Qay|2>?}|vPV6hVo z!iVOo^Wd#+ALGTwtNmhCKtdhg(Y0`AT4tzUUk%vRj28n{v8rtM{7U)}d}1%u*(3{j z)>jPZKIgg?_d^~)R9NIANgo?t-2-vV@MQeLfJA8(n16pt$$f@d61y6oyO0er>pBu0 zo>-AEH0EKUnbooe_MCol?g35L?q#7-dv?g;o2NAfj7!H%`{v~m2fMMxCC2ry0&=Wx zJ$o4uW1V`p+`QE!dtJNDt6vER)FZ-}3Ursw2X8-$T5=ZvwO~bRi$AskeEzARO}&tp zwAg;LC=j1#v5m(BLD>Z6_blDg8h~IS(|N2*y7lVKu&-C7F3$X2YJC#dMj>YjEuFga zJ6?l}s0Wk;OnM8bDS701l0iuGB*G%_Ux9dzGT4r8^{9cY-U)67v3LaCw{$@O#dht4 zznE`tVRJR-@gWLUP$yd69oNB8Y2o-a>4cf_R|W7W*($2;vza%QSlL`^uJ1!=s4Kj1 zg5hRc*ail>W~7;bPSb)9mgLeBUMo+#%v1Oigg%iO3znqny431;`J;V5$b=4-yjpXAwmh>8Xc!xV82u~ByfL||t&#t^CdoH8KT z9Nxg<&4*Ce{kYRCbVXsF+-F65TI~V7Y^$VInKLwvo$+~~lZKMJas{jqUn<|5s0U}1 z6R8B(bm;M{P5Z)(`?Pi{&~i=~W4+<7fkM&NnwQx99TfnJUXfw()VY62)Kh^&9a>y$ zU=ax34dAmTP@B{Fcs>CMty&s4ylr3D6}g^UOURN|ZQH_5TwI1g25t^KZNx%bQK!wa z&<_vBI*siHZWr5JUOH?ZEXUScJt_lk-R?O~<>=)Ep4mYwuMR8tX(IGywETBHeFsi$ zIb_6WW39QUrpk2|+xE|BI&zWu?u4O?+EA=q18;=@Ql_Ke#_ zqB-RR=5=c>{1bx8G@@wAr9v0Z-jR;k=#Bn2qcrXq38qIX7vW@g%q zhM^N~*SCK#p$-=mTUnHL^S|4zqYNoD;ctqTe_Dim<67@TDa&!)3aF<$+p+EU%+!zx z%xd}k0ClCUZ$Hd|YUqPP94^TbS!KTOH~Uv@s?_`pEfxyp=cY=TC~YhZ(L_yu%KA~h zQ>^#JTy@3%CE@(J$?$Jo*(;#IM@N|&OY7F4w^$6 zLNZylQ6OaMZ~R>SAzlg?ugae6t+p`BU?oeZRj0~p~Z-KzB@_;xk0a>6l6)LXTk;y@)S z(qH>T5RXn5jbz!Bm^3w!@*)BLsF+av2|6*p18qKp1^(>a2#Ef zvLoC9|DnIp{rI+LX8&+F>xzlu#U=KyU3$8H_d$84JGxnE8Wt@TQ9R3(XO<>07QC^j zLY>rNAMUoWGnok1VOvxd$T=dFKfKKheZPoVI58<=|96)BAEmoXt#e&IEA8sNsI+6d zPH7>*?r%!V7nN?HepWgWyQuV}GMhP49i=68s4Ct(bSCttzmW?aZA>wUXv}A&D{&8a z6Affdk=dfslj?!G&q}xKdS<@(eb<=%1tTbLxSAe|6vpi5pZE40; zuQ#f=OnunLN1(QS!s|v0Sq@?}#TUgC+(WQ;$&E4n_WeTrJQ=aYPEPI$oN1^^wK-10 zu3hVz$gKr7O6x7rXITTY-uO-Rwsk#m0&fF)jQWYSY{*P}so1tGEy3DEwM-@Y<7p|^ z3F`~N71nCS>-D_=L+i=BWT4cB#FUPS0hYP>>BCl&id~O7eY$pPJjV(rm)MgtW~ogH z$cCMJe(v}V5J9Rxdj~6mU=*~Bo>u17JPCnZ?N_z(lDqa^#AfNUPQC8NdQCs<*dH!^ z)(9zlH(IqDx14prYE4S;PcyxSCXuHb%zIM-SWVok=Su%!yKLjt={(Ip*S}O}uSh5( zZ$BJoRzE=8uDkijkp#XpvyfPeAes?nTBARYO+$}}*N-$Vf4Yz$-eF%eyYNKY$Ik~Sq9LpH4wOmmg!Jl!Jcb6qG zvT&mRs(tBwz$xJsl$_IOd;;^R?N^lUn};I2G1ccoD)H-;ubYtkJ^n4OP}8ugII7iy zkp(V9&jH%MBGM-1aR<)uEk1e@xR(`g)JC7=&2JzW8bx7{Zn1m;FJ)W$Z}Zl-j|+(u zk~U&ybY)W6PD~Hwg?go_CF4lx&pC1lq%+YBP0^4{Y77$l>Ao$y;6l@D%6q;QP`vHF z3uukc!imb|mJN&!0Fo_Oz6}FimEORz+SdR@8FH=hxl1<_KzMbeu_I`A{;bTyJA`y@ zpE-*DD!aFIX<`$jg)DmXD!`Z7@+$Rwxcl|0`A%19_Qlz8Riq8m(aXnlmH?65qo&Kzsw){Zk&uV=#`pU(OW-#@cBbnA8z2E-c}K~e4LMPHcE$N%IrXoUA`VhEfRCrxo1PU}W!;ZO|7^g=Wk3x-N|ov%h6 z5jJCu=zdyTlVU%zmswpQBeJ%2y~FdHUY#*I=G_yAj{JX&y@y|u_xc9>WG9e>B!m#A zNeCezXn?Q;Gzlv#8-|KVH4G7yYEV=h)eQDB0aU;hrl6=n(PFDMVa9<5R0LN8qD8Ac z;KbIRzK^!GJ?Ff?_w)V-&vRe*`rhNZuXz{H^DTT*)-{sd1`U!0rU z1;+?VBr=)DwDG+Hs;~8#9e;G5H-IuFH%{)q4=bgSOLw!icaVtCsq8}Ll8f|O-ZhJu zVz?PaWX})1e-2_>Mr4Xi)?vXt){-dp1QHe<@(12vjp@Iiv7m7&k-D_4*qN)r!G9VT z4U{;<;-p=UI2Dxm9N-o`qi8YH+uV03@l5ZC77R-Iytm1N<=pvW*d1Gt1)Z6G8?n_g zS+`FRf(PU(Qp5+moin~xJD}0~@Qbb7$Dry_tAq}aegfEIn!kJ{)Nqhq#A}%+ad7YM zXfbQI%I$C$6J&a)`GM9#3G}U$dMS%6hDs)j9Eg1H@@-lPk6;rAcgS9M4UJ-LZSuXs zh9YIA%h(bC!q|jh4#Fi1MetrCx9%1C0wbteg$#SfbzZXFXRmjnS#c24d5_s6YAhMO z_1N;m>9Ct$Ual6KmiYB9fFxhuh3Gj#(+JyKMA8q41#D?<%Gxz*l-cgbV$_;9KYzJ9!#U|M+e1YIXb!}$yd z*n!RiPUAkO*PkkT5HQpII3EB$(?7a2UWI(Ci3_tPdAvIeW)8A_NTfXg8MF4hUPUVd zaElgTD>H6&Z-kq<^p0fH6eVmdQax1Jh?7Nh_g$D$Kk6tIA?=T~a80t3l7vAm%}ksv zqeVt4VNy>n9Lq@w@J3N?(vN3J{9Rt+3{m$x~BXkIWavUt>(_@F` zZ{E(oq znlc{*8ITH5J5Pg)5VN9gJ6gppT_J*|TvY;{VeChw^hyOaGCcM?bpK$4JvSX~jg1BxQTCv`r41ApxIBYnG1{+h$Jq?G zM#i_A?K=fs_~p17vGb!Uq=s8e*b5retEq9gKd;RK z4=ic5+v$t+x0<&U=uA{of7~MK;s;+G3bI<377(ZD5mi2enk^zWH$K?u`P$6q?E4f3 z_RiuXVT?uqDEjj3*VKk1#HHAj%WDvty(-J+xgBT|Lbp$>&4E#I5XAAmdnL~#hZYM_ zepM4gkl$B+?u>n^mwuG<$AW(LCw?Z0sSE)}t&G%*Xn=f=b;Lnmj#isU_6*IWLdktZ zKJ#312K|v13lGJV`Km^FSW-UWgV>|F7vhDX5{ou6YReovy03*3Lk15+&l7?nE$O1F z#U3@9@OW+T7p(8GulH5`bgRHJH{!FInf!tFUoQa*^e40feV_f7oE)U?S$YTScAy9< zFe`*Ycm|L9M!UEz92cqtGT;DpTbN^DR1P)*zP`8wPxD3%+PMj5%6Gd{VMWc>YsK2D z0KgJRNl(WkFBAD0y{F~wGSp|Z5$Jb0*>BZoG+t5{jktGpxn<7O$n&mEDPhU zwM$3|Ex=>4hAvbx5M@`!Xg#tXGQX98Zs>^T+c?nlQ#_f=P}#zSOZcvaH<(&(reHG0 zW+V4I=QQdmDkJ4AFV*7$XB542%)wE!+!aA%(HO zG8;TieuK^JXf!lIlp%fL}%&u(iE zi4NuPS|m-QFYFXDpnng(e+q4Xd9v(}p^Zx80}K?}CB7@QzNgS+jCXK~fC?#DLs?0h z9Zvz~dIyx)`bl&>WHPeh{A^w-5X-bfnixEIRfYSvx0m_FTyC0 zLREc~M2K_goSZJ@vF6j)q74M8-Vw6%&p-yk{%0xa6Ndo!3WT69DALO3+3v~79P0>H z_por3_!Z~MgazyHc8P6ikBEIo zJZE1Ln!dnyT}?gjr~7$?Z_)9DM3|hp;HJ}~Bjl}%YZE$kmk-9B^jPU@V1EI_FQ^bo zdfMsIKr=Sh?>0Djj9eVxOyUNqi7XPqiw6s`I~KgtqM4Y`i@fW^A0`*|U8`JKo_1I{ zCN_yyoZaf-F`=%-);io#b`gJN7>FLW0g)La&JOnONQ3PVJSO-sGi7xE`*gP_;|Krt zl5K`mgl(|Ju13+x)kIR*P^PJ#ttcrYf1LzLjA&RgW#Ll5+xF|1hkxDXf7o@fIL27^ z-n3Di)3AzBWPm$Q^Y^aaP))n#v;s{%vbwtLR=*ROlOp#)T={X>a@B|1J$OV{a4gyX zwT6%XdK+3w!H)al6m+W~WdpkK)2jgi9FUrarTfp zC4x1knF-1(hazC>$z_%0<(8Q}@}1w^4vNtU$2iowK0SdHpg>ta&IIIGRG7Owhb^SvXp5aI=P6o*D)o72jgZRFk)1Y|kbYZ|7Rgib z>U^adaCIm7B;ulQOC++F7#52?$GPq*m)D(I{?)H8yi3)60Py#`S_6LS21^ z+H>?dg}83uU-md7Ais6*-Ov}MN^Bb~e~aw4M{MpUM@I2zF!GcrCS2WTowgU` zg{G{9Nu~#v|FClJdFo5lkk80?bo}qA%e4MEHMz0f+Z3)cJBP9wr#V>2u^9v+@HqjN zhr+0Np)@Tgr~m=!@80?MQ00Hu147;->ub?3%dBxBIEU3enkgFx;FfnDdWaJ* zg>2y(Oc>HoEClzv>$g4KSJ2#za!tZ5B@r&)&Z{YMX6q~4TNam}7C`Toos*{%^vq;* zC6=9JmnhD3hRm07{eW?rX`SnU_JK3#lCAJz93Pbj35O;e-cuu=WHn-0wR_rIX$@As zH_PIqVr>1)C^yYY+G|cP4{nEc?P!%Mie4ArTfWFmFtJs(&WKy37t#mz{wwZ#&z1F+ zyhwBc*v^goDi>j98MPrrY{gyn-L{}7UHim~Dofm3>BIY_$%$pwQFhmkK7{^uktf6a z5+hg|`uoSJJ@@JTF(Q+i>0Y{LlY4TgD^YSQ?{o1G%*;Q&%m3Q3gxo)0KNrAzqe5N% zB)l)omwkK8ZN(O3F{WV4v$PmZnMK8_463~_*%5(gOn@LUS0VYP!z_4##LM|@~nh4225P3aidQ`ncY~3LaB~Tb~8O6H7AQZ^}M?B?t zgg8p9+FiSP!c=N2aBF+xUI+qi5qmAq$ZsxZ#kKsq;qpf8&s11gg;XC~*rO`Ms-jh8 zPd29bU{`}#_qGrOad3fqe2=iNx~BqM4JQ4V^EVGa~-fT@UXqiB`6;1Gn;4w_|@3q$(GiEyzM^thI6lf6=;zS-LJeXXNHv7TVdsy5bU-FhC0c0 zbs99jcaqC(O@E$_VHSy7=3Sq+8h97f2lY&ViJpe6E;}A~(vTLjgC(u=eqPflj|b&B zrx|U}TbdR^A%vZ_g(VS|s~v0&TG@SU65k8yF}KagGV^pj`hEP!&MTWCd60PlQ}x}q zQtlO?SI(xTc>Of|P5$l$fVz7o;WBJirQ(@VoR2=4RaOHVGp^qg3pb<9JkAXCv*MF#W`ycgf!(kq|ohyOvv*$|=Y5v=)-php#$1uqJ`OMGM0s z`))z3ZOD>P1^hgR0Bk46e6G}iW9)xdVL?(O_PS=rD+mTA^uD{+pj9p~Wx1~K1~7F6 z$SJa?u0WdyaqtzFoo1EHAsr;6H6ORGy?05B$IhllH|!f_gzxZ937W^{TBi`Iv_)X8 z%q8Ebm@&_HQ-waS8Ba^@K}BWd`b|U-%W2dOJMrmyAwa4U(u6%q{(kZP(;+EMDY9Ct zuMTa#d=6E$7xDei+6AfF8md{_x#v>txZX^##lvHxt||5qDA+@l+G*3%V5C#aUIhi8 zS&VfEWN%C>sWb_H3FwAJ1547o^TUuMY^)WdU(6na4 z2F|g1jbr5@JI{lzS?rQx-2AYpL-jWZ}U$V_xkv>tl9R*u9^q?c1ew~K;L zf0Qn#XIRo!a(N)4ZJyz->A^cEK!3ZPcO&zbIHLkqroF&lbqOZtDPnpKBl&-#B1E_V3 z++Xr&#~lxBvL=0gC7v8sNq@LGmkgUsSJ-3B8})tRY#?y}Iu` ze^?u#GYh;B%;3;@g+a(hLh(c)+zA1L^M2K{EKWm8YB=G{R@V+xuT0Spv$!`afYY04MR!l6&Y=hDjCI&!keZ@^hB|KLG zJe&#A76MVw2ahGe)Ajxu<#zXtd1a{sw!5U8V6-a`Hcd~sz7bapm!;ey`^C;0%!q@t zXeOh-U*(KU7eaMc+Rr0@_6L8m1^zXZX^JKjf&j>u z!3?sd3Vd1S3vEY-&t{DQPRs2VwCD9-0lQIATy#^6+G4(HGzu*l$4VQ|-rXDh7A8X}sySy{*|N3d=}~hWNhR4Z;p*__6O|yLcV364s^G&|;7E)zp$|&FOAOin1U2~7@=h3J8isr+8XdcAx zD`5=;PZpmI?yB)FdtC(U{1AM-Fuu2StT$!-=If)Yb5lm;4Zqy4Bxsi>X&Xf z{!ToAS~}<$^rfz6wYieI0iF;G$<-@)gveP(T2_j!1Ymi>69g#7uU{5hQV-W#Il?Fs zE$cZaS%5Xo5z>fDc^v>wSDTO*?%y^2&uI6tO8S@Bai7Ib{ZFw6Z~jBs2F85t8u-O))f4*y{BL#kP0WaT?2#8aMs+hN z_6`3f_KE|4i5>W-*j+!1z4uSCU-)0KYaeR{k%TlsVdyJ`VR@nbbJi0He9((o$}LGo zJ(f^>n-8eSH(P{1tvfHfZ^*p?%SV2*r&w>k4wLP^pZb)DDteFk(X6c~=1g&4(of8e z%jwe^MBm(SqgN>5_W_fxcbj(RS^jJM3rKric)_>w!mBLWyNmAVaHNqQ1m(Mq@0Y?$ z==~6s*h3+8%;60?9nmz7Th+w~)DTowH{P7ptI-xBNXZDaseu45L6T#J@$&-y(D*-E zL{QL^{I#U@k<(tHZonfD4=?iyg)L}ed!DCC$#X|Vfo{36_W&%-FI8EdGMd31T5+v zsnjC{zK`HJLcSnYC|0}qvVLn{a+RVZr>Z3YQJ;T^(3(%P9X(ObBBb%AXo@E&x({5@ z{K}B3m~zNCN$>D{UqTHntmaBnSHuduF6!@2(d(g=0jCawZ;$%zyOlP@U`Rz#wQztzV~$bfYmay;{> zSd|Q^w2L2$F)9Hzc`XSopq1_;TK;<`b~*}(g(^CersU%WMH^o5Oh4SL^A3ZtsTp!{ zSF6cP6ISD(#Z;7D+VbJ%n~BoK-N?iAP{~!@wgd$7?iF`fZ7Wd2bN*)&7XbCe{LjhQ zkNtX$8t_M=?k1<}%?<$Hj)-F_MMkI~BExOP+w1r|oCSiqR)o5lR|Qyeh@%2NALrmb zw7qxmMjalF*jb!bI>N)5Wy6gl<`AfC6_m~6@MwlbFS8P$Gi5i*WMyS#`s67d3%k2> za2PsaV`h)29rbk(%IX$%X3!HF(O1!m5a?h>D(uQWH`;73RKiN$lVX~sej1ZzSx7f- ziRVJ7qAZ@IfCcRK($UxHQ|)T1K?{ySje$(r&#DGm-});tlPX$$G%H*SXM^o#5W zQ4L;|xC8NX2(D|lwu~@kQK9WI9Gbam(Xf7I%yIBGb~Ee~G>`(*-Lb|gC3|tbBIGsu z%%f)gJ}=gF5SlfAK@%JXb27emWs2pD!pxrzOO!S#?lPDSPs$;SWA7!dqnuOe@?h0o zDgqOFXi8nMa+I2XtGzJaF~J>=jJz{gdU%Xxuw6lJYWWkIYkb6y>?F^ry_q01yH%6h z&T=4%xfw5>7bQsAZ@taeZ6AL+%|e34ea?o9|MxYk)%RX){bK(-*`bT?5B+Gpds#*@ zhCUlT%THp$Xe zxC+Wa!sxd#rRq{*!3dMKk$+F)NozV^C%fuwx@0$pNu_L%;#k=x5A>~Bd6uxkmq8@q3{pgxd}w?yJLY_D^~w1 z#n@P!HuI_By~`)NX1P`x;#|IPR_L}TPidX{?ZPXsDPjkW5yw0!PilU+5eSmUZs`?V z-nC6@_+fYAEKj)8c#u%Ln+hL~Rv^BWSrT)N*)luxhr}RTQ)7q}R3Lfnu7wwpX)EWq zW=ky{F06sLZYa`UhdrJ|g9io<@{5b$U1BNDTTLty^Gx6&jz=@({n$3ga30YH4d?BJ ze;$MbTgv~0=?^z7B|`BAmo6z`jxDh$?N`O)Isk_u6Ko-1m8^ z;a%jy8%^(PbOd1EA}XoVpQA-q`Q~FOGER4?Y>_JJn(+**ln=qXAfP&Zzq!v7ZSCxL z&8R}B8!dr+2gjLJxRnMez#mZBS#$N)S&apkiT{AQy*EmEcQ?bC+xy+&z}xEYRpS3e+%Fkj~<~cBvjU<>?W$_Ao;nCRO$K;j(~k` zlD1H>wbzJoC6&x{JLYIlm&-Q5XYZJPGV!O&fvYy8mT8_J7wl)at`RZn!;xSUnkKuD zqN{-NW5ONk)b5IP!6q7=4-`TW{zql(<8m44W6vKID}~4v5dXZwB6&g4qT-4@$~`_R zaB`)%3fP+$aG=opaQ%T0xvy^?zdBSQsIV+9ZBPZCh6L3g-^)9z3YJslehr8V)u#$O zsu%WM8=r+0UAIRf&cIna^w$5O%K#A2w?BMPBQyLnAEm??r=!b4r@LK|%4( zisk%5lNOf-LrKV~?DWu?qOHhkel&5G#HcvHw_4t^ZbL{nqAxo2;5M^~&_p{_B_QBHllS^z8aASt&qe z6QFo(HKAxg#y4#4=gYSlOzqdX<_WMh57f(X>k)i~BlFTsfp}MI_GQQl4ULw*6;08? zUQv_bK)FV9Aje~0ssblVli8ShQ1UL5>UQ=7G0u^f6TJN2sF!@IKZDsdIIsFocru-- zAUp(rx6!nh=6w}ajtdYPVIKAnLPNl81 zSQSiykaN(n9Vo!M;DRQZnh##uuqHG;-PQuaa-I+Iw?#UwN=xGd$STf3$eH_s-msppv5_QTFWDxlsiQnDdo+PU0q~NIP!f zNjfBIzl=KH`!&xO&Nlmb0_$PN83N6ec&Al?QE%Pf-hnjXl3(^0V<_-@tQ9#Dq^G9!NwWBrtr!kC2-(XZWISh&Wgxtv5aI9| zUFqhatS_Xkp@thc@Z^*}AB)eWs!xs-0@~@vVXea-c?!UIr;#I~u}S$Fx^9V^?E)sv zXyD;sH--0whCRG8C~u=}D>`{kDa;Wae|vh8x5y+S0xsehgpj#}h{~=aid~nk!4+d5?Y;nwP+RH-V-dTg&W&?O zCdeH0Mh&W*o0HLjkt#B+5t#&AH~@s87{N*RT^gQ^o$xH7zMaZO#>pdlsGAMF%nFwR zIFwK2>v$$OJMzraAdrt9Rd+boY9@G}ZTQI#{|OyP~o;`l=hy-6UpYKanpOx z(|0-YmH&G0T!6CdXQ~k(nN>H+v(gLpj&RcKM@#^`ARS0&+>)ATvFbCXSIkd)90ktos!GdpB|}`rpMW-1Jaw^qrqSLNVm%yvh7Sgq${d)5*WAFyb3{qO@`u zhJDKkhFoWu?w4;EWht;L4DL`@cmd%cs!dGFlbfb%k6^$0Fa*grohP9}Qovmd$QC_+ zyqJBmKhM5XC)C;-%6L+{OzCO|G8i(RsT4U;2Z3AGx80P37;J9=s6}Pn6eXPD9+9trV-N*mIGw78mviA6j_sG7 z%jMj4EDwmfwWoXeElL|>>(6Bk#qg!~F><<0wubdO;1MCrVx%3!0HkaJG1&Qw+V{Y1 zav^RhwO+N^=?41SI@|V9UizJR_#hsJ#QVc8v^?=ouwz%(?0_H3VZ}%(J|s+C_LJvx z90Jqo*|AIyU(?9z-<}<6B%wTPqrziC_w#H*w=ccb_G+CBEx33uH<@vPst(AjbX?Q> z!M*9x!WPu2xOVbu=w+WVNh_y!_T9sj09$eW(eAC&Ib%uTgvq^4?=E6Q?)!MAtvMX#tzw_?1999pQbDq5BR2hK z!VZ5?6%22aF5|(Hi#+^yJ|!=Q2nM(W?~OZo`6Z`DBMQFi=*S0dmnmYM$xl0?wUh<) zi3=*l#0!Xtcds05i!Mg4-su&f86tA5of#=Ji0J`zmBrkwA7_?`XB}ZKE{=ud`vbU3 zUv2L_bm;=X0omS8<+d6g>{#zLzBgXI@Thk4CHsYlZJTWh@}ba!JA`Tk`5x&o)8in@ zL#&k3x$XV1gz3vhh}{+~RJ;}zE8`PPoEz@qVGD{dtly$X$3eFQdM-ql`@zN7ECIxP zdlsyx1jy^|w`Otbh4EadM#n425yAh=M!`JfztY6t5L7VA@#9fAj}_vuAm$=fJCEwl z87`C;TE}mX|EgO`$DyqgJ*DbH5iH3p-Cc~bt}0tFdfGxdVx8#X_ciBMy_jYWC2dCc zJ?REA1i0qCPNjF7LJvBX?GpbN^qZE zumwE)Nb?>AWKSxXtG7J!i0Od#ew%qy@2gap(C_N{TGSm1Q)_vx+z`tic2EF`)_KqpjQr+hw%=`BphHLlbnK8v_8ZZue@P zR&weGyg5x&AjYyro5v{8`-+I(*js$FKi_TOywZOF(F^>Fp!!ZDb&7R(mtID55P*e0 z0NAafdQFMNQvy?(Ee2`G(Rh(?O`jHGjk){GSJJBugmfZme3gqEO1L4&)?T?IKXjp@ z0vrZKT}{k%3AJu*Yd*2p!T)3iO*hU9adW54MTULRx5wfup+qAy5{|6QichiTq8%FH zD)<{xXJd)1?^%oXfaAS%5!>_V=X4Kh6E~kD)^sP=Tv*yftUGIHw;cmCwQ3uN4*ZypmxB zTv|rPARy0isnznOy;8djk;gOf)ulk9GgmYDS|>tP&y7NN@tw#AOkr7$-D&LdXn9l= z+gjr!^UefXCWrYB#2y|n#T337(4&BuaJrZ(HmM?>p`NfJzFmwx2lKLkbm)+1?pCJr z8O1k+B^I=i=mT$iWd{>8M?*c@GfnDWYXY%j+J0JYlQVS}k>}}g0m>yA7fvImW7A4r#p+`)-(=_ZnN1?( z#%87WBj7i6BBrFR?Js#QN31oSKYmqn-KnO@PA|ZLHL0_`#oQX3OcMYF)x=KdRZN zuhQ%POU-@}m*!+~6ix1nyzASK#CA&)(`(sGG z->rUJ;Z2}FjK`_l2KK?)r1QcZ@~RJYq|te`AxGz)@$a3|o*f78GZ7Z#+|T6x_~N0( z?xU*YjiHO(l3M#cV(T+atTVVo_34na&m39RkX-j}QH2#bb-tCeH=@_vE^X zG7#@AF-p^mip)8Wls%cKkV$G! zUih{7#c0d=zq1%OE`Fae9*aH+bcbJVEi%c}~^&aRDi?G~u&MOk+*p9zY1$O$Q7 zA*V8(`10Iu+V2k`8ND_qAlx^j8)+8!gPD2Cl;Wl4aQ2x?WocWSkauO?QS}grps>Qf zyt-r7Mm=DH&%ey^b2v(w7oTC(X5b=lod?jQF;d=JQWY`-$aUY?f50~KVT4Vl$6sj7 z+<`fKM56|}rl`o};wLnG)B!(l`~adc@PuWd-6K_n+3b$4)Qnc)%5I)PvRA>+rab{Z%6|9^YE@@M*Xggu}2VO zVlMO^c+cwz|7+A}eaUjD2SrMKrRhO0v2GAgIJJR%#pd_2QYUz|f5A}>lNn=Bg1_Ma zd%GVy7k|9;eg{lHz4V~6s)YnBRAJ_3OF(JYGN$Foe5mDb$5JzD!>AJL_z#|9USCil zGJrJF9jr)!T0y|FcC^fNH)EW}lP_hGOVJ*eRT+aVA_7bj@oGHuggkJT{fp-412_Ms z&dj+@tzwxyD>iqO`#chbZxLLWS=%T?nv{)TIrO4_e8fuA43jjIUQ2Tt?LKCc)2WqQ z+Lq`8q5V2r>&TaP6Lb(bD$qwtZ_PK|dnbAxH=B)&hCoA#$wSk}3Stxvw%$@SlX@RN zvTA!2WCjkeT~HJ{`FP{yA_Xq2MQg?A=Io7kV8&;}LPYjkuh+j*u7q6M++&|+D+OqQ z_unKyU!GIV3|tZa>rUW~=CJ#VkNExgBM|pRpTcg#&y$5Q{I5j(e9Bs!;EIi-uM^UK z1sj|Q!>33wCjJ_czQAIo8sTa)&zHsz&Q%v!qCV^wZ-4f6H`>2!R7W!{Skj3>%o~-i zDu=$G;jh@L?oWk~d2|D5={1jkc78vC@>e@)1(hn4V07}W=*7QCMBqRqGw(#Ag}9I; zx8!n|==zHK)ij7UlPBsq!iAv|JKT*b4W-6|9ETAPS;HY@LrI< zg7GfprsDDg%mNC(Be=)j;XH48h=nU?;0EEy?nI^zVN!!k=pao*kIGV=$z$X;U{0)& zeKCLCvto=x8VE|2WWye35R`j;{5>AlkaWnd8I7wyqDiHSfOKykFwauKD_ir&I2Hm))b| z#kQ+`Z?U~C0obk+Rh%oj!wRsw>;Ybr1qBs?W4jO zA@zV4yCk{k44Vj*buz}Md&((pK|YA}9rb?z^!~gw=mE(la&5C;0z99A+Yu{t7zhhH z8N?;q!9~5nO1R~ep?>wz7Y7_uAsJVA_4)<~OQ75s^ZYC>HfRqmP!xOy*mL zzBbAcn_ja;HYN!W0n&3cxrs+JrWLaOqoMQ>p+Edu?w<`M@G{tZ8q-&%_(#R-QM`gu_*U0Wh?NY3f?jMti~m@BP5^F{v<%nj97sD+&=oGE`ML9_^2+% z>xX2=p{}v%VPex2a8rhrZtWO2d23xp$Pg-!@}&lKCOvD3DLR9KdHr%67A#vA^&wOD zg6I~+9`U`UVHde&0E{7qTmeom&AZv20}1(p4!3@*nHgSeirYpbJC~r<10Y-Ayw0k0 zSgAK*L!M+aB>CRHFDx^L5*0j^EhWhmnJ9vr*v;J>nt$G$xnWrxPM<|PV~X47$HH0$ zKvV8;33n*i;kCI@c6b^?CEXG<_t*u?fY<7488HcmdRV|MM!TJZkNqCKaSh09i&VYg?_ZhErV z{^jEv@1A)X_Z;%wbM?_L~=?r%lD&SY%*_k9n zbq#t~o=rYrM^7NYOg7}+O!mR#sFQ8;0~~9qyAwhhT#ji+=>Fg`2WpP%CE~$kU+d&x zGyfv?7%$Yd8;&+*p6*pa2VV|GY{?>gUX%O3F{(@bKQL-hXMzp2|HMf5H%6^6myZ~A z^Q)?$`!kn*D{VMRfCla0obwD0vPXxMFy>8VhsMZFUo?$r8*se`Y>ALADQ(@=qo$o1 zC-`b1zO<8MNtOu)A`ZnQ{|wh1ABl%~i1g0|ihTOGJdF3e4%vN69#gV_U>}Eq7YUi_ z2cl}Eg}f8QsOS?$ei_nXTa}6BCyXEs_=eCwF*0-f10&*pViXbSm~bk}Zi{d~brpRN z^MXC~_3-qM*A4|(T&W5B;rOzqMO$*Y-n)Iig0FenJ_NyKb9&|^KYC4~;RLy!1F{=_ zo{LTRWgF$&w!PlJ5B0-lW6!R8;ALT(b7Mp~!d*X8;N!5Riie`KB%s-Q5N`@8r*#b_ zE}C*Z9sveNlo|LY8{HF19qPe@#P~{LDF@OGEk5{U()EY-G?Y2JOUVrjNWdbkEs@?P41za%T4a{XS}5?44gjUDIW=%5%5cQAT;H{na#9an>XIu zdJRdSRgC(X3u4?3Zn#a>W!+xd5PWvw{HE?cWL>w25?`F$KhQ0_*qSTTCKTcceYW$; zw1C+mgvB#;?P*otTI5BK&7<`C?Fi?}huftm$M>wn+G$1|Zo!`5C+lJq3yR+_PHBv3&n^MBi6vNvB zpOUtrXs8`{42NElY}Cz3G@wddT=yCNv?9l>+`<%yNUR`+zSaa4_&(A1qNAiz0mA^d z0?0oo;o85Zb->v8XPxmca6ZmXY19_}2+Zj&b7lhBJL`EGm9E@ZAU}E1Vh8i1kGn&` zd}vf7z{4=xjmEbbm3y7-;OmRiu8wF;4=_$JV{VG-N?^}(YI=*MQ14w(qhPh3AoudP z_&Pz2z6%!O)5FimLh8=>xMRMBYQuOYW(U%n6JXM?A_AtId@s51kZMsu6x&5I5GIh- z5xi)r&8S*z!jpvek=h8A18pR51!`iMQ&*YuC&K??PdfRwpf4esczsUdXhe1E$W-3i zZ^w8+EJHP7NlMMk%&WBdE@d>{NWCJ+ec5gYjy7lwKYu|+OGcVXwC$7qjIOM&ZqmR5 zIeVs>$IVt@9=u8uK0JO$x@gN2A+&MV*PB0~q${lb#gBjgP9&$mK+E%ad5DJD@uHc~ zOEWnZ_F&bEiO-(@$!NWM*}2}PHCVc?LX8baBOYEm9hY3hNZ)n zX`P~IYep9a;Z6PM>}HSF^T-@`sJTe3M03NSZ_A@t*J3{>nE&DIcKi_?#f5S-f&|fU zlKFuxwh_UjDHGszr$yKcMh!z#4s}n26H*F#h0qfaf}aZSAeX}_%GRU9`aD}g<+rt) z#MW>h7@54?9O!5e+TqWSIkd&e5@&(Y8SV|zY$qCV=3u~Lpmyd{6D4^VhS_`^ARAyMH5|z;n#7|1&sJOINW-EV0*5j&ssON-rJyt)CUa^(N;Ul zeh$ee=B3mOCBgF2Qdr2`Dc4ro$>#Y*dcPgU&|)koPE zqdUrfvIsXl;#a9X7`AFU_mr32Y_H$fipLYruN<&aa2B ze$AK0_Q`$=5h5(w^29p3ucwv)t1vlLJSM-D6oko_4u_?HiW7pOA8RN{DE@5*K$LO1 zQKcKis!?F&s5lQ7_*`vJ2D^b#vNGJK$?FG@0MbnK_ayKq#|r}=CLd{V@;^=DiKu+J z8{VYd!JzIggf;S_S`B6b8Oba*ZE+=~Ut} z``z{-rW|RX*C!3)a3Yutw=P6RM>=qYxFw74KyZN`VEali2~3_7#33YVrepQokUa9R ze#oG@N-9r;)5{3cbUrh*EX(aQiIFE&)J2iEYo1;O9Jnd>J9;_Fxq-AY>b8xpQIKGw zm)EFf-CeF-TmTy{Ybp*?@? z!ZDr#nfGGQiuyJ}ixzY^<9g|;Y6VvT&jGN+fp6mvbNk=rj1E0Y$oJUBeEM_q_#R7O zyJpRn3o&0n-SQv{!z}Wu%wO41;XI|*Bki+ zwRWQbLSP6(pmqrolTr4nbk2$#n?BKG>$gu(;X=lY;7;DTG6rYxh7wJ|-{Rri`P5 zyZd*tN!&FYh>StHE7bbH-e`1x_d5O;jEuIS+Avf`AcFukXba)5eOt-m+CIniw&*NK zHk`zTAn4wImq=LGhsRh@a#m_Gum{NGI$P)SVXFKvRMx%s#Y{jrNR!XZbn;N ziS!SW*IXkhr|O-l>#~})oVm-0rr&ffZ-C6S9$NGj1E=<)q00&MD{E6yQejo|b8K@< z`5BmCXb6ycqlcBCx?=(MGV1r~h5cWq2V;lAI!%$6RMZfV8bZ%$A6e;PaUxFqxS z@89f;ih!u7s3?eg;TkO#C@Pv7nwi@ankAMsXl7>C00QC;W=2+qre$S?W@R%qgZq+| z+Gfji(5!57=GZo4GtHdmwq}}he&^Z#@o!$f@9X+r>*w0{UYUW;71D{|22Rs=0mn5V zPZ~lCQ+dhtbKew)*H3~j37owxUSNmxqpN_}W9+qdJK;E8 z2Q%A(C8W(-3W&gFUtz-i>rAxUqks;#D2L{8zHN>+fXM)W6wBC3cr$@kp}9 zaY7H6A>7!4R)MRmPk4w%@k1%B=o!XV) z@k}Z?SH_sR-)3TW6t+_1`)s>RuLgA1@E=~A0l5oS_m&0n)z=8M=uZO6U;qbh+5LNQ z13ao7uk8jy0YajP)F@YkMo)J@R3d5>@*WypYN5Y#p^%kGNsb-li9;V{zGH6}T`w|q zG~tH7QbmS)GI2k6jds?YA=OJK^JFi(fh5#cme@gDbA}&X7Z3!QvOsfScnho69)3ye z`ESpRohU6F?~O!rEJJ!1 zA-|)BwdqexTGCFeZ4pj3ud(ylW4&Vh%;C$k53ll__ql?! zbJLv2Lkm4Dd{+ctX_fe6h}^_m5>uy{pD#El_Nr%2V4ZaE_(EgLmK1Q6f=V_xZ*Bx9 zs!+d*ili8@Wt=k}ow|OnYhFqaawk=@dc%)ilTuiOhpONG=kG7|TSj(pxQm%h=q3lCBDBM|#Vbc7cO^qZ6kRFe=my+^C8}l3p*NMj*2KoI z!CMYR$)SNBXK8_eMnD-(lAj+OGT| zDj`c&R`JC*11#k+VRe0r#UgD-_q$XtVxk`j(v zK4|VqfuV7s$9)MY5_vBnBu<;q+E7g6?HYdDI#)#4GTEo@7i7MI$k~OaYgB(Qih8|J znXDq^x@T)7ME0rm)*HAtrB6=ecQ@~LB|ud_%~(exD% zEOm)8$iW(A=KUa0_Dt)UXpdZwI_U)15Mgh{ES@V5l?2Vm;ESIS7MN@ap%}v_h5=P( zl$%&#=oYI`yQf>X`>uA&xXF)DZweN~>zWa#R+`AQYVMn<;vpsejAQB{n`kWdVo)o&hWWDwrb$B4Y3TV?ER>MgH%``q6>@ zy=eeCQ1Z)ReIYE2n)Ad3E#aSrlGwh1)CyA+M9bN*($YOiIbz(t@ntt%QktKK(JOj@ za(GG(HAAdKB3O14W4hzr>=1Wgz7tD21c5E5GShf3IKGS;;Xuv=D2Qs6tv;wmrl<*V zF?*I6q&0QtmTjZ@YEKrGC#>@wXK^>6>MtP6Sj-)bMFd-1n%{$Z>_6xI=CaY&(lS_l z3H*$$1iVD?#fscvUhL^N56p!Xl!W?{KI7m(HfF9&0YlqTS=y5zrp+=MwEjLb_-9iP z6Fi}G5IsGQN$AuSbM%5W?k&48c`yeewhe1SkQE0gt5DKb04H^&X2s?!j9dEC3eIR_96{>1rWjZLKrW@`if1K zT4()d%KO(FJ1>v(=c{JgM@)AD)N8tsZ|mty;A~;<@fHo;*wL6vbGt}pGI`)J@0`}! zMpWb=ZVMmt;k=7E&Yzx{asL%ZTNlnGMuG-b-W%Gnyg+wnwLp@ZjvOfwcRyIPv|ws0 zo;0O3VYn>G>j0iwlo($c=&_PB`KC6gE6I9JV~`_13r@d1Wr0n$DbleZ=}r!S?>*28 zw1p9=uBS*&X>F6}BIhN?vqZ3!YOU|?QHlc$3T?6SA-THYe^qXA-6=??9##d0MAlkw zZ>DvYPe2%y*pOpk6biv&@0UNcHtcG}o_Vd|sQ86NUMr3UJvR^fGSF-D6^|PB+tY!x z7mo=gVLG{KY23p-jVtD;pgUWvf2!V=@`wK2=El?kqFfWr-l;fqsm?)xX2LuWj|A~9 zdGmOF8^%jP(ZaqExv6lJgZonCk`l-j^Eh~2CadHts64gI`cR%1)F_@LMwCJCj38-c zS9aJ1DcpfaOmGrCC+X9h-yqb?(ly` zH;vE9_>cS=*Pr`i$!7{|goSf1+~w8x`I$&B;OR`u{>TiTY1e zfF;pEo+|Xs*gRZ+!)uWOr4jKL|FVz=8qz;*Z;-!9HBWr%RY$XY$V4F-deL^J$QCZeGz-|C5A&bZs zSUbM~QRi|WnE|#%(aM>CCFZEQZEUQO0Kq;65BIRHm^{j^Cm-u$tef|92*Ilc_Dip> zfuz>xv{ai(t5zx8NW^KonD$Tf?^Omu{`j#tS>11;!Z z=lV@?^S~P3K^oZD-W!dr!LsXOP`KqFE8QuY3&YQ}jH(P`-^~TNVL`FeZg&qKjWHp! z-IZF_vEiT5u`T_`g^Ciu!D5>-0VaX^8T5?y>f!z}esZvi&qz>aT0+3BP=2x{N*!-O!o) zQ*QqDgO!sva6j$J0oFrPleN9ZqN{D?j=vVG1M#`dP$L%e!$trZn}>hw@@2$yp&Ubg z(rKBi=L1Fd`OcG&xWis0Mbzx9K?~d2i6HxDB*BE-ly8vR4tH$ot=QT=f!Jy_*Y<=> z5Jt-w^5(XwAljpL4)=4c@TWDIeboP3^D()=>w!ds3FCvGF0l(dc^-Zklw}f^kQWLb zxr{>k%e|JD6m$rl5bzlDJ>$xa^n+^V28MC5x_($KM%JC2e!6KGzufgM`fD}1^9UCL zhyOY4^xuht5I9^H20@O*uw5Uq5pV?fJvby@5N6OLtjGcfPC!BgUc+XW7Jm93@V9Ef zOY9NWg7i){185o6Z4~aar?AFV8m^H@6Y+GeG zrEYrs!b@d!U2U3w z{^&9dv&nTQ4BeEBh)SPfSDFibi`{9C=5!vs9z6;IfXzjYJGOlQu%Y?^h4= z>AcZ0{eeu%;x-3ZjoX#OGvZhLAQRf^Kzj}8^5$a|;r(+}0jMikv+S|H8G5wUA*JVgwW3$tp805oL9A93NYWcC0X#*^+E(|;^Z*`H!JpLj!l978$u{cQ zQ5x9x0$=D-fVZ?3c{ZATE6xa}z6i!3QurB&t+7et6DYd^0&@&_O?yS~M8IUkc8H)D zN!dLaaK0h@;rT!xiw8O|Lx(P$n$DTCm>xbTN0v~aBE!wqp2gYm0?49mh3s03as$7K zS@tc`H*#^Cqzvc#UA4N_0XSRE4rPNaVvRz}EjFxc%aVCnd-&*YXd#ylse7puy>;L> z!&3Rl<^r!ZwXHn0=v?yqTQno<-tV~7j6K02HdrF1C3Am~X|m~yv_{tU&_FqMTi&~p z-U?^)<51pS$ddi(%jO}`lbE2??anrB!cQk-`?FhUlcBxfnyT4&aXm41z{i)`I~PQj(JYz=ZtM!eFY zeCzl)EUooQFf9tkZ0*Ed0DKxD(dZEg#CCS=|BNs{PVJz={>;&fl*J{biXCN@K#pqP zK2-Gqd8npvdnHA-SS2j1uRBDwv8g^LH9AxscCJl{Ie#(tqCD@AB@lP{z|I5C`qK1I z>G)q&87M&d^X%39W2=L8N^@~`@EYSVqr^8hr}ybyemNQNwvs~OihG)d+NTOh1FklZ~f z+~==U1pDT1nvKv;-z+D%1WxIMfJaGi2=h#mk={9=3(4wJ@5j|m4(=&Jr=uvf02b=_@cM!00i7Q6|Wcb z(}O}PE2}_rokaN zQy1i^pC-!zv}ePA(uRRN=362AKR&B`YOxfJ!CCRdJ(Ww)5~o;|Zjx;Ry}YK{Wfm5c zh;_jxZy-2SR)M{LxZuR3qGS{QTW&)1J~kd!35n;f2Bjf+=(7H$CBmkJ6x;5Wi+#1E&_I<5+JaxgX4k!B=xB$EG zJ_%OI_`DhQ(QW+amotmwNa_TXWz8E=#Hdln8^WgaiB53Hb^gl?3`@*sRC5K%u8R3wWUnn^TnEM_X5 zSFt_>yvz}4T}37Ji^s6l4P1?)oM$KyPM~cHY>1^zm=umpo~#R(sZm1R2WiUk8DH+uxcb$xHGI z0z08%9{BkT1l);)lJx8d4@(O%cF#v=F>D;T7Gkx02l)A9O4hIQ+?%b3glIq+lJs^L ze0;>m&u4C~8pt6Fz)qjXf5y-M0Y)eoJc1h)jn}3%5Bo{+Mi$$duC={Z;cvP0j;8oR z2plbfsd{hpNi3%uUk6E(#C~zcM@Y?*a(hZ;$NiXD&a((g=C_BJ9-BUN#mdKVdEmj~ zQBRv?buY-=LIIt)FHy*xrLJiN61TXfsyAqtCngEX;)xC#HCtm530{*t2q=N%&IGC$ zeatnOwM=b@x__{oaeM}yg@jt;DF!@a|C8>JfPCOZ)?{N~$o3MV(+}c~&UIm8fjo-d z{H`xj2rBc5j@MAXMucQWG=-QI(L$5U17zGMAzqs`qPg;r;;BtOuLiUuKweGR6+O5A zJVH#oZ?2jW-*5e%UxB#M{ms&Xn6Gy&BKA}|?@3<5AnkmcM*iof-5?lI-_cy>lal_g z82JA65Fe|#VoM{S>IfqDIz40&29&qBh_q~o=B*iPG)Wh9g=Kx zjCsY>b@CQK%L7Sy>5Zd@a3=vCqTR8sdN=&y`-CT^z(HtaIJZ{Ej+OeOj{hF^>KdD5 zZm&DCbOZb15U(NHN`b>fshKI0 zCMcmd?orot*Rg%=7fEKWOm;P}m#5NR*aTe5%pA}3lDU-|EnT!+Sth29ok`05VQS2Q zuI3>ui68EdNsDaP6lQL^GAkaG_%SpmH}^Omvo|3SNv$b`5l(?^k66aya|;L|tJ&ga znnZQN2bMHVwAjjEnBj5zU3){-OJbp;3>G{48pLAfYid{!bZr9;YMPfoP(#|Bf;^WQDaO-6gdfc`#g-b~%d-9R0$v?6??+4wWuk zm5#hP;^bkCNz_EJS+OB-``{n=mQrKk66Y!`F0anKfByE=0 zOQx<|G%pO`%f8t;yEFNsS9WvEJ8b!_%FH(iu?=SSP0kOEcKh~2vtK+95zuzrQfxw? z=deMDZ$UD^Hd_6kWVmSUN(^7Ehq=sVnF~lUoc*@=ArQ)fk5KYBD6GeffjbXHoU!f7 z*d#l%rLza=i~faX8;nkq|8*D%+$#c8-}yfTfC|3R^Mxe1I91zO2-_n0=8f`hQ30@I zAdvj(0Rb}V{a|x!aYKFh>Y0cEVSgu~GKoBz&lPj&?!7J{SiFuumG+1Ox84UYC?Z2kVncId0)d!Fj9?zW>YHO+U;o>Crvv2jUn!(6YT#FaRF?H@n% zB4pFgFSlNg1MjDGXT84q8XJ7{imrI$<*oZ{f@AY$^;U?qW(PTFEYei#$DI=vxgO94 z%30Bz+%iWi?!`s%>3p}XZ&5{3Z)}gklaVP3bzF?)h7zC&kjH`HvN1bi}Z1^ z{9k|-n$+x(fPxycFcI`E+HQ%_-WR**9<@5iw(4aCXz&~$#H=Csnt?hJQl!aeHUz7= z_|B790=0-V~qc1Ny*^O+*2tq)=`F9tzz-zDeUM*srP z?M@_Vx+P&ls^U}#w7F*cXt%-yhp>rxH!CL;Ia3Y1uBwaRRdm-%iksQXNc5>EX9X<% zB(8=ScpQ7U(Xgrd)E8gVnIyW*x!X6MsSQr8LL%CcY1b5Tdq=#e5GG!ldB_U(6k8Y8T(w(kbUh5bl0UfAYRa?+q3yl5A& zoBh7x_!TadGxnu8o4dLY+h5}R!}}&>|8M3~(G<5>s}Vp9{krvfbil6BLe`x5FGKFJ zUj0fF$hKn+^6deFZduP@h&kxU^aD8d0C+CI{w;UXDYu7bvvvC}fCe71k)*nsAAenB zNp@6o1) z0!6*JHB16SL5^I!JH^L69U%GQ>K2s@2nt7QEVwb_gA;>r7|e!68NIFDbvix(m-=8V z#Zc=gEV!49aLkF3Mk1T}>$jg9c0x%bl-*R8NrPbZ;=#!Hl=!l7W=i_-`0YMRcKamH z9htjnA9({H&=h*`bd#`kd_kA(Qdaa(qTPoHxk;}k@NuHC5YI*hs|ejz^LPl*EbZr< z1M^8G>94k*89G9h+4Hd_8(|0!EW)%jw6+qrO>}M%mIPGIaR$LX9M{ZUz7n>PJB7{A zRF}sAD;}}-#GmIjNzs+era6^R?U3Y8`=HK!Ijl~m!bLEvuwl7pDGB~V{VCOu5h~xq zS^(oYtbZ8G$kWTz<<9pW@w$koC!WJXtS{F>ITFap?!-iNH|;Q>I>KfC%vn`Ln=W+ zNDmv-VgOBHV|!Yk1(N1*jHd=RK@bExgkJKpy9jbIGsNNbbBhZ;p^fHdX+BjNqwbb7 znD#B9^~bl8o5*5}(sBJm-R9J(r^W7zsw~j^IrFF3Kpbti+re&L#7L|vHc;uf|BJ7g zO2I%EW>vulX<3!0O<}|qILR#~%-lpZJrZ2})g2Ig+lKZ%(V?l%&sIYU5Z!n3vTDEU9{ z4ZM|VdR{z(6QS^YSLvn_*V*qtA+j#aEz}Cn8RHp3vxGJGFRKv=`>W|icp%~fisx#W zWfVt(biP15j2$=>iDEiB$OX2WR>!e{Y~w-%Ngud+od!JQCbW8njRS9KJ0@0;(PK_u z<)y$V|Gbm75AqSrDiBQNZxF3r@aKXzxYG9TSU|bbyiK35R0*1cOGD?e7ZD8(auz=B-3eMBA;f(0{_hD*&-xo(puR*8ajzB$S_pPJ#AyibjA$ELk zd4y@y;cYd0k^^<j|ndvgZu z*J*&ze82m#Vk9Jo-($9OR{Mk<{MKOqH<6FSXHf82nZK3sQJbYBZJr2*KIzoSFSR)R zVH|T{Rtp-6EI7$ed#Abtxt00;wGg_<6Te(a>iB3V^F&ge{w4w%%C{emMLxDdUDBYb zt=QI& zXEi4V<4_WpYxSsCB{y^7L6BBXhn_@iA({Vrx}>jMMi_Zt++YZDg!qOJSb3uFbpqC3 zj>jJZ+TCG2Fm`3h6>Zy?10h}sXE7z zPiUm%ueP2wih|sYppK?!4>$^sYaIV|`VYO4JreFUPUBOv-Ug?SSd>!Ig1i7I^!qQX z#$zi?o~+Sbmwk61#DWi>j{0I)@DA}2i$ZX}`Tz!aG=VHWtC8*dV}*Lw8AK|{@`nu z2dBa!DS~8hujoKUCFA}<@7L4{qv#K!CygoM`Wh%~Sw8nA%q^A!AFq}9Y@%me3SkXg zDD5L2R`(>&P6l$QjHTG7&M37 zz|s#a=v?4o9rKaT^0MKdfBC04&{=Smv#c-$o)Zm)3sf294S_{LBbw;aTYA%a#}T5S zcch5xOTTt7(#^y<87|UcR0;*cCd3#s5Y0|7HWGt9JH2bF(OnOCzL)o86wCEol%GMh zOt}TZ#Kjb`V)|pvvOsGRiqi}YvnWUe=X|Sl&u`mA`L1oFAASdK?L7^onke6_w@VlG zD(#Sro#D9!q8%8-FPB>*?NVH5_aF=pRsF)RuE!qTIfdI;gw(Y4>$ zl-#Ox>tF29n}7jddrU7`*p&dEX(yt+=sY^a<}KAWe>lQ8zW1fgk=C;DBl9UE=_^wyR=+PN!@tOk?xh- zqqC-JTo@5-RTgzH(6Q;{;gj_vFnFzZD;ECha@$H>#8={ueeJc0EjZzP@JrCYh4-Mn!g-!i*w+`d?>IoYZA4j17#DWee{uG+VTTakn7SbC!f@^*Es8&QeA{XmDl^SXDYEs*1if3P(`=C?L$9VL zK}WlaZTCvD9rgv_(>_JZf0g9_j+Via{4`@gFUjb#O?DCWL*32n)|}pSZO~6)O1~m0WWI^=XkIfScHdP&(Pnj%YBeJ z7r%4m4daG`#JqZcM>N_uYZ+ndcEL3iV&x{ZTAq5k+;P2yL>253f@Dc%v(?wh+_=J958irOv3}QCMnckg}m0Nj2gcZki!%7f*F@Q@9c_^bp;sk{hEI%0Z(|TjI~N7=I;oQw<>g~D_WN(~ue};jIU`R; z!|b@bs90LSTk7Gx=h#m>Eg0hh!L~KLyf6s4wk9R&d!@PrlkkdKl!0+m78if%aj8%S zHp!H39y6Oz(Mb9%Kx0t6DHTih0CjL5Ewpidftrt^2|fpAf400o8V@S|V<$$oQm&N7 z6qhMO%T;Q%rP8*l7=57jfXyNK;sdC)YBfy4<&4?@bOPWabGfO`hz zeDg)Y#W}~s&oEZLDv!gM*SeLK=_koy9dRL5l~kWGYW0gn@r~3Y#%~5r@z{)Ia5|53+^;|*v=Rt2?N|xoiExsj+}xc~G43J?EjhNND0YWB!Q03dscmsR?H^{*;(JF3%A&tmw=m)m!f|8GMhD** zhE1_;BdZfzV8-Ube|dat307nd{_~KRReUmWyeFc|(zw&yi)~ zHdrjD)SH;B_oDI~6TGD24?R6S_5nrUvEEBPjs)bod?8eVEJnm5NQxgOJG*{jl2eKJY%rR^j2MqMSVdX*_R3rw ziQ&z*YyIO^X2m{Nx2o9c*MD&bQOV5&gdR#pQ)Ru9ZN3yC5)q|~LvN+A`xQ(vmt$2NzM(p6R zv?YxV#W1_zX7N+aH1Oa}DqwtJM0<>FgKme5EIv=$oBU@MZG$E$p>lSjy=eBnLy9&? zvQwp3Ud+6IY-xD@vK(PkJe3HKzYi5N2KpqLhz|~(apgj!-(arI2E$sfr- zM59dJWA8ro+81zi+#tsGYIb5BpmjNws1P3ii>;fkzm=kL%~x~W1wEn%5#BRdK!$bQ zWC~>U)@(LJ-qZ(kT6Y2TR}9L}$g*X>dx1lM8qp+f8K!!WQO1O@r82KTLk9=a}nR(xud0uifbls#?ZPqr8 z)-QlIB6YluElpa2y*}+qlRzmfq641m!M(W(8H-%pcnWJl_VHuRj&&=rIO5G4^iOtE z&3h1++E-P6KM*mZqX!+l>NGi`anL&~Sz#a3IIRncvHSMRkg!M^v)VqW0ML)O3Ob)c zAL4K6g5b8EYw}qgZ4lbzMkg@E9clTKQO1@IX{b%FN`?=lCsq5^ve|=s2J#SVp$EV# zi=P-pl!J`=cSyD@Z@la3Eu-Wol|NkniP62MUU(+%{+r??HxjLac4E%a z5cW%+KnA}Xmh~WFVV-EM7o<;|nCM5*0rH^3Afwo=Z9ItLNXX#Yt$YD|>!z!(Qdhmn zn+S$1Jv{Kms_(ag0&2hjqH)zlKo2**KozCI-eju2*lY9Iii|$32mBSFTF$R*3L2;g zv&Wn^M3G{1i&XYftrI9xdwF<)xgoxlV2(*0+fWE!v<))D(wb#!6(p>0l~5wMF0O(8 zzN~g|@NTW2U)BO8yR_EV-6J~y<`>j#Y>JxPgKj_}T-F4x*md8V%7Z}ShdV`^wJ2N0 z>QnLP(tXPFumFD%KW0OE(<(Qt`ZAm8H0?|x z1aW54LOMM%B{w5(P0`2h;AF06Gxd4hyUr|*ufIPRmc>xkn^TX!1xChSdD$>PFy42# zl`aQ-B?fJSMut7BHJ!=o!y+UH{~%2Oi0fF^ipER%GZyC8)vo`jFX|3^8dJIC@uAb{ z{~A=l-d9Auw|-bva`*Y@Wg^bPR`XCL8>hh7;NiKN+k?h*&t zEoYDh0$kD+$L@)2}ZZefyCI)ArDv@oL3tA z3x^pv9x`jsk!+DtlF|AwJjMD{E0%b+zpK!y#QY7d>`vo`$dF{ZGOW^s2=74Xo(Slj zJXexRb05mFPMVKJxeW;Cq%C*HhYwPro}0&7JOtU0 z@ne(S4*iPse?C?ef;g^9)n=8bg`<(r!&&h;BldQO0N@LEK>>0Nhy(vU{sc?2dXZmd zLomcjcHVwn(ghAAw<6O7tg6?pUg~!f4TL#Mo;^bNy7$Ee!zi` zhYGVk_XR(e)BlEI^~px0XsbtAf7*yrw@)>1Bk`CGelN&}OWAz zD3O^&w+>N!h@)S>AGnDM0_k8HHWp6}?FZ>>;C0sG(r3sGtKoprnlFE4uHJ&H z*&Ias>8vjok|E&SBSYAC0cJVPr0{w;)MwUpL>%Z0MIF#(RQ6 zJk(BT&|&X`^3==*guRulo3bv>VOox;dbIygq_NUlVc8iOVpV3`*j7v!vQo}lixy*i z;kJ|p+cN#?Fpx<9Y%Tv?km4DPL2GfbP38$C?n!qXtHvEFCOVCKEp#R0ot>moICyxk z^GqXjDUOXxJI01!wjE|T@Qzgh1pi9r8P0{CV3p~`SfmpF;_i70CEKK-HAJ|`)G`;D z5D2@C+D~47dE6UdOUUuM+mH60EW!ZPy*+KzyN(sSFMg&>w@wt%);$a#Mla!8;vBJo za_i0L0Z<+$yr*70?FRV*Bs4nfKzz!}Xx9gRV1P?b?TIe&=jow`eK<34FRq`*Lcr-mlA%ub$reu1Y*w`F7{k?EbypnJI+@yJW#s zhDF-QkgK3vCxGBME}?k8;t7dG!FaAF)hb^kHfPiQdFg%x>JWKDnJ7dS*T`eL9VL$* zmU`p-qzt3mZL%_-pT#P`Yr+2ohp;JJg#j_o@eTLDV#gT|vCqlcUg~!v>__OxKrhc0 zS*vtH&>5n{I&N*~J@XV;S{k)HR?lDq#7Ei-Y*olJwqs_D@%dvsvkTPX0_>xGf@^0I za&ynv1P8fFVlhO6Fl8mXfu(I(mr4?r*3b~Cf2^&2y{)IsrlILI~#9G z?0~(-6MjYg#zC-mFS@|NYR^PS$JlI~7-69+ULN776@=lJo{B}h(5A4hJ#}v1b#R0Od3K~Nr6_T>wHs&FFj9j;#iy*@{AF3H zIix`ua|4~;y#GPj+C8c8m3RV0}a0w_PG%C-=^{>8#P})`6nAQ z?O$xR@<28oT_T-;#zm<8M<@HT#Ych}_HF_*DuCHGl1Sy5ZKo3aT^0AE8;cWF+yKN+ z;9kRBkEP$-HEB>+vroY>qIR7VHJZX-v(O9M9(+v=^#Vu6zJ9*C(ryR)Lo*a^bhBs_ zI(++1(~nlb3gA^$?*>J^#NmRQHrQPsXZ;wf)4%o`EeeB^JTR%D`=8^sAR9-aD_@^x zZX%wkM$-}$K5>8+-FEP`Yv_)j4u=(XPZ1-l=4PE0Eb-3_$Qs{&82dqO(rdCeKPdnGS_hpX4 zDBlU58{xz1_f;4l)$k=dPPD2F(vu(VV96Oj^6!{?3u5$8!Qn3HA|TdDf%*vO#4EA1 z*p4Cy{-Wus7jF$*=@`CP*tEvFB5qF?&osXoELrUvkCwqM+|Hh|W@xA?+cM23^8qUm zV7iL{vrXeaD-cFn7q(8TmpUh!XxNU40O<_YacGQ|cwzHOH$Rtw=6-u$%h>dn>xCncWzGxhu>_=^r}8L6;rR&JwUI zmliaNY2ysC0{s=6^zhXWBo1=-*IaUE=WSDT3Y;Xe@VF)M0rs*c*K8T9jV*I~N2Z6u zlanvjRBz9k0u|4DNBH4B-BA)0wN3@ek`<;=mza?tkS-r7;FwJivpIsExLJe7TpU+9YB=?Q6;wYX_av&S*?8t;m_bgT9^exRX z2~&-PFD+necTeMuj62AyFq578sLI`ovL|hNX)v>dw=T166{@d58iv>5L*hB9&7A{| zi|Ob>&M3yBn@CcmmSS*IfUVq>#2G;c{4IU~R+aw#H}3=>bHK-c^LBvnc7rY@$Z38u zEMA1^m4;H*&8=453@)}r6QRs1Dy&$#+(On7lWHNgS{QYZeBNMJVsVl?T5i{LshtQJn+icDyR(pn{VSS{I%edku+RP0w@$O&0kQKS8)@8gES^K7mM^g#2v*q8a8T?V)JA)`_5aD9=$I(Z@WH_YFJsx4vq6j>@FfE zNJ-!XI3#c%oNz+S4~9YEMKoc_{*nt^u@`oAh4}20i>ZaIuchH&Ltsu_ya#;t$JU0O za;%le2=6qc#QsAsnfh7V$9y?86uDdYq&MVc)X+b9N27|ywpuKo<3;y(KQ+DoChS%7 z^R{NO&EhY0TISWgqwce>2t+Ri1Hp(d>{+DD{o3ymI##Dw#d2VcBDN`>#82cvX9#RO W^^qFsO9B%I(DgB)r1`^ROaB)ZvkbKW diff --git a/public/favicon.ico b/public/favicon.ico index 6ec8a8cdbc688bcd5191ae02b31bc66b1b447100..b4d8eac4318c0d7e5836a15837570394f065ef3a 100644 GIT binary patch literal 67646 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
  • FDxL%=$Ty&d0K38J1)tly$h%)kP;yTm%+_iIHy5ID8kM=UKA%!{hLC;BT0Bu}orTKXC#)7QG?Q^#^Cr6Hg#7;O=5` z-;B&9I!71wWewm(HuOO@X)~n(v*#)g_*a*lkLYY&=t;{z+nbP+7>B+-G%%wSm94 ziPs8sgtoJ%f%}w^y0z+M{#$tn5m3mvb%Is33 zFDSulG#ZP`%3^VMTG1z(v0Km{RhD67>^cPQu}5FqD(bxb!inD1+UUdICA16ZLKtjK zq5v~$rPd;t*WfOXNXXF$?9aDME-l4e|Fs1+;y!i%qXs*Y?U2*YxNiJC*+<&9o%;)z2HxdWS2@`>k5nrz8n0j z{LH`V_quWOmRP{u#WGW^$S&4@_fU$$bT~ux@iC zv&kX!LVvk?9|Y`--!h)TVz6 z9sEgrR>>dWZ~4R|;xYDh#ke1lr?HCk0^>Wyx*C~JV!!|E{&;k1UKsKB4A=*wcC_xm z&e%?!IVb%6Bf^0h;rK&1;;*v~_|DH2)DEss3#;q*;s15U_Vs7q{@c~Bb@8<>zV?By zec*9@;Ckf$4B%ns=aKL4I-iJq9X)?D^7Uh%cf;@hHU)PH>vl@-Y4#Un`r&X)Td!l`uxdM VK5*v9lb$RJpWi+iKL4Np{lAiE|D6B; diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..22c89badd93dfa60cc3c32a5507ec003f4c95a9d GIT binary patch literal 4291 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 LWMNpR?-KPtOe>Ox literal 0 HcmV?d00001 diff --git a/public/script.js b/public/script.js index 255e99f..111e566 100644 --- a/public/script.js +++ b/public/script.js @@ -89,18 +89,18 @@ document.addEventListener("DOMContentLoaded", () => { uploadContainer.className = "upload-status"; // Use the existing CSS class for styling uploadContainer.appendChild(uploadText); uploadContainer.appendChild(statusLink); - buttons.appendChild(copyButton) - buttons.appendChild(deleteButton) - uploadContainer.appendChild(buttons) + buttons.appendChild(copyButton) + buttons.appendChild(deleteButton) + uploadContainer.appendChild(buttons) uploadStatus.appendChild(uploadContainer); // Update upload text uploadText.innerHTML = "0%"; uploadText.className = "percent"; statusLink.className = "status"; - copyButton.className = "copy-button"; // Add class for styling + copyButton.className = "button copy-button"; // Add class for styling copyButton.innerHTML = "Copiar"; // Set button text - deleteButton.className = "delete-button"; + deleteButton.className = "button delete-button"; deleteButton.innerHTML = "Borrar"; copyButton.style.display = "none"; deleteButton.style.display = "none"; diff --git a/public/styles.css b/public/styles.css index 46d7799..3da918d 100644 --- a/public/styles.css +++ b/public/styles.css @@ -18,10 +18,7 @@ html { font-family: "FG"; - background-image: linear-gradient(to bottom, - rgba(11, 11, 11, 0.92), - rgba(11, 11, 11, 0.92)), - url(./bliss-small.avif); + background: #111111; background-attachment: fixed; background-repeat: no-repeat; background-size: cover; @@ -29,8 +26,6 @@ html { body { - /* font-family: Arial, sans-serif; */ - /* background-color: #111; */ margin: 0; padding: 20px; } @@ -53,11 +48,11 @@ h1 { a { text-decoration: none; + color: #ffb6c1 } .bottom { font-size: 0.9em; - /* margin-top: 1ch;*/ flex: 1; text-align: center; } @@ -73,31 +68,25 @@ a { .container { max-width: 800px; margin: auto; - /* background: white; */ - /*! padding: 20px; */ border-radius: 0px; - /*! box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); */ } +.container>img { + max-width: 100%; + max-height: 500px +} #drop-area { - /*! border: 2px solid #00ff00; */ - /*! border-radius: 6px; */ - /*! padding-left: 10px; */ - /*! padding-right: 10px; */ text-align: center; position: relative; width: fit-content; margin: 0 auto; - /* Center the element */ display: block; - /* Ensure it behaves as a block-level element */ background: rgba(202, 230, 190, .75); border: 1px solid #b7d1a0; border-radius: 4px; color: #468847; cursor: pointer; - /*! display: inline-block; */ font-size: 24px; padding: 28px 48px; text-shadow: 0 1px hsla(0, 0%, 100%, .5); @@ -107,11 +96,9 @@ a { .button { display: inline-block; padding: 10px 20px; - /* background: #; */ color: white; border-radius: 5px; cursor: pointer; - /* margin-top: 10px; */ } .upload-status { @@ -128,101 +115,59 @@ nav>ul { #upload-status { margin: 20px; - /* Adjust as needed */ } .upload-status { display: flex; align-items: center; justify-content: space-between; - border: 2px solid #999; - /* Optional styling for the status box */ + border: 1px solid #ffffff; padding: 5px; - /* Optional padding */ - /*! border-radius: 6px; */ - /* Optional rounded corners */ - /*! background-color: #f9f9f9; */ - /* Optional background color */ } .link-container { display: flex; align-items: center; margin-left: auto; - /* Pushes the link and button to the right */ } .link { color: #ffb6c1; text-decoration: none; - /* Remove underline from link */ margin-right: 5px; - /* Space between link and button */ } .link:hover { text-decoration: underline; - /* Optional: underline on hover */ +} + +.button { + display: inline; + color: rgb(255, 255, 255); + border: none; + border-radius: 3px; + padding: 5px 10px; + cursor: pointer; + font-weight: bold; } .copy-button { - display: inline; - background-color: #7a6fff; - /* Button background color */ - color: white; - /* Button text color */ - border: none; - /* Remove border */ - border-radius: 3px; - /* Rounded corners for the button */ - padding: 5px 10px; - /* Button padding */ - cursor: pointer; - /* Pointer cursor on hover */ - font-weight: bold; + background-color: #1e8a1a; } .delete-button { - display: inline; - background-color: #ff6f6f; - /* Button background color */ - color: white; - /* Button text color */ - border: none; - /* Remove border */ - border-radius: 3px; - /* Rounded corners for the button */ - padding: 5px 10px; - /* Button padding */ - cursor: pointer; - /* Pointer cursor on hover */ - margin-left: 6px; - font-weight: bold; + background-color: #b83434; + margin-left: 6px } - .copy-button:hover { - background-color: #6057ce; - /* Darker shade on hover */ + background-color: #156412; } .delete-button:hover { - background-color: #ce5757; - /* Darker shade on hover */ + background-color: #912a2a; } .status { color: rgb(255, 132, 0); -} - -a:link { - color: #ffb6c1 -} - -a:visited { - color: #ffb6c1 -} - -a:hover { - color: #ffb6c1 } \ No newline at end of file diff --git a/src/config.cr b/src/config.cr index 1fbd8ce..eccdb4e 100644 --- a/src/config.cr +++ b/src/config.cr @@ -2,36 +2,43 @@ require "yaml" class Config include YAML::Serializable - # Colorize logs property colorize_logs : Bool = true + # Log level + property log_level : LogLevel = LogLevel::Info + + # Port on which the uploader will bind + property port : Int32 = 8080 + # IP address on which the uploader will bind + property host : String = "127.0.0.1" + # Where the uploaded files will be located property files : String = "./files" # Where the thumbnails will be located when they are successfully generated property thumbnails : String = "./thumbnails" # Generate thumbnails for OpenGraph compatible platforms like Chatterino # Whatsapp, Facebook, Discord, etc. - property generateThumbnails : Bool = false + property generate_thumbnails : Bool = false # Where the SQLITE3 database will be located property db : String = "./db.sqlite3" + # Enable or disable the admin API property adminEnabled : Bool = false # The API key for admin routes. It's passed as a "X-Api-Key" header to the # request property adminApiKey : String? = "" + # Not implemented - property incrementalFileameLength : Bool = true + property incrementalfilename_length : Bool = true # Filename length - property fileameLength : Int32 = 3 + property filename_length : Int32 = 3 # In MiB property size_limit : Int16 = 512 - # Port on which the uploader will bind - property port : Int32 = 8080 - # IP address on which the uploader will bind - property host : String = "127.0.0.1" + # A file path where do you want to place a unix socket (THIS WILL DISABLE ACCESS # BY IP ADDRESS) property unix_socket : String? + # True if you want this program to block IP addresses coming from the Tor # network property blockTorAddresses : Bool? = false @@ -39,40 +46,42 @@ class Config property torExitNodesCheck : Int32 = 3600 # Only https://check.torproject.org/exit-addresses is supported property torExitNodesUrl : String = "https://check.torproject.org/exit-addresses" - # Where the file of the exit nodes will be located, can be placed anywhere - property torExitNodesFile : String = "./torexitnodes.txt" # Message that will be displayed to the Tor user. # It will be shown on the Frontend and shown in the error 401 when a user # tries to upload a file using curl or any other tool property torMessage : String? = "Tor is blocked!" + # How many files an IP address can upload to the server property filesPerIP : Int32 = 32 # How often is the file limit per IP reset? (in seconds) property rateLimitPeriod : Int32 = 600 # TODO: UNUSED CONSTANT property rateLimitMessage : String = "" + # Delete the files after how many days? - property deleteFilesAfter : Int32 = 7 + property deleteFilesAfter : Int32 = 14 # How often should the check of old files be performed? (in seconds) property deleteFilesCheck : Int32 = 1800 # The lenght of the delete key - property deleteKeyLength : Int32 = 4 + property deleteKeyLength : Int32 = 6 + property siteInfo : String = "xd" # TODO: UNUSED CONSTANT property siteWarning : String? = "" - # Log level - property log_level : LogLevel = LogLevel::Info + # Blocked extensions that are not allowed to be uploaded to the server property blockedExtensions : Array(String) = [] of String + # A list of OpenGraph user agents. If the request contains one of those User # agents when trying to retrieve a file from the server; the server will # reply with an HTML with OpenGraph tags, pointing to the media thumbnail # (if it was generated successfully) and the name of the file as title property opengraphUseragents : Array(String) = [] of String + # Since this program detects the Host header of the client it can be used # with multiple domains. You can display the domains in the frontend # and in `/api/stats` - property alternativeDomains : Array(String) = [] of String + property alternative_domains : Array(String) = [] of String def self.load config_file = "config/config.yml" @@ -83,16 +92,16 @@ class Config end def self.check_config(config : Config) - if config.fileameLength <= 0 - puts "Config: fileameLength cannot be #{config.fileameLength}" + if config.filename_length <= 0 + puts "Config: filename_length cannot be less or equal to 0" exit(1) end - if config.files.ends_with?('/') + if config.files.ends_with?('/') config.files = config.files.chomp('/') end - if config.thumbnails.ends_with?('/') - config.thumbnails = config.thumbnails.chomp('/') - end + if config.thumbnails.ends_with?('/') + config.thumbnails = config.thumbnails.chomp('/') + end end end diff --git a/src/database/files.cr b/src/database/files.cr new file mode 100644 index 0000000..3b15bd3 --- /dev/null +++ b/src/database/files.cr @@ -0,0 +1,83 @@ +module Database::Files + extend self + + # ------------------- + # Insert / Delete + # ------------------- + + def insert(file : UFile) : Nil + request = <<-SQL + INSERT INTO files + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT DO NOTHING + SQL + + SQL.exec(request, *file.to_tuple) + end + + def delete(filename : String) : Nil + request = <<-SQL + DELETE + FROM files + WHERE filename = ? + SQL + + SQL.exec(request, filename) + end + + def delete_with_key(key : String) : Nil + request = <<-SQL + DELETE FROM files + WHERE delete_key = ? + SQL + + SQL.exec(request, key) + end + + # ------------------- + # Select + # ------------------- + + def select(filename : String) : UFile? + request = <<-SQL + SELECT * + FROM files + WHERE filename = ? + SQL + + SQL.query_one?(request, filename, as: UFile) + end + + def select_with_key(delete_key : String) : UFile? + request = <<-SQL + SELECT * + FROM files + WHERE delete_key = ? + SQL + + SQL.query_one?(request, delete_key, as: UFile) + end + + # ------------------- + # Misc + # ------------------- + + def old_files : Array(UFile) + request = <<-SQL + SELECT filename, extension, thumbnail + FROM files + WHERE uploaded_at < strftime('%s', 'now') - #{CONFIG.deleteFilesAfter * 3600} + SQL + + SQL.query_all(request, as: UFile) + end + + def file_count : Int32 + request = <<-SQL + SELECT COUNT (filename) + FROM files + SQL + + SQL.query_one(request, as: Int32) + end +end diff --git a/src/database/ip.cr b/src/database/ip.cr new file mode 100644 index 0000000..c818185 --- /dev/null +++ b/src/database/ip.cr @@ -0,0 +1,16 @@ +module Database::IP + # ------------------- + # Insert / Delete + # ------------------- + + def insert(ip : IP) : Nil + request = <<-SQL + INSERT OR IGNORE + INTO ips (ip, date) + VALUES ($1, $2) + ON CONFLICT DO NOTHING + SQL + + SQL.exec(request, *ip.to_tuple) + end +end diff --git a/src/file-uploader-crystal.cr b/src/file-uploader-crystal.cr index 850c4a2..b333235 100644 --- a/src/file-uploader-crystal.cr +++ b/src/file-uploader-crystal.cr @@ -7,11 +7,12 @@ require "digest" require "./logger" require "./routing" -require "./utils" -require "./handling/**" require "./config" require "./jobs" -require "./lib/**" +require "./utils/*" +require "./lib/*" +require "./types/*" +require "./database/*" CONFIG = Config.load Kemal.config.port = CONFIG.port @@ -21,7 +22,7 @@ Kemal.config.app_name = "file-uploader-crystal" # https://github.com/iv-org/invidious/blob/90e94d4e6cc126a8b7a091d12d7a5556bfe369d5/src/invidious.cr#L136C1-L136C61 LOGGER = LogHandler.new(STDOUT, CONFIG.log_level, CONFIG.colorize_logs) # Give me a 128 bit CPU -# MAX_FILES = 58**CONFIG.fileameLength +# MAX_FILES = 58**CONFIG.filename_length SQL = DB.open("sqlite3://#{CONFIG.db}") # https://github.com/iv-org/invidious/blob/90e94d4e6cc126a8b7a091d12d7a5556bfe369d5/src/invidious.cr#L78 diff --git a/src/handling/handling.cr b/src/handling/handling.cr deleted file mode 100644 index be6f554..0000000 --- a/src/handling/handling.cr +++ /dev/null @@ -1,401 +0,0 @@ -require "../http-errors" -require "http/client" -require "benchmark" - -# require "../filters" - -module Handling - extend self - - def upload(env) - env.response.content_type = "application/json" - ip_address = Utils.ip_address(env) - protocol = Utils.protocol(env) - host = Utils.host(env) - # filter = env.params.query["filter"]? - # You can modify this if you want to allow files smaller than 1MiB. - # This is generally a good way to check the filesize but there is a better way to do it - # which is inspecting the file directly (If I'm not wrong). - if CONFIG.size_limit > 0 - if env.request.headers["Content-Length"].to_i > 1048576*CONFIG.size_limit - return http_error 413, "File is too big. The maximum size allowed is #{CONFIG.size_limit}MiB" - end - end - filename = "" - extension = "" - original_filename = "" - uploaded_at = "" - checksum = "" - if CONFIG.deleteKeyLength > 0 - delete_key = Random.base58(CONFIG.deleteKeyLength) - end - # TODO: Return the file that matches a checksum inside the database - HTTP::FormData.parse(env.request) do |upload| - if upload.filename.nil? || upload.filename.to_s.empty? - LOGGER.debug "No file provided by the user" - return http_error 403, "No file provided" - end - # TODO: upload.body is emptied when is copied or read - # Utils.check_duplicate(upload.dup) - extension = File.extname("#{upload.filename}") - if CONFIG.blockedExtensions.includes?(extension.split(".")[1]) - return http_error 401, "Extension '#{extension}' is not allowed" - end - filename = Utils.generate_filename - file_path = "#{CONFIG.files}/#{filename}#{extension}" - File.open(file_path, "w") do |output| - IO.copy(upload.body, output) - end - original_filename = upload.filename - uploaded_at = Time.utc - checksum = Utils.hash_file(file_path) - # TODO: Apply filters - # if filter - # Filters.apply_filter(file_path, filter) - # end - end - # X-Forwarded-For if behind a reverse proxy and the header is set in the reverse - # proxy configuration. - begin - spawn { Utils.generate_thumbnail(filename, extension) } - rescue ex - LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}" - end - begin - # Insert SQL data just before returning the upload information - SQL.exec "INSERT INTO files VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil - SQL.exec "INSERT OR IGNORE INTO ips (ip, date) VALUES (?, ?)", ip_address, Time.utc.to_unix - # SQL.exec "INSERT OR IGNORE INTO ips (ip) VALUES ('#{ip_address}')" - SQL.exec "UPDATE ips SET count = count + 1 WHERE ip = ('#{ip_address}')" - rescue ex - LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}" - return http_error 500, "An error ocurred when trying to insert the data into the DB" - end - json = JSON.build do |j| - j.object do - j.field "link", "#{protocol}://#{host}/#{filename}" - j.field "linkExt", "#{protocol}://#{host}/#{filename}#{extension}" - j.field "id", filename - j.field "ext", extension - j.field "name", original_filename - j.field "checksum", checksum - if CONFIG.deleteKeyLength > 0 - j.field "deleteKey", delete_key - j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{delete_key}" - end - end - end - json - end - - # The most unoptimized and unstable feature lol - def upload_url_bulk(env) - env.response.content_type = "application/json" - ip_address = Utils.ip_address(env) - protocol = Utils.protocol(env) - host = Utils.host(env) - begin - files = env.params.json["files"].as((Array(JSON::Any))) - rescue ex : JSON::ParseException - LOGGER.error "Body malformed: #{ex.message}" - return http_error 400, "Body malformed: #{ex.message}" - rescue ex - LOGGER.error "Unknown error: #{ex.message}" - return http_error 500, "Unknown error" - end - successfull_files = [] of NamedTuple(filename: String, extension: String, original_filename: String, checksum: String, delete_key: String | Nil) - failed_files = [] of String - # X-Forwarded-For if behind a reverse proxy and the header is set in the reverse - # proxy configuration. - files.each do |url| - url = url.to_s - filename = Utils.generate_filename - original_filename = "" - extension = "" - checksum = "" - uploaded_at = Time.utc - extension = File.extname(URI.parse(url).path) - if CONFIG.deleteKeyLength > 0 - delete_key = Random.base58(CONFIG.deleteKeyLength) - end - file_path = "#{CONFIG.files}/#{filename}#{extension}" - File.open(file_path, "w") do |output| - begin - HTTP::Client.get(url) do |res| - IO.copy(res.body_io, output) - end - rescue ex - LOGGER.debug "Failed to download file '#{url}': #{ex.message}" - return http_error 403, "Failed to download file '#{url}'" - failed_files << url - end - end - # successfull_files << url - # end - if extension.empty? - extension = Utils.detect_extension(file_path) - File.rename(file_path, file_path + extension) - file_path = "#{CONFIG.files}/#{filename}#{extension}" - end - # The second one is faster and it uses less memory - # original_filename = URI.parse("https://ayaya.beauty/PqC").path.split("/").last - original_filename = url.split("/").last - checksum = Utils.hash_file(file_path) - begin - spawn { Utils.generate_thumbnail(filename, extension) } - rescue ex - LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}" - end - begin - # Insert SQL data just before returning the upload information - SQL.exec("INSERT INTO files VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil) - successfull_files << {filename: filename, - original_filename: original_filename, - extension: extension, - delete_key: delete_key, - checksum: checksum} - rescue ex - LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}" - return http_error 500, "An error ocurred when trying to insert the data into the DB" - end - end - json = JSON.build do |j| - j.array do - successfull_files.each do |fileinfo| - j.object do - j.field "link", "#{protocol}://#{host}/#{fileinfo[:filename]}" - j.field "linkExt", "#{protocol}://#{host}/#{fileinfo[:filename]}#{fileinfo[:extension]}" - j.field "id", fileinfo[:filename] - j.field "ext", fileinfo[:extension] - j.field "name", fileinfo[:original_filename] - j.field "checksum", fileinfo[:checksum] - if CONFIG.deleteKeyLength > 0 - delete_key = Random.base58(CONFIG.deleteKeyLength) - j.field "deleteKey", fileinfo[:delete_key] - j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{fileinfo[:delete_key]}" - end - end - end - end - end - json - end - - def upload_url(env) - env.response.content_type = "application/json" - ip_address = Utils.ip_address(env) - protocol = Utils.protocol(env) - host = Utils.host(env) - url = env.params.query["url"] - successfull_files = [] of NamedTuple(filename: String, extension: String, original_filename: String, checksum: String, delete_key: String | Nil) - failed_files = [] of String - # X-Forwarded-For if behind a reverse proxy and the header is set in the reverse - # proxy configuration. - filename = Utils.generate_filename - original_filename = "" - extension = "" - checksum = "" - uploaded_at = Time.utc - extension = File.extname(URI.parse(url).path) - if CONFIG.deleteKeyLength > 0 - delete_key = Random.base58(CONFIG.deleteKeyLength) - end - file_path = "#{CONFIG.files}/#{filename}#{extension}" - File.open(file_path, "w") do |output| - begin - # TODO: Connect timeout to prevent possible Denial of Service to the external website spamming requests - # https://crystal-lang.org/api/1.13.2/HTTP/Client.html#connect_timeout - HTTP::Client.get(url) do |res| - IO.copy(res.body_io, output) - end - rescue ex - LOGGER.debug "Failed to download file '#{url}': #{ex.message}" - return http_error 403, "Failed to download file '#{url}': #{ex.message}" - failed_files << url - end - end - if extension.empty? - extension = Utils.detect_extension(file_path) - File.rename(file_path, file_path + extension) - file_path = "#{CONFIG.files}/#{filename}#{extension}" - end - # The second one is faster and it uses less memory - # original_filename = URI.parse("https://ayaya.beauty/PqC").path.split("/").last - original_filename = url.split("/").last - checksum = Utils.hash_file(file_path) - begin - spawn { Utils.generate_thumbnail(filename, extension) } - rescue ex - LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}" - end - begin - # Insert SQL data just before returning the upload information - SQL.exec("INSERT INTO files VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil) - successfull_files << {filename: filename, - original_filename: original_filename, - extension: extension, - delete_key: delete_key, - checksum: checksum} - rescue ex - LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}" - return http_error 500, "An error ocurred when trying to insert the data into the DB" - end - json = JSON.build do |j| - j.array do - successfull_files.each do |fileinfo| - j.object do - j.field "link", "#{protocol}://#{host}/#{fileinfo[:filename]}" - j.field "linkExt", "#{protocol}://#{host}/#{fileinfo[:filename]}#{fileinfo[:extension]}" - j.field "id", fileinfo[:filename] - j.field "ext", fileinfo[:extension] - j.field "name", fileinfo[:original_filename] - j.field "checksum", fileinfo[:checksum] - if CONFIG.deleteKeyLength > 0 - delete_key = Random.base58(CONFIG.deleteKeyLength) - j.field "deleteKey", fileinfo[:delete_key] - j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{fileinfo[:delete_key]}" - end - end - end - end - end - json - end - - def retrieve_file(env) - protocol = Utils.protocol(env) - host = Utils.host(env) - begin - fileinfo = SQL.query_one?("SELECT filename, original_filename, uploaded_at, extension, checksum, thumbnail - FROM files - WHERE filename = ?", - env.params.url["filename"].split(".").first, - as: {filename: String, ofilename: String, up_at: String, ext: String, checksum: String, thumbnail: String | Nil}) - if fileinfo.nil? - # TODO: Switch this to 404, if I use 404, it will use the kemal error page (ANOYING!) - return http_error 418, "File '#{env.params.url["filename"]}' does not exist" - end - rescue ex - LOGGER.debug "Error when retrieving file '#{env.params.url["filename"]}': #{ex.message}" - return http_error 500, "Error when retrieving file '#{env.params.url["filename"]}'" - end - env.response.headers["Content-Disposition"] = "inline; filename*=UTF-8''#{fileinfo[:ofilename]}" - # env.response.headers["Last-Modified"] = "#{fileinfo[:up_at]}" - env.response.headers["ETag"] = "#{fileinfo[:checksum]}" - - CONFIG.opengraphUseragents.each do |useragent| - if env.request.headers.try &.["User-Agent"].includes?(useragent) - env.response.content_type = "text/html" - return %( - - - - - - - #{if fileinfo[:thumbnail] - %() - end} - - -) - end - end - send_file env, "#{CONFIG.files}/#{fileinfo[:filename]}#{fileinfo[:ext]}" - end - - def retrieve_thumbnail(env) - begin - send_file env, "#{CONFIG.thumbnails}/#{env.params.url["thumbnail"]}" - rescue ex - LOGGER.debug "Thumbnail '#{env.params.url["thumbnail"]}' does not exist: #{ex.message}" - return http_error 403, "Thumbnail '#{env.params.url["thumbnail"]}' does not exist" - end - end - - def stats(env) - env.response.content_type = "application/json" - begin - json_data = JSON.build do |json| - json.object do - json.field "stats" do - json.object do - json.field "filesHosted", SQL.query_one? "SELECT COUNT (filename) FROM files", as: Int32 - json.field "maxUploadSize", CONFIG.size_limit - json.field "thumbnailGeneration", CONFIG.generateThumbnails - json.field "filenameLength", CONFIG.fileameLength - json.field "alternativeDomains", CONFIG.alternativeDomains - end - end - end - end - rescue ex - LOGGER.error "Unknown error: #{ex.message}" - return http_error 500, "Unknown error" - end - json_data - end - - def delete_file(env) - if SQL.query_one "SELECT EXISTS(SELECT 1 FROM files WHERE delete_key = ?)", env.params.query["key"], as: Bool - begin - fileinfo = SQL.query_all("SELECT filename, extension, thumbnail - FROM files - WHERE delete_key = ?", - env.params.query["key"], - as: {filename: String, extension: String, thumbnail: String | Nil})[0] - - # Delete file - File.delete("#{CONFIG.files}/#{fileinfo[:filename]}#{fileinfo[:extension]}") - if fileinfo[:thumbnail] - # Delete thumbnail - File.delete("#{CONFIG.thumbnails}/#{fileinfo[:thumbnail]}") - end - # Delete entry from db - SQL.exec "DELETE FROM files WHERE delete_key = ?", env.params.query["key"] - LOGGER.debug "File '#{fileinfo[:filename]}' was deleted using key '#{env.params.query["key"]}'}" - return msg("File '#{fileinfo[:filename]}' deleted successfully") - rescue ex - LOGGER.error("Unknown error: #{ex.message}") - return http_error 500, "Unknown error" - end - else - LOGGER.debug "Key '#{env.params.query["key"]}' does not exist" - return http_error 401, "Delete key '#{env.params.query["key"]}' does not exist. No files were deleted" - end - end - - def sharex_config(env) - host = Utils.host(env) - protocol = Utils.protocol(env) - env.response.content_type = "application/json" - # So it's able to download the file instead of displaying it - env.response.headers["Content-Disposition"] = "attachment; filename=\"#{host}.sxcu\"" - return %({ - "Version": "14.0.1", - "DestinationType": "ImageUploader, FileUploader", - "RequestMethod": "POST", - "RequestURL": "#{protocol}://#{host}/upload", - "Body": "MultipartFormData", - "FileFormName": "file", - "URL": "{json:link}", - "DeletionURL": "{json:deleteLink}", - "ErrorMessage": "{json:error}" -}) - end - - def chatterino_config(env) - host = Utils.host(env) - protocol = Utils.protocol(env) - env.response.content_type = "application/json" - return %({ - "requestUrl": "#{protocol}://#{host}/upload", - "formField": "data", - "imageLink": "{link}", - "deleteLink": "{deleteLink}" - }) - end -end diff --git a/src/http-errors.cr b/src/http-errors.cr deleted file mode 100644 index 27371d8..0000000 --- a/src/http-errors.cr +++ /dev/null @@ -1,12 +0,0 @@ -macro http_error(status_code, message) - env.response.content_type = "application/json" - env.response.status_code = {{status_code}} - error_message = {"error" => {{message}}}.to_json - error_message -end - -macro msg(message) - env.response.content_type = "application/json" - msg = {"message" => {{message}}}.to_json - msg -end diff --git a/src/jobs.cr b/src/jobs.cr index ab47716..c77c5cb 100644 --- a/src/jobs.cr +++ b/src/jobs.cr @@ -20,9 +20,7 @@ module Jobs LOGGER.info("Blocking Tor exit nodes") spawn do loop do - Utils.retrieve_tor_exit_nodes - # Updates the @@exit_nodes array instantly - Routing.reload_exit_nodes + Utils::Tor.refresh_exit_nodes sleep CONFIG.torExitNodesCheck.seconds end end diff --git a/src/logger.cr b/src/logger.cr index b613acb..39378b1 100644 --- a/src/logger.cr +++ b/src/logger.cr @@ -25,15 +25,6 @@ class LogHandler < Kemal::BaseLogHandler # Default: full path with parameters requested_url = context.request.resource - # Try not to log search queries passed as GET parameters during normal use - # (They will still be logged if log level is 'Debug' or 'Trace') - if @level > LogLevel::Debug && ( - requested_url.downcase.includes?("search") || requested_url.downcase.includes?("q=") - ) - # Log only the path - requested_url = context.request.path - end - info("#{context.response.status_code} #{context.request.method} #{requested_url} #{elapsed_text}") context diff --git a/src/macros.cr b/src/macros.cr new file mode 100644 index 0000000..972473f --- /dev/null +++ b/src/macros.cr @@ -0,0 +1,26 @@ +macro ee(status_code, message) + env.response.content_type = "application/json" + env.response.status_code = {{status_code}} + msg = {"error" => {{message}}}.to_json + return msg +end + +macro msg(message) + env.response.content_type = "application/json" + msg = {"message" => {{message}}}.to_json + return msg +end + +module Headers + macro host + env.request.headers["X-Forwarded-Host"]? + end + + macro scheme + env.request.headers["X-Forwarded-Proto"]? + end + + macro ip_addr + env.request.headers["X-Real-IP"]? + end +end diff --git a/src/handling/admin.cr b/src/routes/admin.cr similarity index 97% rename from src/handling/admin.cr rename to src/routes/admin.cr index b765386..fa66d2b 100644 --- a/src/handling/admin.cr +++ b/src/routes/admin.cr @@ -1,6 +1,4 @@ -require "../http-errors" - -module Handling::Admin +module Routes::Admin extend self # private macro json_fill(named_tuple, field_name) @@ -37,7 +35,7 @@ module Handling::Admin failed_files << file rescue ex LOGGER.error "Unknown error: #{ex.message}" - http_error 500,"Unknown error: #{ex.message}" + http_error 500, "Unknown error: #{ex.message}" end end json = JSON.build do |j| @@ -69,7 +67,7 @@ module Handling::Admin failed << item rescue ex LOGGER.error "Unknown error: #{ex.message}" - http_error 500, "Unknown error: #{ex.message}" + Macros.ee 500, "Unknown error: #{ex.message}" end end json = JSON.build do |j| @@ -107,7 +105,7 @@ module Handling::Admin failed << item rescue ex LOGGER.error "Unknown error: #{ex.message}" - http_error 500,"Unknown error: #{ex.message}" + Macros.ee 500, "Unknown error: #{ex.message}" end end json = JSON.build do |j| diff --git a/src/routes/delete.cr b/src/routes/delete.cr new file mode 100644 index 0000000..2467174 --- /dev/null +++ b/src/routes/delete.cr @@ -0,0 +1,39 @@ +module Routes::Deletion + extend self + + def delete_file(env) + key = env.params.query["key"]? + + if !key || key.empty? + ee 400, "No delete key suplied" + end + + file = Database::Files.select_with_key(key) + + if file + full_filename = file.filename + file.extension + thumbnail = file.thumbnail + + begin + # Delete file + File.delete("#{CONFIG.files}/#{full_filename}") + + if file.thumbnail + File.delete("#{CONFIG.thumbnails}/#{thumbnail}") + end + + # Delete entry from db + Database::Files.delete_with_key(key) + + LOGGER.debug "File '#{full_filename}' was deleted using key '#{key}'}" + msg("File '#{full_filename}' deleted successfully") + rescue ex + LOGGER.error("Unknown error: #{ex.message}") + ee 500, "Unknown error" + end + else + LOGGER.debug "Key '#{env.params.query["key"]}' does not exist" + ee 401, "Delete key '#{env.params.query["key"]}' does not exist. No files were deleted" + end + end +end diff --git a/src/routes/misc.cr b/src/routes/misc.cr new file mode 100644 index 0000000..6af5306 --- /dev/null +++ b/src/routes/misc.cr @@ -0,0 +1,66 @@ +require "http/client" + +module Routing::Misc + extend self + + struct Stats + include JSON::Serializable + + @[JSON::Field(key: "filesHosted")] + property files_hosted : Int32 + @[JSON::Field(key: "maxUploadSize")] + property max_upload_size : String + @[JSON::Field(key: "thumbnailGeneration")] + property thumbnail_generation : Bool + @[JSON::Field(key: "filenameLength")] + property filename_length : Int32 + @[JSON::Field(key: "alternativeDomains")] + property alternative_domains : Array(String) + + def initialize + @files_hosted = SQL.query_one("SELECT COUNT (filename) FROM files", as: Int32) + @max_upload_size = CONFIG.size_limit.to_s + @thumbnail_generation = CONFIG.generate_thumbnails + @filename_length = CONFIG.filename_length + @alternative_domains = CONFIG.alternative_domains + end + end + + def stats(env) + env.response.content_type = "application/json" + Stats.new.to_json + end + + def sharex_config(env) + host = env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]? + scheme = env.request.headers["X-Forwarded-Proto"]? || "http" + env.response.content_type = "application/json" + # So it's able to download the file instead of displaying it + env.response.headers["Content-Disposition"] = "attachment; filename=\"#{host}.sxcu\"" + + return %({ +"Version": "14.0.1", +"DestinationType": "ImageUploader, FileUploader", +"RequestMethod": "POST", +"RequestURL": "#{scheme}://#{host}/upload", +"Body": "MultipartFormData", +"FileFormName": "file", +"URL": "{json:link}", +"DeletionURL": "{json:deleteLink}", +"ErrorMessage": "{json:error}" +}) + end + + def chatterino_config(env) + host = env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]? + scheme = env.request.headers["X-Forwarded-Proto"]? || "http" + env.response.content_type = "application/json" + + return %({ +"requestUrl": "#{scheme}://#{host}/upload", +formField": "data", +imageLink": "{link}", +deleteLink": "{deleteLink}" +}) + end +end diff --git a/src/routes/retrieve.cr b/src/routes/retrieve.cr new file mode 100644 index 0000000..144c8a4 --- /dev/null +++ b/src/routes/retrieve.cr @@ -0,0 +1,56 @@ +module Routes::Retrieve + extend self + + def retrieve_file(env) + host = env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]? + scheme = env.request.headers["X-Forwarded-Proto"]? || "http" + ip_addr = env.request.headers["X-Real-IP"]? || env.request.remote_address + filename = env.params.url["filename"].split(".").first + + begin + file = Database::Files.select(filename) + if file.nil? + ee 404, "File '#{filename}' does not exist" + end + rescue ex + LOGGER.debug "Error when retrieving file '#{filename}': #{ex.message}" + ee 500, "Error when retrieving file '#{filename}'" + end + + env.response.headers["Content-Disposition"] = "inline; filename*=UTF-8''#{file.original_filename}" + env.response.headers["ETag"] = "#{file.checksum}" + + CONFIG.opengraphUseragents.each do |useragent| + env.response.content_type = "text/html" + + if env.request.headers.["User-Agent"]?.try &.includes?(useragent) + return %( + + + + + + + #{if file.thumbnail + %() + end} + + +) + end + end + send_file env, "#{CONFIG.files}/#{file.filename}#{file.extension}" + end + + def retrieve_thumbnail(env) + thumbnail = env.params.url["thumbnail"]? + pp "#{CONFIG.thumbnails}/#{thumbnail}" + + begin + send_file env, "#{CONFIG.thumbnails}/#{thumbnail}" + rescue ex + LOGGER.debug "Thumbnail '#{thumbnail}' does not exist: #{ex.message}" + ee 403, "Thumbnail '#{thumbnail}' does not exist" + end + end +end diff --git a/src/routes/upload.cr b/src/routes/upload.cr new file mode 100644 index 0000000..c5de027 --- /dev/null +++ b/src/routes/upload.cr @@ -0,0 +1,279 @@ +module Routes::Upload + extend self + + struct Response + include JSON::Serializable + + property link : String + @[JSON::Field(key: "linkExt")] + property link_ext : String + property id : String + property ext : String + property name : String + property checksum : String + @[JSON::Field(key: "deleteKey")] + property delete_key : String + @[JSON::Field(key: "deleteLink")] + property delete_link : String + + def initialize(file : UFile, scheme : String, host : String?) + @link = "#{scheme}://#{host}/#{file.filename}" + @link_ext = "#{scheme}://#{host}/#{file.filename}#{file.extension}" + @id = file.filename + @ext = file.extension + @name = file.original_filename + @checksum = file.checksum + @delete_key = file.delete_key + @delete_link = "#{scheme}://#{host}/delete?key=#{file.delete_key}" + end + end + + def upload(env) + host = env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]? + scheme = env.request.headers["X-Forwarded-Proto"]? || "http" + ip_addr = env.request.headers["X-Real-IP"]? || env.request.remote_address + env.response.content_type = "application/json" + + # You can modify this if you want to allow files smaller than 1MiB. + # This is generally a good way to check the filesize but there is a better way to do it + # which is inspecting the file directly (If I'm not wrong). + if CONFIG.size_limit > 0 + if !env.request.headers["Content-Length"]?.try &.to_i == nil + if env.request.headers["Content-Length"].to_i > 1048576*CONFIG.size_limit + ee 413, "File is too big. The maximum size allowed is #{CONFIG.size_limit}MiB" + end + end + end + + file = UFile.new + + HTTP::FormData.parse(env.request) do |upload| + upload_filename = upload.filename + + if upload_filename + file.original_filename = upload_filename + else + LOGGER.debug "No file provided by the user" + ee 403, "No file provided" + end + + file.extension = File.extname("#{upload.filename}") + file.filename = Utils.generate_filename + full_filename = file.filename + file.extension + file_path = "#{CONFIG.files}/#{full_filename}" + + if CONFIG.blockedExtensions.includes?(file.extension.split(".")[1]) + ee 401, "Extension '#{file.extension}' is not allowed" + end + + File.open(file_path, "w") do |output| + IO.copy(upload.body, output) + end + + file.uploaded_at = Time.utc.to_unix.to_s + file.checksum = Utils::Hashing.hash_file(file_path) + end + + if CONFIG.deleteKeyLength > 0 + file.delete_key = Random.base58(CONFIG.deleteKeyLength) + end + + # X-Real-IP if behind a reverse proxy and the header is set in the reverse + # proxy configuration. + begin + spawn { Utils.generate_thumbnail(file.filename, file.extension) } + rescue ex + LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}" + end + + begin + Database::Files.insert(file) + # Database::IP.insert(ip_addr) + # SQL.exec "INSERT OR IGNORE INTO ips (ip, date) VALUES (?, ?)", ip_address, Time.utc.to_unix + # # SQL.exec "INSERT OR IGNORE INTO ips (ip) VALUES ('#{ip_address}')" + # SQL.exec "UPDATE ips SET count = count + 1 WHERE ip = ('#{ip_address}')" + rescue ex + LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}" + ee 500, "An error ocurred when trying to insert the data into the DB" + end + + res = Response.new(file, scheme, host) + res.to_json + end + + # The most unoptimized and unstable feature lol + # def upload_url_bulk(env) + # env.response.content_type = "application/json" + # ip_address = Utils.ip_address(env) + # protocol = Utils.protocol(env) + # host = Utils.host(env) + # begin + # files = env.params.json["files"].as((Array(JSON::Any))) + # rescue ex : JSON::ParseException + # LOGGER.error "Body malformed: #{ex.message}" + # ee 400, "Body malformed: #{ex.message}" + # rescue ex + # LOGGER.error "Unknown error: #{ex.message}" + # ee 500, "Unknown error" + # end + # successfull_files = [] of NamedTuple(filename: String, extension: String, original_filename: String, checksum: String, delete_key: String | Nil) + # failed_files = [] of String + # # X-Real-IP if behind a reverse proxy and the header is set in the reverse + # # proxy configuration. + # files.each do |url| + # url = url.to_s + # filename = Utils.generate_filename + # original_filename = "" + # extension = "" + # checksum = "" + # uploaded_at = Time.utc + # extension = File.extname(URI.parse(url).path) + # if CONFIG.deleteKeyLength > 0 + # delete_key = Random.base58(CONFIG.deleteKeyLength) + # end + # file_path = "#{CONFIG.files}/#{filename}#{extension}" + # File.open(file_path, "w") do |output| + # begin + # HTTP::Client.get(url) do |res| + # IO.copy(res.body_io, output) + # end + # rescue ex + # LOGGER.debug "Failed to download file '#{url}': #{ex.message}" + # ee 403, "Failed to download file '#{url}'" + # failed_files << url + # end + # end + # # successfull_files << url + # # end + # if extension.empty? + # extension = Utils.detect_extension(file_path) + # File.rename(file_path, file_path + extension) + # file_path = "#{CONFIG.files}/#{filename}#{extension}" + # end + # # The second one is faster and it uses less memory + # # original_filename = URI.parse("https://ayaya.beauty/PqC").path.split("/").last + # original_filename = url.split("/").last + # checksum = Utils::Hashing.hash_file(file_path) + # begin + # spawn { Utils.generate_thumbnail(filename, extension) } + # rescue ex + # LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}" + # end + # begin + # # Insert SQL data just before returning the upload information + # SQL.exec("INSERT INTO files VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + # original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil) + # successfull_files << {filename: filename, + # original_filename: original_filename, + # extension: extension, + # delete_key: delete_key, + # checksum: checksum} + # rescue ex + # LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}" + # ee 500, "An error ocurred when trying to insert the data into the DB" + # end + # end + # json = JSON.build do |j| + # j.array do + # successfull_files.each do |fileinfo| + # j.object do + # j.field "link", "#{protocol}://#{host}/#{fileinfo[:filename]}" + # j.field "linkExt", "#{protocol}://#{host}/#{fileinfo[:filename]}#{fileinfo[:extension]}" + # j.field "id", fileinfo[:filename] + # j.field "ext", fileinfo[:extension] + # j.field "name", fileinfo[:original_filename] + # j.field "checksum", fileinfo[:checksum] + # if CONFIG.deleteKeyLength > 0 + # delete_key = Random.base58(CONFIG.deleteKeyLength) + # j.field "deleteKey", fileinfo[:delete_key] + # j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{fileinfo[:delete_key]}" + # end + # end + # end + # end + # end + # json + # end + + # def upload_url(env) + # env.response.content_type = "application/json" + # ip_address = Utils.ip_address(env) + # protocol = Utils.protocol(env) + # host = Utils.host(env) + # url = env.params.query["url"] + # successfull_files = [] of NamedTuple(filename: String, extension: String, original_filename: String, checksum: String, delete_key: String | Nil) + # failed_files = [] of String + # # X-Real-IP if behind a reverse proxy and the header is set in the reverse + # # proxy configuration. + # filename = Utils.generate_filename + # original_filename = "" + # extension = "" + # checksum = "" + # uploaded_at = Time.utc + # extension = File.extname(URI.parse(url).path) + # if CONFIG.deleteKeyLength > 0 + # delete_key = Random.base58(CONFIG.deleteKeyLength) + # end + # file_path = "#{CONFIG.files}/#{filename}#{extension}" + # File.open(file_path, "w") do |output| + # begin + # # TODO: Connect timeout to prevent possible Denial of Service to the external website spamming requests + # # https://crystal-lang.org/api/1.13.2/HTTP/Client.html#connect_timeout + # HTTP::Client.get(url) do |res| + # IO.copy(res.body_io, output) + # end + # rescue ex + # LOGGER.debug "Failed to download file '#{url}': #{ex.message}" + # ee 403, "Failed to download file '#{url}': #{ex.message}" + # failed_files << url + # end + # end + # if extension.empty? + # extension = Utils.detect_extension(file_path) + # File.rename(file_path, file_path + extension) + # file_path = "#{CONFIG.files}/#{filename}#{extension}" + # end + # # The second one is faster and it uses less memory + # # original_filename = URI.parse("https://ayaya.beauty/PqC").path.split("/").last + # original_filename = url.split("/").last + # checksum = Utils::Hashing.hash_file(file_path) + # begin + # spawn { Utils.generate_thumbnail(filename, extension) } + # rescue ex + # LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}" + # end + # begin + # # Insert SQL data just before returning the upload information + # SQL.exec("INSERT INTO files VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + # original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil) + # successfull_files << {filename: filename, + # original_filename: original_filename, + # extension: extension, + # delete_key: delete_key, + # checksum: checksum} + # rescue ex + # LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}" + # ee 500, "An error ocurred when trying to insert the data into the DB" + # end + # json = JSON.build do |j| + # j.array do + # successfull_files.each do |fileinfo| + # j.object do + # j.field "link", "#{protocol}://#{host}/#{fileinfo[:filename]}" + # j.field "linkExt", "#{protocol}://#{host}/#{fileinfo[:filename]}#{fileinfo[:extension]}" + # j.field "id", fileinfo[:filename] + # j.field "ext", fileinfo[:extension] + # j.field "name", fileinfo[:original_filename] + # j.field "checksum", fileinfo[:checksum] + # if CONFIG.deleteKeyLength > 0 + # delete_key = Random.base58(CONFIG.deleteKeyLength) + # j.field "deleteKey", fileinfo[:delete_key] + # j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{fileinfo[:delete_key]}" + # end + # end + # end + # end + # end + # json + # end +end diff --git a/src/routes/views.cr b/src/routes/views.cr new file mode 100644 index 0000000..4b025e3 --- /dev/null +++ b/src/routes/views.cr @@ -0,0 +1,18 @@ +module Routes::Views + extend self + + def root(env) + host = env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]? + scheme = env.request.headers["X-Forwarded-Proto"]? || "http" + files_hosted = Database::Files.file_count + + render "src/views/index.ecr" + end + + def chatterino(env) + host = env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]? + scheme = env.request.headers["X-Forwarded-Proto"]? || "http" + + render "src/views/chatterino.ecr" + end +end diff --git a/src/routing.cr b/src/routing.cr index 6e8f8dd..27dadf6 100644 --- a/src/routing.cr +++ b/src/routing.cr @@ -1,26 +1,50 @@ -require "./http-errors" +require "./macros" +require "./routes/**" module Routing extend self - @@exit_nodes = Array(String).new - def reload_exit_nodes - LOGGER.debug "Updating Tor exit nodes array" - @@exit_nodes = Utils.load_tor_exit_nodes - LOGGER.debug "IPs inside the Tor exit nodes array: #{@@exit_nodes.size}" - end + {% for http_method in {"get", "post", "delete", "options", "patch", "put"} %} - before_post "/api/admin/*" do |env| - if env.request.headers.try &.["X-Api-Key"]? != CONFIG.adminApiKey || nil - halt env, status_code: 401, response: http_error 401, "Wrong API Key" + macro {{http_method.id}}(path, controller, method = :handle) + unless Kemal::Utils.path_starts_with_slash?(\{{path}}) + raise Kemal::Exceptions::InvalidPathStartException.new({{http_method}}, \{{path}}) + end + + Kemal::RouteHandler::INSTANCE.add_route({{http_method.upcase}}, \{{path}}) do |env| + \{{ controller }}.\{{ method.id }}(env) + end + end + + {% end %} + + # before_post "/api/admin/*" do |env| + # env.response.content_type = "application/json" + + # if env.request.headers.try &.["X-Api-Key"]? != CONFIG.adminApiKey || nil + # halt env, status_code: 401, response: "Wrong API Key" + # end + # end + + before_post do |env| + tor_exit_nodes = Utils::Tor.exit_nodes + api_key = env.request.headers["X-Api-Key"]? + + # Skips Tor blocking and Rate limits if the API key matches + if api_key == CONFIG.adminApiKey + next + end + + if CONFIG.blockTorAddresses && tor_exit_nodes.includes?(Headers.ip_addr) + halt env, status_code: 401, response: CONFIG.torMessage end end before_post "/upload" do |env| begin - ip_info = SQL.query_one?("SELECT ip, count, date FROM ips WHERE ip = ?", Utils.ip_address(env), as: {ip: String, count: Int32, date: Int32}) + ip_info = SQL.query_one?("SELECT ip, count, date FROM ips WHERE ip = ?", Headers.ip_addr, as: {ip: String, count: Int32, date: Int32}) rescue ex - LOGGER.error "Error when trying to enforce rate limits: #{ex.message}" + LOGGER.error "Error when trying to enforce rate limits for ip #{Headers.ip_addr}: #{ex.message}" next end @@ -35,97 +59,56 @@ module Routing end if CONFIG.filesPerIP > 0 if ip_info[:count] >= CONFIG.filesPerIP && time_since_first_upload < CONFIG.rateLimitPeriod - halt env, status_code: 401, response: http_error 401, "Rate limited! Try again in #{time_until_unban} seconds" + halt env, status_code: 401, response: "Rate limited! Try again in #{time_until_unban} seconds" end end end - before_post do |env| - if env.request.headers.try &.["X-Api-Key"]? == CONFIG.adminApiKey - # Skips Tor and Rate limits if the API key matches - next - end - if CONFIG.blockTorAddresses && @@exit_nodes.includes?(Utils.ip_address(env)) - halt env, status_code: 401, response: http_error 401, CONFIG.torMessage - end - end - def register_all - get "/" do |env| - host = Utils.host(env) - files_hosted = SQL.query_one "SELECT COUNT (filename) FROM files", as: Int32 - render "src/views/index.ecr" - end + get "/", Routes::Views, :root + get "/info/chatterino", Routes::Views, :chatterino - get "/chatterino" do |env| - host = Utils.host(env) - protocol = Utils.protocol(env) - render "src/views/chatterino.ecr" - end + post "/upload", Routes::Upload, :upload + # get "/upload", Routes::Upload, :upload_url + # post "/api/uploadurl", Routes::Upload, :upload_url - post "/upload" do |env| - Handling.upload(env) - end + get "/:filename", Routes::Retrieve, :retrieve_file + get "/thumbnail/:thumbnail", Routes::Retrieve, :retrieve_thumbnail - get "/upload" do |env| - Handling.upload_url(env) - end + get "/delete", Routes::Deletion, :delete_file - post "/api/uploadurl" do |env| - Handling.upload_url_bulk(env) - end + get "/api/stats", Routing::Misc, :stats + get "/info/sharex.sxcu", Routing::Misc, :sharex_config + get "/info/chatterinoconfig", Routing::Misc, :chatterino_config - get "/:filename" do |env| - Handling.retrieve_file(env) - end - - get "/thumbnail/:thumbnail" do |env| - Handling.retrieve_thumbnail(env) - end - - get "/delete" do |env| - Handling.delete_file(env) - end - - get "/api/stats" do |env| - Handling.stats(env) - end - - get "/sharex.sxcu" do |env| - Handling.sharex_config(env) - end - - get "/chatterinoconfig" do |env| - Handling.chatterino_config(env) - end - - if CONFIG.adminEnabled - self.register_admin - end + # if CONFIG.adminEnabled + # self.register_admin + # end end - def register_admin - # post "/api/admin/upload" do |env| - # Handling::Admin.delete_ip_limit(env) - # end - post "/api/admin/delete" do |env| - Handling::Admin.delete_file(env) - end - end + # def register_admin + # # post "/api/admin/upload" do |env| + # # Routes::Admin.delete_ip_limit(env) + # # end + # post "/api/admin/delete" do |env| + # Routes::Admin.delete_file(env) + # end + # end - post "/api/admin/deleteiplimit" do |env| - Handling::Admin.delete_ip_limit(env) - end + # post "/api/admin/deleteiplimit" do |env| + # Routes::Admin.delete_ip_limit(env) + # end - post "/api/admin/fileinfo" do |env| - Handling::Admin.retrieve_file_info(env) - end + # post "/api/admin/fileinfo" do |env| + # Routes::Admin.retrieve_file_info(env) + # end - get "/api/admin/torexitnodes" do |env| - Handling::Admin.retrieve_tor_exit_nodes(env, @@exit_nodes) - end + # get "/api/admin/torexitnodes" do |env| + # Routes::Admin.retrieve_tor_exit_nodes(env, @@exit_nodes) + # end - error 404 do - "File not found" + error 404 do |env| + env.response.content_type = "text/plain" + "File not found.\nArchivo no encontrado." end end diff --git a/src/types/ip.cr b/src/types/ip.cr new file mode 100644 index 0000000..d1cd187 --- /dev/null +++ b/src/types/ip.cr @@ -0,0 +1,24 @@ +struct IP + # Without this, this class will not be able to be used as `as: UFile` on + # SQL queries + include DB::Serializable + + property ip : String + property count : Int32 + property unix_date : Int32 + + def initialize( + @ip, + @count, + @unix_date, + ) + end + + def to_tuple + {% begin %} + { + {{@type.instance_vars.map(&.name).splat}} + } + {% end %} + end +end diff --git a/src/types/ufile.cr b/src/types/ufile.cr new file mode 100644 index 0000000..d35c705 --- /dev/null +++ b/src/types/ufile.cr @@ -0,0 +1,34 @@ +struct UFile + # Without this, this class will not be able to be used as `as: UFile` on + # SQL queries + include DB::Serializable + + property original_filename : String = "" + property filename : String = "" + property extension : String = "" + property uploaded_at : String = "" + property checksum : String = "" + property ip : String = "" + property delete_key : String = "" + property thumbnail : String? + + def initialize( + @original_filename = "", + @filename = "", + @extension = "", + @uploaded_at = "", + @checksum = "", + @ip = "", + @delete_key = "", + @thumbnail = nil, + ) + end + + def to_tuple + {% begin %} + { + {{@type.instance_vars.map(&.name).splat}} + } + {% end %} + end +end diff --git a/src/utils/hashing.cr b/src/utils/hashing.cr new file mode 100644 index 0000000..8eb096a --- /dev/null +++ b/src/utils/hashing.cr @@ -0,0 +1,11 @@ +module Utils::Hashing + extend self + + def hash_file(file_path : String) : String + Digest::SHA1.hexdigest &.file(file_path) + end + + def hash_io(file_path : IO) : String + Digest::SHA1.hexdigest &.update(file_path) + end +end diff --git a/src/utils/tor.cr b/src/utils/tor.cr new file mode 100644 index 0000000..b266d08 --- /dev/null +++ b/src/utils/tor.cr @@ -0,0 +1,38 @@ +module Utils::Tor + extend self + @@exit_nodes : Array(String) = [] of String + + def refresh_exit_nodes + LOGGER.debug "reload_exit_nodes: Updating Tor exit nodes list" + retrieve_tor_exit_nodes + LOGGER.debug "reload_exit_nodes: IPs inside the Tor exit nodes list: #{@@exit_nodes.size}" + end + + def retrieve_tor_exit_nodes + LOGGER.debug "retrieve_tor_exit_nodes: Retrieving Tor exit nodes list" + ips = [] of String + + HTTP::Client.get(CONFIG.torExitNodesUrl) do |res| + begin + if res.success? && res.status_code == 200 + res.body_io.each_line do |line| + if line.includes?("ExitAddress") + ips << line.split(" ")[1] + end + end + @@exit_nodes = ips + else + LOGGER.error "retrieve_tor_exit_nodes: Failed to retrieve exit nodes list. Status Code: #{res.status_code}" + end + rescue ex : Socket::ConnectError + LOGGER.error "retrieve_tor_exit_nodes: Failed to connect to #{CONFIG.torExitNodesUrl}: #{ex.message}" + rescue ex + LOGGER.error "retrieve_tor_exit_nodes: Unknown error: #{ex.message}" + end + end + end + + def exit_nodes : Array(String) + return @@exit_nodes + end +end diff --git a/src/utils.cr b/src/utils/utils.cr similarity index 50% rename from src/utils.cr rename to src/utils/utils.cr index 083146e..d76f4aa 100644 --- a/src/utils.cr +++ b/src/utils/utils.cr @@ -43,25 +43,57 @@ module Utils end end - def check_old_files - LOGGER.info "Deleting old files" - fileinfo = SQL.query_all("SELECT filename, extension, thumbnail - FROM files - WHERE uploaded_at < datetime('now', '-#{CONFIG.deleteFilesAfter} days')", - as: {filename: String, extension: String, thumbnail: String | Nil}) + def delete_file(env) + key = env.params.query["key"] + file = SQL.select_with_key(key) + full_filename = file.filename + file.extension + thumbnail = file.thumbnail - fileinfo.each do |file| - LOGGER.debug "Deleting file '#{file[:filename]}#{file[:extension]}'" + # Delete file + File.delete("#{CONFIG.files}/#{full_filename}") + + if file.thumbnail + File.delete("#{CONFIG.thumbnails}/#{thumbnail}") + end + + # Delete entry from db + Database::Files.delete_with_key(key) + + LOGGER.debug "File '#{full_filename}' was deleted using key '#{key}'}" + msg("File '#{full_filename}' deleted successfully") + end + + # TODO: Spawn a fiber and add each file to an array to bulk delete files from + # the database using a single SQL query. + # In the end, all old files should be not accessible, even if they are on the + # drive. + def check_old_files + LOGGER.info "check_old_files: Deleting old files" + files = Database::Files.old_files + + files.each do |f| + full_filename = f.filename + f.extension + thumbnail = f.thumbnail + + # TODO: Check if it's able to bypass the path using a filename with a `/` in their name + LOGGER.debug "check_old_files: Deleting file '#{full_filename}'" begin - File.delete("#{CONFIG.files}/#{file[:filename]}#{file[:extension]}") - if file[:thumbnail] - File.delete("#{CONFIG.thumbnails}/#{file[:thumbnail]}") + File.delete("#{CONFIG.files}/#{full_filename}") + + if thumbnail + File.delete("#{CONFIG.thumbnails}/#{thumbnail}") end - SQL.exec "DELETE FROM files WHERE filename = ?", file[:filename] + + Database::Files.delete(f.filename) + rescue File::NotFoundError + LOGGER.error "check_old_files: File '#{full_filename}' doesn't seem to exist on the '#{CONFIG.files}', folder, deleting it from the database" + Database::Files.delete(f.filename) + rescue ex : File::AccessDeniedError + LOGGER.error "check_old_files: File '#{full_filename}' failed to be deleted due to bad permissions, deleting it from the database: #{ex.message}" + Database::Files.delete(f.filename) rescue ex - LOGGER.error "#{ex.message}" - # Also delete the file entry from the DB if it doesn't exist. - SQL.exec "DELETE FROM files WHERE filename = ?", file[:filename] + LOGGER.error "check_old_files: File '#{full_filename}' failed to be deleted, deleting it from the database: #{ex.message}" + Database::Files.delete(f.filename) end end end @@ -69,7 +101,7 @@ module Utils def check_dependencies dependencies = ["ffmpeg"] dependencies.each do |dep| - next if !CONFIG.generateThumbnails + next if !CONFIG.generate_thumbnails if !Process.find_executable(dep) LOGGER.fatal("'#{dep}' was not found.") exit(1) @@ -77,39 +109,17 @@ module Utils end end - # TODO: - # def check_duplicate(upload) - # file_checksum = SQL.query_all("SELECT checksum FROM files WHERE original_filename = ?", upload.filename, as:String).try &.[0]? - # if file_checksum.nil? - # return - # else - # uploaded_file_checksum = hash_io(upload.body) - # pp file_checksum - # pp uploaded_file_checksum - # if file_checksum == uploaded_file_checksum - # puts "Dupl" - # end - # end - # end - - def hash_file(file_path : String) : String - Digest::SHA1.hexdigest &.file(file_path) - end - - def hash_io(file_path : IO) : String - Digest::SHA1.hexdigest &.update(file_path) - end - # TODO: Check if there are no other possibilities to get a random filename and exit def generate_filename - filename = Random.base58(CONFIG.fileameLength) + filename = Random.base58(CONFIG.filename_length) loop do - if SQL.query_one("SELECT COUNT(filename) FROM files WHERE filename = ?", filename, as: Int32) == 0 + file = Database::Files.select(filename) + if !file return filename else - LOGGER.debug "Filename collision! Generating a new filename" - filename = Random.base58(CONFIG.fileameLength) + LOGGER.trace "Filename collision! Generating a new filename" + filename = Random.base58(CONFIG.filename_length) end end end @@ -117,13 +127,15 @@ module Utils def generate_thumbnail(filename, extension) exts = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp", ".heic", ".jxl", ".avif", ".crw", ".dng", ".mp4", ".mkv", ".webm", ".avi", ".wmv", ".flv", "m4v", ".mov", ".amv", ".3gp", ".mpg", ".mpeg", ".yuv"] - # To prevent thumbnail generation on non image extensions - return if exts.none? do |ext| - extension.downcase.includes?(ext) - end + + # To prevent thumbnail generation on non image extensions + return if exts.none? { |ext| extension.downcase.includes?(ext) } + # Disable generation if false - return if !CONFIG.generateThumbnails || !CONFIG.thumbnails + return if !CONFIG.generate_thumbnails || !CONFIG.thumbnails + LOGGER.debug "Generating thumbnail for #{filename + extension} in background" + process = Process.run("ffmpeg", [ "-hide_banner", @@ -137,11 +149,12 @@ module Utils "-update", "1", "#{CONFIG.thumbnails}/#{filename}.jpg", ]) - if process.exit_code == 0 - LOGGER.debug "Thumbnail for #{filename + extension} generated successfully" + + if process.normal_exit? + LOGGER.debug "Thumbnail for '#{filename + extension}' generated successfully" SQL.exec "UPDATE files SET thumbnail = ? WHERE filename = ?", filename + ".jpg", filename else - # TODO: Add some sort of message when the thumbnail is not generated + LOGGER.debug "Failed to generate thumbnail for '#{filename + extension}'. Exit code of ffmpeg: #{process.exit_code}" end end @@ -159,25 +172,6 @@ module Utils end end - def delete_file(env) - fileinfo = SQL.query_all("SELECT filename, extension, thumbnail - FROM #{CONFIG.dbTableName} - WHERE delete_key = ?", - env.params.query["key"], - as: {filename: String, extension: String, thumbnail: String | Nil})[0] - - # Delete file - File.delete("#{CONFIG.files}/#{fileinfo[:filename]}#{fileinfo[:extension]}") - if fileinfo[:thumbnail] - File.delete("#{CONFIG.thumbnails}/#{fileinfo[:thumbnail]}") - end - # Delete entry from db - SQL.exec "DELETE FROM files WHERE delete_key = ?", env.params.query["key"] - - LOGGER.debug "File '#{fileinfo[:filename]}' was deleted using key '#{env.params.query["key"]}'}" - msg("File '#{fileinfo[:filename]}' deleted successfully") - end - MAGIC_BYTES = { # Images ".png" => "89504e470d0a1a0a", @@ -221,60 +215,4 @@ module Utils end "" end - - def retrieve_tor_exit_nodes - LOGGER.debug "Retrieving Tor exit nodes list" - HTTP::Client.get(CONFIG.torExitNodesUrl) do |res| - begin - if res.success? && res.status_code == 200 - begin - File.open(CONFIG.torExitNodesFile, "w") { |output| IO.copy(res.body_io, output) } - rescue ex - LOGGER.error "Failed to save exit nodes list: #{ex.message}" - end - else - LOGGER.error "Failed to retrieve exit nodes list. Status Code: #{res.status_code}" - end - rescue ex : Socket::ConnectError - LOGGER.error "Failed to connect to #{CONFIG.torExitNodesUrl}: #{ex.message}" - rescue ex - LOGGER.error "Unknown error: #{ex.message}" - end - end - end - - def load_tor_exit_nodes - exit_nodes = File.read_lines(CONFIG.torExitNodesFile) - ips = [] of String - exit_nodes.each do |line| - if line.includes?("ExitAddress") - ips << line.split(" ")[1] - end - end - return ips - end - - def ip_address(env) : String - begin - return env.request.headers.try &.["X-Forwarded-For"] - rescue - return env.request.remote_address.to_s.split(":").first - end - end - - def protocol(env) : String - begin - return env.request.headers.try &.["X-Forwarded-Proto"] - rescue - return "http" - end - end - - def host(env) : String - begin - return env.request.headers.try &.["X-Forwarded-Host"] - rescue - return env.request.headers["Host"] - end - end end diff --git a/src/views/chatterino.ecr b/src/views/chatterino.ecr index b8dc463..d7572ac 100644 --- a/src/views/chatterino.ecr +++ b/src/views/chatterino.ecr @@ -4,17 +4,18 @@ <%= host %> - - + +
    -

    Chatterino config

    -

    Request URL: <%= protocol %>://<%= host %>/upload

    -

    Form field: data

    -

    Image link: link

    -

    Delete link: deleteLink

    +

    Chatterino Config

    +

    Request URL: <%= scheme %>://<%= host %>/upload

    +

    Form field: data

    +

    Image link: link

    +

    Delete link: deleteLink

    +
    diff --git a/src/views/index.ecr b/src/views/index.ecr index dd8d711..9b691c9 100644 --- a/src/views/index.ecr +++ b/src/views/index.ecr @@ -4,8 +4,8 @@ <%= host %> - - + + @@ -21,24 +21,22 @@
    -

    - Chatterino Config | - ShareX Config | - - file-uploader-crystal (BETA <%= CURRENT_TAG %> - <%= CURRENT_VERSION %> @ <%= CURRENT_BRANCH %>) - -

    -

    Archivos alojados: <%= files_hosted %>

    - <% if CONFIG.blockTorAddresses %> -

    <%= CONFIG.torMessage %>

    - <% end %> - <% if !CONFIG.alternativeDomains.empty? %> -

    - <% CONFIG.alternativeDomains.each do | domain | %> - <%= domain %> - <% end %> -

    - <% end %> +

    + Chatterino Config | + ShareX Config | + + file-uploader-crystal (BETA <%= CURRENT_TAG %> - <%= CURRENT_VERSION %> @ <%= CURRENT_BRANCH %>) + +

    +

    Archivos alojados: <%= files_hosted %>

    + <% if !CONFIG.alternative_domains.empty? %> +

    + <% CONFIG.alternative_domains.each do | domain | %> + <%= domain %> + <% end %> +

    + <% end %>
    +