diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d9324..a696048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## CHANGELOG +### v1.1.2 (2023-12-26) + +* Macstodon is now fully compatible with WebOne 0.16+, and should also feature improved compatibility with other web proxies that can strip SSL. + ### v1.1.1 (2023-12-10) * Fixed a dumb bug that would cause Macstodon to crash after authentication if you were trying to sign in to an account that had no lists. diff --git a/Macstodon.rsrc.sit.hqx b/Macstodon.rsrc.sit.hqx index abb3529..52305c0 100644 --- a/Macstodon.rsrc.sit.hqx +++ b/Macstodon.rsrc.sit.hqx @@ -1 +1 @@ -(This file must be converted with BinHex 4.0) :%NeKBh0dEf4[ELjbFh*M,R0TG!"6593e8dP8)3#3"$Qc!*!%meK6G(9QCNPd)#K M+6%j16FY-6Nj1#""E'&NC'PZ)&0jFh4PEA-X)%PZBbiX)'KdG(!k,bphN!-ZB@a KC'4TER0jFbjMEfd[8h4eCQC*G#m0#KS!"4!!!$Qc!*!$FJ!"!*!$FP!"$D@P8Q9 cCA*fC@5PT3#PN!3"!!!q!!$J30mZiCXmJ!#3$3lX5!#3$NeKBh0dEf4[ELjbFh* M!!'r"R*cFQ058d9%!3$rN!3!N!U!!*!*TiB!!$M4!*!%$`"#`G8+ql&3'UN0#U` 6bShAUJX*[KTYHi9#MEkZePPP9p6cF#P)MH%$35*,hd1G`5&V1'YXK,P9iX`r#T* KFXAA1,(k"hj"!-NDZc5!#XHNl%*[ffV!*1Df@eejmP5Ck9Qe+ZJcQ$P+$m#jiZA @U*chD&!h+SYL'BI4`aAUR-))aD"Yhkb#E+5"c%pf,BG&"bQYdq+6R@K4h2KB`2h b`YL4#A),,[UjJ@R'HDHCh)@5X0*JR6#2aG4rTZ*EXS@!r5(L6kF-HP9eQY@'',B Y%N#!dFYB@RBkD-,[H2B4[Cl#HS@VTQpTVP&TU&6'U2"pSRpBcj!!!Gm-pq'R'E0 @"1KJ[ED8AX%%&TrCY&,Ca4V[8N[!RkI(+a&8`i1&Hf`ADE&'ARpJ'ME2S%*B8$R ,kY%2Sc5*pZUE[)"hkAPapFf)PUNdFL18+l!cj,SUX`Dakh'8d@kDYAJ4'"PH9%* 2cj4C5BPd,'FCLLE5AV!aVURf@4fq1GicIE(Ff"50ZC1DPSPL!BBmLSBhIcTkFr! p%@!1FM#-+6cI0m@@691`*r$cVH9'IPHNmIUe0'`q3j8Bp-NJF8T!UZ8U+*lj!)M Q(1+)FU(3)%j*[k$cpbhGTjKcBK%U'8`3!iI5AfpU[!PN,1cT`80YlHl(1rfie,A 0PKHND8XCV+iURZ#paD43A89jq%a+8IUVA5kfZ5X@%fei4r-frDf##EcUpDKU&%F *L@&`aVbl"TR+q+&H9IULjeC8eP*RdfJ-[V$$JNL3!ik1!iJV00h2aFpT[)6IVB( ,@*%RNMC*LX(##@bIk(N%8E!TRBaT@(a"REr4U['q*Tr"JD6d@0CYpT6#D)c5S8T kPZPH5KBdL3'elrVjm[EhSRBhP4I3TbQ+IHAVIX#"hX2YbpG(4XT'Ef+)YZ*4`S! DRp9@2T3cP[G'PTKDj%D'Ga1lYUr#+E1r0TfJL$0-piDqP29[LC,1mcTFZb+4FCj jc$`9YF*a@Yh3'j'Qd!!1!e1#H()c*Dd&)*3cdGA''ETd"'Qk6h9E&MI5fhh##bj DIJRBJ`E#!D"dLqU*3r[jjhapQqB5(R&"X[q!T"5D+bVAJD8'P@GDkF@6mKm&ckq $TTi[i43c#pGp(NT5TYh`R(3`63Lk"4"$Qi!S*Lba3*jd%N&bfS'&i5!)!M4`Z@a MFXeTpJMQPpIF1jI`U0,GqKM+!ILUlRM#U1@'N!!F0j`'JHQq6rqrMV!%@4#TRpr +-CV&H4Md2'3HDMT6$p'Fd80$e486%"+DBXmIK!c4(`)q1pM4*+1c'8)r9Ih+RpU K5HT4qA(qd`4!CiK&p)`rN!"L'&%3m4$SXV-BA2X&0La08rZ!ejKb@eTi1U6%0cj Y&!0*CkJ8#iU5fRaMZPh)r&'$rKV$Z@L)*Ffk!'0Md"QYpI9@b(mGh8Mr&1D3!+8 +MN5TKTSTC35Vf`115![dlim[pFhai58!P,(41US29Yf,P[l2U!XJ%e&#h)D'+VA 9mFB-`#8YV+YVDb6N*caXhXQ8'Sb6rSVHl+)S2%VLc)bC8%cL9Y5e"kjm,$iS$hU )k-)m0`0GLhL+'$qT(e2NL@cpF+!rq$%6CIDQ54ZQSPmPZk4Id6%4(e'bP'NTL-j k4DSdBX+B`)(8FXND#a)EZSB4ZQkU,&A!CU+akJ'qZieC9mK+8``+Kl`Y#cU[k%6 4bq)C8Y8L'dZUDlFji+16rlh)S($STK$5I`b9SPU3!+Ma*282dU84UGA"`dJJ2im e,rClY@XN4[NJ@3G,T-0Ah+YNCjLVU&FhaZbKF4ka2!p(,8k'&hPmc&2VEG(rFR% Vkf81Qc)ZGlY3hdCKSF'1dD,JLNJA51-YYM0RKk&fl3"3-DNpKkerl342iA+mi[6 #Pi"21`FS9**lIbQLJ2H#EYdaHQ3%0%%3V&#U9AbQa#fl1B(G5!1Yf5Fd1a`-(*& CDKMc5LZXjZhYH5l9IL$D)P[F)$['A(8PL*LJL`E)CM11)jV!akHlXmb--4Y8HM` 0`h#LKbML$ZY#(2Pc-aX%GC8LfCG!4)"M,l'QSfm`p&MXVL5!SCINqKjl85!CIda BPmHV`h3FXTl&%8GXc2")k%`Qah1XjIm2IC+'P[ANID85@%+FKqXKme8(0-!TR!c *9kCkPcp'JUAr)&6bi*F4%[&-GAfE'F!L9X"iiDcUB$5!@TCU1FGjbQ'B+-@!#8T )%p2TY(JmN!"Y21J3@0CPD-P)D9YH$iDA)UI(U,rlaT[CLdC8QB)*ZPiJ!19DhLI N-BD,5p([##`"cQGc3MY-U%Ch,0rpJ%1lA&LYkG!45XhZGFEDP2rDikGp!9h00,# EXdhBhTL)PHJ@D6jNie+GQ`L$Y8jcrX,[QYC`phA30HGkKUk8#61dr+Ld2Lr1ZG` L3a[QkDZ%Y[Xr'qXp3pc35dFB3KXfqQVC02-M*-KGZZV&5AGA4C)VjmHI#9h4FV" %U%2qq'01P3X+Ll0F(PYM(U*T(@cL@PB%*MI+m&&VYYAFQP30(aZQ#hBr(Cq`Sb2 IaM*Y*[I*34F([DD*Fbi2piQjJURjl@!!E08NSI0T0-Q(C)T$%L#Q#lJN`Idi%,U (cAIrKAh*2XiK950+E83e4aUB84Cl#5Xeqip4jlT+i2jPXSfB)GXRKX1@h@3jQhq [XDJmNKJ$4QK8Q%*Y1NrDZ%b%&E3%P2V"[4QXlS#HDNCVFAV&iq09pUQ@jkclqF6 `A"ZN8B2&5N3+eQTEY[2G@IQf0CpKQNSQ'H1Ne9&)5@""L),%qeF5IK@cDJkI[Y& QP,a5b%#@H2qBH5GQa55&3Q2dUGlJQ5Q)JlTbNIUi-D`V(YrVhS+fZ'#+Ra$%rSd "9JrUcHZ83p4Q3mJ2!Y(`T34lfKhkm4G6"QQrdf8lqS!21P3a+AdrGN)XN!#!kS3 'Kii$FBKCmc$VmNha'aDKG6G3bf15Z2%aL!HQGRfT,8J-%[S&*Dp+DDGRq(jB@NP Qfl-r8LH4aY90Vh2bl63HhMFJH@Z6e0!LR)H+j[98,dlYHVBQKKi%jTJENJc[dG' 52Y%qAT(Hd6NA)Nk'NPXP*'$)"&D#e@(V@)-aA(T3aRam!12q3i2JB[25Nde(ae8 MqdJQ#4GlaUcd9mamf)Q3!1Rj,@6$dTj-"9r4ETJ@B)UKK(3ilICjE(kk&%5I"%d mfY"E"A*60"%P6'E,&d4SF,VZK2b3!'F9CBbcJ$QS[8XqU836(r-#1(N9aiN$L%( I15qQpfmFr9FCA`Di3')3U!1URV0V)%ii#fGSD0AN!kmNr9cf-U1+`1,*6@fAP6m !Ni8Mp@c-NjHmL2!,I,Vf"TGq(59CATRTl815HIA-+3dBeJkF6HNa0'F0INDEP"I F4%FE)V@*)NkM)N(0k%N4ZL#H)%Sr&QNa%mTI!eBYcKpPq5pUm4UemULpPQ#IP6& Z&0BJXMJb+KCm!*FMcFYCRkbS@a-$RAURd")HX9jm61aH1VU@i(G8fAZEe6bN)YG c0-%2,@MHYqlX3FrHT[#C@p'ZjmBSVXYGC23QQ#%4`QmQYeTM%[A6YDT[p#kYSeb 8CN"M0DB%*"!c9Tcbr30!2)6X5&4)96F5#KHGDDG%m3@K#JYVUK%V+XZYMMb*(Ni JQmT8lcdcJhHQVVL6Yr+)5h$'#plF%!r'``%NFdm`h,Tp*H#,R54&IpjbbKpQqF2 C!lCDqPme#5-H)r(8U-#k62P(6qqHkHk)Br9PY*2I'X2Ym)HP0B3paF#XXKYN"5c R,TQGTI-pF1LTq3mJZ0QL!JJ[Iil'[+@rUP5cDU,(EZ8*jZ)'IS2lH5QA1m&8Ml$ )E$[`q$T'q)G*8V49BbjEcSe,"IrKVFr8QpaMXRL3!+9[S'1!cFJH9jbF&q+mL)J TpeMr[@SE9pFMbqrdQdZ0$T1R6(riB+C05&UJP8I16H[m5,K#HC@"c"6cqR(4LVK 6E1%23FCGrfDVN!#5&V-P!&d!$5CG+Ne'pZ8ii[L'p'ShqA1`bEJ,&p+4@km9Xdh k@U5'hEYa!`2p@%hDF$94K)8kiqE8fBT$d!+rF2+2RBM+PQABJ,$NNQMF%6erq`5 %q+&8#U[`G@Uq8cP'keDY5NlL53Y[XrL#BL1f*Qh!,4kFQ@(ea(5E5D,)*p+Q9mh 385KpQEIARSUC5a#-%)3b&$E'@Gc2-UGcB9(EMC&!ZGF[Hr-D2S[`E@3RDIEr@TE $U1HR56#PDm5bD#SY6fG-aGeGeQ9SpM(AVH"(F2lLR'bEa('SmQh($d46lSZ(4!p iEC*m$%YFSEBJIiPR8&TjQh`Z1eYXSP5"baJ*mU(Tb@QBjI')khSZYe2@aY!C"1Y kk[FqkFKLEDQJPf!-Z,2X108mKX1[%M%jSeS!afUXE'#3!2)"q6F@S0DX43Qi#4Y *%B6(Ec!56(iK@AqJL'LicQ6LXdA@U0%(fSd'B9p,@jMJbZJ&ar+MrcX'"FqP!e0 X50lJH*8'%bJQ'X''f`R"i3b5JZQSmPBK%HJVJK2*HM3NCm[ab)KH'(2LE`(0Hh9 Y(S[9F&qV3eFe5B2FL2Cc!jNKfD0(*r'p8!H2Y35k1`Q[6$r*I"mIcUSNDj4pF,I A1m%+(q5lkU`-26PET9%44Ra-ML$lL#1,2)#q'kSZ6$PmKF6#U@df@EL$%6M8i25 iM%1L2"Tbm,d2(p6Ckp9qS[NY5UUFJ5XSGFdK4'I-%hE0i4&dFMf&6RB("%Kp*3+ cBdq$9qd&$S"[H"5bP!kh0Db5A#DpjmE#)V9$N!$F"4X4RpCX#mFX&G!rLP05(EZ K"CJZ6IMlRB8I0jEMNYaapNMFkAF!!A,1@6p9666&`YH#J[5d,`j$RlX@ML+`XP# !ir4Y'3"5X)HQrYfPT"pRZHG`BE4FElE%KpTA-Qkl42U02id5#JYGVqM-D+Ab"iF mBlH-,Y"*[p'*6"Z%Z(YT4jEXfrAE*4QM`3I+r,FI(DmfhjY*5aD%a%rS6Jp''Q, 6eh9M@kXFTd,1)M3+dQ%)TFUCS6jVk#A2h+R"CM5('TBSR#[cY#EYBb0iQCaiBhZ kI9mbX*c8Ycmh,MJ6dh9@9hlYD14[-6&Rd3pGL!#SHJi3I4JjC9J9I&2#NV5Cl[r 0pm)6G3Y9akI!Y,%iYq-`RqkC)HT-C43R5fRjL!lf6Z3rcqfUX+6eb8rG,fVbZC[ ki5QhSI!8heef&3p8@"Y'),1*H3Ef([+iQ#PP(M581A0ZjM(KeZA$&`C,"&UM*k) 9DqFCC!02L`BL8$1'AGp91P%-J,CmCHi1iK(YY-SA2%!+lCMbGqB+$NA0FIF(SRS j'd$614@1dqhQ60j`$LPbq86h$,+8#LZkl#S6phN*58'B9De9Q8#,$$e"l#*TBF" 1%RQPZ4+b)EBSG`Ce6Kqm,$[R5l!%X0i!q2IrMY8A1*0kia,Pm#Y8Q3+,&C!!MQ) jEU!P$H3a(hqUFZG06ADbSP[+l2YDL-jr8)KH$c`cf2LbG*PVTX3J"YV4$qrLhTq bP1eLE&%EJI(6DEpk41ld%A$9KI`8L2qI$!fG[H!p4#8S5DNH+Z$46rZe8Yhj`m" 3PbU'Gdi6"ZSIFJ!a%-r)p9)-CH('Vd#LZkD)(DQbEYVE8I1`K5MUAkl!'IUTkh) P[@EcMT)FlGpK8V'!95)IA0d#(iaX"a`qC+eVpAd3`*!!eYBc6*QeUeX'&Kh%PJV 4LRU['H,*B4PK8-khXQB-24VC+hc!(T4lELHfVP&LBekSqLjr@!bEjF`YiTU,p8' %6&r`C`bp!4V'(!rVf`X5lp1!+Xj9i'G6"VfGiXIL!CdZX(UAHAS5(Kp64UbAi4a c4+p#$k590!ai&[ecCTqicj[Yh,$Z#'FeA9K9EC0d1hRfmP#cGdC+H0iNM*DLHef c1Tk)@!bPAP[FY[)6iaf9)0AVJ8jd`@@P)j4CC5D)C0I""&"fd`I%Sr3%5'(f(GZ 4#+G$%,aBdhbE!#,)1X!Q"q6rIX`f94EqQJ1#$",ZFRE#2)-#VId52,3@6B$Fblb TA%+Pq,!el5@+Z(cC31Y-1kcaGaeLKT9l'BMXLQK)*b--D1+De,5S@S$BIU&Je@R kd5@A!,0V*4acJVJdY05pCBf)MkXEAVYM26RaD'kBce2&,ID3!'Fbkem#iE(14L8 @Ud,S6j[YJKCBdaj+c4p0p4MIZeha4"j8EeUUQJUE*NLc,Mk()4k@YjQ$H("Y1jX TcFUmNGicr!Ack8+-,L*NKLEXd!laQ@d%cjK(&8IhCd5mi3!*q1q8BZK`ddeiN!" Je'eLI`q%f'A)NAf,DTFPEM&j9i'hKElK@MDQCjMN["jTH4rTR23NaMpQC5jr`Lm 'D`"25X$@phH@2&clr-V,0[EM)6X#&alJA!rVfB`04rq4I,!*r(XJmdG)Jac!5+L KAET-i$rq)BTT@KQi5@e#S(8,6"A)%f,#*"#8!JR#YVq[VeQ&5p3ZY'rS`kh2`[m N'QY1JG28QcS0`S+Qm'Zd`dUre0LD1%6U#N)94[e#($Mql-Q%L#2PEa25jP3pGF3 0KM8dC,JpREUE+Ra%BfV"b(T$@lIpqj9KJ)DpLklm#X+he#BRNAi$Md%'H[XNIdX Tq146b'2MG9E-F4XL%mr([J2"VA8@mV)DEYk8PBHII0-@aKN346pNL+1ZH-IerYr 0Emie[H%aYF"6abRFji'RcZa88j)[,dBUVALdI!f"'4lriFZ#qBXe%MF1a6a`I)T E-`YZE5+mf9Xjc[fM+L[Y`+E60UMZAp688jPJ02apmNdErE68$m-VSjqf*pK*rcp Uj#Pdkj&4I(%kdLh%&+,6L'D5S*Q8[FM9Z!hq&fiCA)"fSk5MI-*HhpD,%q-Y5YN M0ai@B5hL$5"hHJ(`RUpfYVqN1a9V5YE4+!-4JCVMZ6XeV#X"cV*ZrL*+8E!Ur8% a"jPGGUkCq1DDlN*p[PP+cHb[VGNAdeLZV4"T(lYC!%8S@pJ"4HZ&H@@VB4!!*SB ePBI!@pFkp%cb[4dVeV*lHEh$cL2B`f02*8HA`d1rpfDN#6l6MVV0`c+L-VVVIa[ l)iHAGpaZlV'q4&-Jeka&JRpFa&'ZdY"-"e(Pc%[2SaPS$hU1KfhK0'[eXAm4ar! f*kNd$S*eV03J$9TGTD4J)ScB)p'hDddC$QmifV!IE1J*d,a+(di4H1*pC+Pi0PT &a#cBPk[P-YbhKMPr,L@QMq*SmM'++jlXeY@+bAGq2ATaL)da"[`a$UhhC"rHm#U IJ8D-01$*B'"&bCFl4!5leFjpCGqVV@i!QeI%C#%d*p#SK8m@Glk5cXrR*qj2QLp j*",IPB&lCdLd-$qp!k6M[i*,maU[+J8809J'Q#2pMeP[i4,[VpH(!@)F"'!@(N- VBT!!3eHMdIrKZFa@ra&4[PTf6Y5cG9(cCafBRTCm$5bAc1,3Xl,8i6*KAGTh)+I bk@iTL#p(Qra[0fPNI%`m'L1K)-EbiBEJc-S*!Rd9cEJBhB@&YjD&D@&%blf,m[8 CZlF`&0b[cqBYR2$cPl)@ia'9N!#fqF4D@'N(68p,Emf,hAa#[jr2-LpbjA2#(QG "`N,j)b@j9kN95EYhYi!)S(&,kh6rlfPbhM&(A0Z5r5m48JQTT#NcJCkK1V!ID"q Mm12H2p`NFAS22LB[M)eE(C-b+'jN$-'(P&T$V*+0Uk**Kj6H9Df+S&YpAA2ZGkK DKmLp8-HAAY5CZCi!#Z-Fm"%bbEKLIY%k8iD5MlJb*KrM0Ni"`33+MkJ0f`6H5kK !%!0)'kR[TMH4$Lm[mL"F-UXXP,+iLdjXS4#E*CB"CX5Dr@h8ed5d')MLDYZa*#c C1d1pHqaV0!*`D*r`9Z8I$-5p96DFlrIaMUGYk-!9#I)L4)b813"XbLZR-k(lMe" L%"iDGNkA8$+4FIAR1[@XhpYH2a,H92d'cDf[(Q&Jc0PZE0Db2K-)#$YG"",3TM- +(P*m2`##!5f"1p0%$EZ*[J-1)(PMKkqaBc4Bb$AN$`[mYFDJ4JGa`BUfL2%XK#e -Srm9,RBV5%J8HAi3Na55l-I3B6VLXVI`"ZBHFU638cDp6ei%dllKRkXQPkCqei- U)+qb)8i26SeK`[LK!0Zprm'ApjU1%i+[SGHi9U,ECQRG0j4G!8@pRq#+XMj86Ak 095c')pi6Bebbamk0NbU1DB-iH(DrCMZ$&Amq!#cpHVb,TFJMX#qEjK@pafje!6( -rKT%fd0JTUcAVrT5RN,,RP@'&"B8d5U6!d*PMS)UrmFA,ZIjZZq,#)lbr8#N,hK d'&)`I'MAaj!!NF3qPSK9#Z0U''E%C++(G!#VEYZ&8#LVIf[cBJ-2!@0"YIaLJ'% 1)-GYMJ+XA6qMX*Nbj5BGca[Q8p'fV&)VbAJ'&D)86Sf(Nj+[m#&D9Nc[jSIqBpI Z&kjNlGbR@fq4RmN)HDZQ"U`D$6AkLEc!GYIeGN1`jcr%jkMA3ca,'SM-G3f&-dR (`XP$5a0dYMafPblrDDl3LDmja5FRVRK-X1Q)L(rM&Qj,QH)XZTVXl`3KJZ%4pQF d9fmk@+`S'9c*8MQ!Z1jq(qSmPGANTSVpGXRG4N$d%)FYl1CG"#@INeQkheS%(JS C%Kij&EB!G2NM-E%I4-1*l!Ph9,"-[E$4-%`G0#'-H2e3*c5+acbj'"$[JXpSI4D 6d3C*Rj13!!1pFRb)Hmrm&P`NNh0#PjB-1,pP#B(HGXSPra8Uh'khl'X8FE8pD5K VQ+ZR&1kM1&U[Fdh6Ff(R&2DGP#KmE"haI6X(Y1%b+YSYGbV4r&[JM'mG*,X5P1i MM'Gd0Y*ZRRLeZM[6*Pmh8ePRLmN)X,`k8jlaLSEc*L*MVBmQelcDZh([SEF$b23 E-I#p!P1*#Ib0r%lR!V[&l@+A4PdLP`UTIFX%Bc66RT(``%r"!aZHB5i[@!U,lL# 1S)L60(4!e1U68Ll(53)dNeDbLPS)VM3XUqLLTJY[&lABi'3D#[j,%fRHrFf$i1D drm+c#UIAG&[SY0LJc5+%I%'1&h+D'[qU`-ARrjJ!ckj$*@3[&U6ckb#bXJ5ZQ4V d0ckaJp3)Zj!!rq2&#"m6%ZT)#RFN*)`D,cTC@8aGLrF3Zc)f*m5JdmZ"0p5UPfh GUP-Mb[Df,MPT)`-)V1#EKiHA,[HBLjkZP@JJd9U,-GC@@kRI6Ci!SRY@cE%-bT[ l!jj4XXP4XBVL0TCYHBZNVYN%@Q(c',q!l'(`DPTqU"TKdeRf2lENdb4%j#"NZcP lY3M*eNjEAa*AfMNGZ*r$!PYmYRL-lJMdXH#'[+@V'qpZPS'!h%m29ZCCGB1(i+C j`V8dL5eaNB!*$$GDI1DQq88+`C)aPKUDV4$eSM`cMIp,5$kEl1-YHAF8CbQ80'U -XY-8(J@CMKJcJ)c-8[%pe3AE#A)5C!Z3!!VNGC-GkFc6lDAiPipc6'+,MTDhmmU pT6qqUSYSDGMaC-S)pqdZaZ)`LUjI[&-)%j+E`(*3[CPPLcpXa&%f+q1V#6*!GUR Vh'DU@UN4IdJ2"`4,r%2CH`1CNTd@-&%eGQTE,k4pM8XmRIYdHkRehV!DB&-fjEY FeP`HC11BKJiE@8EfU&X*&-3r[e8m"mF(V%Y9(0ZZ1aZ0mD+8K(6R9KLkK`h+Aj! !*d1HrY*p@$A@GS6jA45UCASPfSNqUeVGB'kSk!V(*)42Y+3"`R)TME9V(lp@lQb %JV%%9jK35CjEXiS+&#ZcRLQB2Y%EqB0D2V'McU3jCq')8!"hRf6cZpPr`C'[1aF C3#&YB)&a0k1NmdM)kDiY5[2K*+"8Y`kL(8Y2c4iTfj,Ph%Z'kRm9(YkIJQKV4BA led9D*MZ0%8aY4@T%,Ljf@#c-E9#1dN[bE"k+[X6"bP,P@ZCZ@4*V'VEc)jRpL[d ,reQFLiN!1&L`[ZXa6kNHLIik)aVG5@#0%pFbVN#3!+hN3LXVE6CFe)!IdXJpZiB 8N[4+[5@$`N9m%@ifEL2Nrj5RX+A@(1`P1dqCpAppBF!f[9fYD`"8m'9DQ(T3HJ` X%"b,iUVHGVe`M"-#iX8Zbc3VCZH,ip%CRYB6ULLlQ-eb9!Z1i(&qBPC-kaFb@U8 #kl-aLDm5AeEQ&U&bK8*JKR3p3rZie#&DF4EIiTJ[l%Z"+Ur$%[6Yj83Zq*R8MY' `*-bqfUdN3l#93r3P1bDlSY-"6@jZq86D1irYfDdpVCR%66&&@SpaBq4"h@YX`1B 8aHZ1mJEQhMK'Mc`A#,JHRm8Z"(18cI&l"@c"AD,&'#%lQ`$9pNaaqM)r6FqY+GR pk6U9bETkci0fDNebIbT"!a9ZF!'R6"pcVAQMR[Hd#3*iNG8QeB1pJEHYC,aA*Sl R!mF148m"8kkG8q"2!LkP(diPBae,U(KNPB[L4TC&kf1R8`MIPf!KZiaMk(LVN!" Ld2%3L99(f`[NSJlaEjRQma[QfRpD3FkFF)8$JL(`hdC'+P)9'9p)NiP9Q25Fc5R F02hJc9p'-mJ,#ZH-kTJhc)hN8qidM%46!+2+dk9XP%mmYr6HRQ%"a11&SlSYcae IP%(5deX!PNZQ!rD8+QY%F`j03(j+93(CQBIG+3*rqK[3j"U55#X$a&LB!,MUFQE %EKlqQKM$6B4XflRVD4QdK1Y9DQ8)*1iN5eBReNV+8C(aVa@)8(UklTkRGbTR*Pq M5m,"p+V[hj`j"+ZkdU2BYeU#3C*$f39625mMV4q8AE`[(12[5h!,MZ3,mVl'%Gd qmSb1Z)&QZYdcV,#*cf+8Y6[b`Z!Gj2Y@L)KJcfI*6cS89E&bhPbZ*D(TK6$a0bj lP+LKlcHESXA1EVC@CP@T0R`(2c1N-+)SXb*4BJ1%CL!iaI1IZj1`8i+5%biVK,Y EmTTE`k2B%39qf"iSN[1cd%88Ib`QENFeklN1rP8+Ua+pjqFA[B$K1F@XkNPkc1i 2FJS[2AaK*$6DJD3L-+-iPap+&Fc5cbcMHqf6(hrQ+Pb"U-pX#52D1MQPCZEAX0$ afZjQ5j@dNLN5fi%$P+dbH'N%YGGc')%R8hd`%h2b5dKcjd#EP-@@e#1L4!XYZ1l 2ShB`&R5+YP8)'D1r*D!k(E3!MV&qHI%Y8GEBlF-QV3p[P2eAEl8GF1bfYQ+,J&h GP6GmpS$@9rNZIrHVLC3$S&'J4,Bmk$8[0pD6P+9a'kBL0'H(P"lMGlCiUma"$I9 FiE)ccURY@-)p+XAHi3#(hpTrmKDi)(kMHF(IUh@#8ZV[L"2!66`@LANS-*)&jjl Aq3U9)TEbbIF%YZCdPbH6JAFd3[6-YmHQ`hNEE4@2[hMANBJd9C9leH-#3,P!`%V A*Tbr5Rqh(,,[U(DS!A1EMq6H@6@1&ThM[B9$CDcVUhNPYI!`L#5)NFb)qelej-" 1dXI'D(fCAZE2ZiqDRbZ1DV&2LA2"BL#a91Vq,mII+R8'EA#d*4Y5BBa)&Z4jr6C M@8#9#F5i%%fV1MRpLKGa5$TL!P$['Y"N[SmF299-DeD9GJaRAHf1AV-MSKV!&aj `dk+VqQGkmS,rQN3dURmc6eEAr$LaQkl`qh1H"hhkQj9NEPHV[b43CmDMm&a(U$- BIQ6QYNLfI#9Y6la@ciL#@U%qf@TPHVdZqi4&LDD1'Flq4c6)RNVFRJ)r-FIC85h 3&a*@Z&lBl`mSm1(2,'BjUmSALaV,RQC5)LKLc)6+%PAVi(PeE!M1G@0*Za'#If$ cU'Z+D'+T+`C$eafIr2kZ4S%kp,Gi'(bjkT!!0PdJGJdD-YZ,MY!lM#S'rN@k!5p "ASjbb0J)q!D&`Rf(%f+KaM4EViR&JL-&FJEXR5BBMbZD"$@5[3I00l@+i#U9cdB [c@V2FD'@lA(@PDR58bhr$6b+RADY"TZH3&5bA86*F*`"FQJ&%eBH!8De$DT[Y+h $3KaqQ`4dHX%&0hrdSTRkDP"b1ZPI@-9'k!*6!cd&&Z,ZHR@A19'S#X(9Hidc6B( `SKGej8Di949C(*!!HN-pM2B9qqaYe*6Hf42,8+"c,!X-hXaBpbQApMe[%qRIie$ $a66RXkUR*K)SrUA($)H(1&hV3J3!f[81k"B2a"%*N!"VFmYBAVTP3E0[PHhK2r5 R%hFZQiSATUE)DkTqXGU2#Aq@ZEk3!-Y&d1mU%d"2ThZ*"GP9I"l+VB%[j86HJP4 3'6N'FS*`BF0D3V%5VG+%+e3,,4b1'mSUKE%*M'E&9S9Q`k%+UXblZZRJ43A''qQ hb'jfVD#8q1Epe!U'#3Q"lGd4Zql,FBXQU!3AlhX0JF1!jUY@Zi)h5TpkdMX2HQL k1*b([F`H#[N3&SiBTU[cC'L*@[3iZSpXNiRA+F3aI1UXrl5qT@Ap9-KS`Q2fq%N -*0apKDMDK@A(@HJJ@"9Y59%8eEf0!Ufp"!FVKpTCFba4'VM%`MR0L)BRLkj9ZN4 XV*FK%&%AHhj1NFlr[E%DPKF%p2TaXKrY23"8k"TM0Y1G2$La5hefa8FXS0c[mD[ J[4-hBYkPT29D%#fTM)f"UHq-ebVJeD4f52S36!rdQ%@6NXN$5Q['c*@#eHUD*&f M&SZSf4j,AQLiHB+'[H15Ydm8)Uh-S3Dl)&J+Nf3X+!2Xfa5'5Q3rGi(H2qKpjE` Cd&L2'4DdN9H&H#YrqBT+H1f(8["*mrlCkF-361m5pU2lbmF4l1&XQEVd4m%YB$K ZG[UX68&U(5Jr#RB+AlDXYam14$2TBqMpb9CVj&B''6F0b*FQJdBTa"(%1`)YQX6 XCLep&UU#e(bMjZG*2q9GbUPZZRdJ!@-p!VBfE5@B"JM0"Pb9h*6'#c+A(G%)U`@ &L-4+K&1RKP3C1hZ9A@JH4X#CCjJaQU[@flGU&B-XPD)6dP0rG`dh%,,"6Sp-U*U ,c&YmdGj`,B3"D"cD)Ci&cj(Z0#*j!,,1cmMk@&#+5hAqdJ)CdZ(rD98jHRqr$B2 E6f,dJ8PIk2Akrj9N2@MNhRm-$T!!k+$0'6,8&rCpe[J%prIi2C+JL`3`[j'ik$5 J,iAkRaV(")iimEC0"hD""i3cN!$5JZ(f*%@&,YQ-dZ)YJ%"NS"&-pqTY1Rk0`Up @+"8F(2(ZQZf%f1$,[ZNc53AQ%ZhM$R)mE3U[aYKZr1k$Y3N9d$"5-cE!B6qAQ", l++$EPm(&j"6(QrJmejmD*S5ZPV)#F[6%EG8@!A4D#IFAdHT`h`KhZbNCG8(G4kl 3SIa(f[aX5Q-*&ME[DqeJ0l!D14$3"U2kbBC$RVfB-V('SeIT*H"T,&h5q46rdIl KRAVcXPD!dkCq*'C!L5`29&C2Fk3PMe[LNR)a3S3kCT!!HC+hT`5)r3ld'+(`5L- cmQGQS@@-%aDPp,SpBpHCSqTJS'iUa+V5A![4I6J*Y&cp!H0dC8bA+XTK*im#YA` a6kBrH+%Chb%NriG$cZIX82T!FPj6%p0hG&K'Y&8m803!l,cjGrcdIBhMbKi2rXl [2ZkHA5S[fH`"iPfmTQ`R44e[U+B#f$Kd(50Trkb3!19NmNrk,HFe2kCiX"bE&(Y k+Y-iMH+$Y@I@VL5EC"0'G`Fi--"KcJ90lK66(%S,0LE+P-`Ik4V-[SLdK8*'-fH B3S!qNi)q5h3%M(1#4%M&q%F@64UF[8CIcBHVT%*"QY&p-HcRq4T-fK[[pVK)CB4 YR*[5D)d8#6e[3N%M52kjDf8NkUKS*e((%#hjK3a+ea1XIb4Ia,!U0BUJfU0%CSI Z2jNS+h@EFXZf`3MP8rAUKbFmd`aiMQ[(B!k(K0,q#0i$G0jf+-@1'b`%c'G)KH[ RTK1j#+eB`'r9)G#U%B2KBUejG12jI9C2Jk(['AVjpeLPLfpNfi`+T3f(KmqhriG X%S`GCJRR%`bC&#$e"r"4!c!hS[[R6)4%D45k#EfASHeLT[kaV0EK8*`@a!T6-2H "3Xe,prEX0K@YkNkGj+(+6E[+FLTlBHYH6KFZ&Y+20+RKQ`GdiPJI("9'cm"K8mM (`i%-P'"99J5ETZ3@+L)RNASeaUN(Z1SY"-8`[i1h'aDhH"ZrEErdkJRlTV$iEKf Q&!N5UEYP(#IFBUc%prb[(Xjc+G3HfX#ABU$3!T@0cJrAU6-Q41rXT#VlRL`lA`e qkr8i9f6Xcj+Z9[Ie+XS-'8eifL)RR(jmjR!#mbV"hKbhBA$[R-kd`A+,S&SI#+P MZY01"Ar$9*!!q-G!mpiR@U%SF+Z(Emc1YkNL$p&4rG8EXDC6#UpPB"V8'AKAjEj G1rkVF,ZkVGkd,T@@Ym8)cP&U$cKHG!B@#Brq&3GDqhh)(k#I$YfLKlHlZkliNLN 1C"'I'8941jP(DC3dKBL0[l!b9k0P$BlF(0QQ$6'JXS0[K[m*Bk8R8$1I@[#+@%S @`CdlX(dpiihT[1'eUim#Rb,#AM`QN`,X'$$k46IH))P1EN88CTG`mKQ3!#P,Gq9 '0%&VP*BE&CaeD0CE%%10P"3m@b!h[&iBBYp`VM%04k6JY(P5fH8HK$2hQA&U*!J pk#UXJrl+EZFCZr6fSLqipc(,3P(BPMdk(l0!B'a0qJX2(Np&UJPb9,(Xlh#l"kj @`YYm3#NDkS9(pEM-(FALd*PN*%B+60`qGFjFTe-K$dZIXkaRkf(&BFK0rcPBjL# d&rhbG!8,AM[Z+)icRQB('pN)k#N2A9NQf2h@YeJ2kdrApik4la#jb)FMJAEC*r! CarcYaB`P&YKkrmRQI2BKcJMl513NNYhf1!(L0Y'aDa6F&Y8,$[B8KC3pBR+3!%% `$VcaE6hHqp%F!Y1M@4kJ2j*'1'UP2k0`ppQp"6+&&"GLfE9jDBPF(C9Plq'JKNT +$bI,[Q)F#34M@`Si+)k69B9G`"("P92'9'di6McFU"%4l'9Ur-kZXq'IKAfNm3@ B2IiT`iiCL#Fdjjef)%A*1baSaP5aYMlEiYS4+#Qk(AN+%Q%`$+P380)JBX0dT4( -[da*+HaEA%GMNCi8b(pXij9+A0j'RZG05k*MS-`m!+8Tmj2a6*5T&-8SFf`-%'S CYpZVpZcjAe9TKYa*-V"q'SZ2#q#SR31M!"JYBHk&YQ2)GA%F5J`fU8Y)SZl*UkV 0L+Y"("pD+QFE1H'5)&+$#XbVHiMKZQN6")AL6UG16i)-XQ#&ePVm4aFJ9d9&*-E e8pY9aDJm"i9AY0Pk59FDChCdBq+RHiYB6Te8&NFCZ-"(iY01"+l&lKaNYh!kYpe aFN65Aj8M(D%$196HNhaG1ScqK3IRXDGF&&CeXM"lBdS$Pf"2P1I+0Lc2RX(*[*i ,$P0+eA"LHr*6#&M$d@`%C1RXF0jG9U3l$HH0%L[4h"P4kf[KB0Hq4q3m2h[akHK GjBA(9)4)1E+,H[K`R[Ek(fBE`$Aq-9@&mG3!$'b,D*FKT(#j$H&YKmJ"r[lclKp 2Y!!Q%q,fC"h5lQ%4,UJmP1A+iMGId9@fS9Y9)lS62qa93G#BGFL4CX!(IlIlrJb 9Fh-("!NEUqKiC!Tqp8SmcLS6ATZUi3"cfd"XE@Q%`*,P18[M8J&,Z8leq0@j0lD j#JDp!)&ahRZ&)*e4SR"&9eDKGfDYjirrVU0IRl`4(X4U+DEiL+2Fi`Yh95-U'Si QZ[3JGbG$q6+21F0fm#M3-Jp%!Cm!kT!!`ce%lVjacf2E(S'A(epJ$DX*Y0a[`), mIhPr'RrSIVBm)6!qEGj+,m+9M@-&[%"!php$bXS(KfZlJVBMYjR$4@0Sl"1F4HZ X9P[X2Xcm&H6ZV0)A&b2'h6CSV(,2mdhENl0!N!!1#$TY6,UJj+8"1+aHR01iJp! a2[l2@-GD[@e$I,Jrc!"l59qY'Y-N([kClAiGeID6$@k5['J5d$fb,UV*'G0Kp4Y !f4$@de[5h6")V"pIFV'2&pCc$8mI3`$BeeeiSMIm0PYT#04YbrId[A4EdM8AB4h -M@YCklIE'V3"bf8f*S0hDkT%$GCIkYi`i%m48bX)UH@EaeEEi[GqXX4ES"AGGB& djf$l(4k@)4aTKjIAF+(+dfqfLX&j@lV'Khf-V5VC+1KEkPcKVGj5bB6m'GlG-qf eCM+Lr&3BSU0)25+9qhbP*Z2q21KE8$!A!60S+5E)jDrkl3RdP,L5H$',T0&AfVS [MT'YU9cLS1%%FLCBUL5fdmZFY0#NB(f%90N+kR0`$$8("%-rXVc1MY-qVRm,(R@ D-j8[Kr)FV+PQCS0KlB%9M%T@(4@Q`Xm+kZpP35A`kH(9Hhc")FF4CNc,I`3c36F ,J"`PjfY+-H&NKM[jchAKj+H3!#amU"'p0'U%fLAe$,XJjKS%)KRN@0famZTlSjd b#1fR&&kMLkUeFZVTe1L21jeSE0`DE[PP1aR38bAXH+b"TKHZjfkGQ8VUY@9ZSk6 G"983LN06SH5MEK!@-aa'4+U!D,,0T!mk1FQ,(Ab3!#rdC&`81LV8NfmVr&Ea2AX ![rM`ZkMXLRbULVV#S3q"PUkmY,(eFYV1TT2*9!&`r,iJI0*&eJ(Ip4RU%&9A)eD !4IhQ!90)Eq0hJN"Q2#Yb0)ldVKNQJ-NN3(hHDQL'UTdP[kJGrNIHXf!-HqIB$+4 B0m1G[q"e@mdh`M2hUJS&0e4MZX(bABAUjBLB`a2(IFikiB%Y-qZ@a`8NI"(4E0d #[D8jTkD`F-0k*[YfHe5&[K1V`RlS+KbH(&"*ClHjj"-Dj!*LBB[A1lZ4EYZD,Ml )ZMC66Ia)[!(,9[a1`Gb,4Z*"Q@2J&E'(!)S`@e'K+@Um`hfZSCGhFrc(5&A5'Ah 0XUA#EKD2Z$"kk25r+*elk"HGlZ%KhdU6%S(&"HKPXdBHd4B&&@(L[!QJipk5+V& [*&(Ah$BdMpM5b"X9ppiG(P12CYf9(JT+'5!TSTp-Gcq,!DXldahUQX%c[+*k@*M qC@lqA4Rj&R'8S6lK"AX1Jk9KIlA+N!#Z[N,8$,"l(2`eYVc-E`AB(qrUb'!"BEP fP)FQbe`+*)q!JDP,SXh"2&N'4(TiaIkNqeM`#CP(*DD[Qp5@e*FZ#am)*6[dRU, '`K-B9+qJ6Y,8elY8a+$cLFb8Crcl-&K'PHD2ENJE#K$S+4%pjeYRjTXT1+SE'0H 46F'hAABKXGdP`Q*@'r(GhEZ`T6L'[HlP,MqqDVBU1HMa6FkKRCmDTbqp3A5QImq (2bK-L6XG[EcI5LeL`QCfQq9E3L2VT60H4$"Ap5N[9mMjhZ*'@lj!dmqNfhNZLjC %a2bVcj`hrf3Y8J04DYNMCH'""E[h3NC"e8$C6AdNrc`+(6K`8UJk`993-!kj68B K3%DJMP9jkj(!1LFY@Q)+j*MZ8Xi!U8(Z%#FVRI2PmEkm$iClJTM)B!BFAiAN(UC Lm+"&[5lIhMBYpkqT@4m'CdQ-eCRU9l&N[+H5GjK-``6m!NM6Fid6FDEhjdF!j)6 eC'2SYck5%i'j9h"khj!!Z%f%mN*"KXP"!+KdKQ@44M,D40$9%,3CHJGe&NcFN!" `"Q)S&1bGT4[!Cl3e0%m%Q9J!YVP0,c2-#!,1mr5"rdS02946k3r!phaMIdDUGrr 3PQ$9e!X#-dYR'H*&aJUCHVL8S'h8CT!!)@i!iEa,pJ8AS*(5TVp1SdirlTlc#)` KE#6A@CqLHZc-H'$&LM'2AZ'$9RD0NqV-I65`lhjiUA(hB[L3!"04ekrUJBCQG$D cP#)T@aKpf4!qNmdQk#L'l*NbrHLkpc$4&&(MIHp%CK$`!iTmB[YdkLi(d',)I0* YJ+QjU6Biq5k%[i&NmRfDaGdp5M)lh(Ma#ajHb%mlPY$*SFdej'IMc,"DBTeGdDN #QZ6K5r(IUY%35(f21DpFk#X+)2%,ZMYIBRB%bRG4mMIR-S9(eTfA&2Z-4H9%3[c *%K-%EpF!Tj@*4fl1'"H[(#XUQ8GH@+CM!fC1&kk-r09jN!!F16f%JQqmJaP*GAL F%"L`E88B'X0T`BX10H3Zfk-iZrk5QE#h(#e6H#LUjUj3U6&QT3T,dFUk$CmA,'# mXebXBfZj5LU3!)HmL%d-!A$"2FJB"3[mF&Ij3b%-'Q6+pXcQ@RM3KpLCcZ%-@Ye )@q-IVU,-94Z19$XX#ekMbBaf$DJ2b4KbI+iKr'q0Z"NNl+Lh6J*qaTcB@AG#0XF 56q0""jV1+Q4,N!!AfPQAF6Rfdl2V*+1TiPLd6jVd%MJdf2b63A1,&[BM9rf[L`S XG)+&A)RYhaGj6+!0d%ejF6A+QCU2IV)ij&iTNTI%fpib8CdAULd1rZZB$h'r1k` heKf04la[j5NUA(XF1H25J$kmLNF+Tab%Vhq4k#(d[+(ek1I-955"L-,X!0#F`$j Zj-DATr-DZ5501#N#TfemP`1ITY3[lT(G&!0iDHqMCjTGU,KX04bkHIKaK23[UGZ NbjUF6aQ(*ZYB"$IEbA!Lp0f@3AirNq5U21+3!&ZI#`[c#96G!NUdl%NY6aLmb!5 DaAd%!VqY9rU')a%jqlCl,B!55cMX&Z,[ibB"Q%IP[1JX(qa)--1k-@UV,jL3!(' &B62bT%DXqkb1(MXdLS*3(XHN2P#SPPj8r8rL`DQe)Dj1B@+-2F1-&eD*jL-S!ra 9UXQNlXeX%A@)V2f,G2l+4LC))#I(ZLMN)TRLh)QR&)@"kmD-Ni`EqT[&iB9ekIM adZ'1qEp-*)QjYA4f(Y8BM@ZE5bQ$LCSbENf68a8pZQ8J10S2M%9JcZDcTD2H3Kj ReDCD0UqdI)()MdCXS4Ub*a!b%5`rISZKM[b%@+'C3N9d3Gd'+)65H*GeCA'Q#aq 5&2pZ(VajNHbdBPY6h@1rA#e2R+1B[IdC`H[1FhP"U2FfE+'m@erJlRdhF0)$U9c 3pGQEFS82ebrLCU!a&*0JcYLSq#c56NMUAlbH%`K"bDk@0E5-jMF$A1r$qi+P)'Y IQQjDQ`RN)j!!R(&5E#PIKKQRK9!Rq#3CF8DZ)!#BCrGTUkpJN!!epaa5*E&!$r0 jrP2DG9A*f(3RRhaFVVRdQ(*L&EBKkp,IF)imL@JY5L5qp*a!d06qaZMM2!I#kHL C043V1fDQ06VaE&@b!AeHZ@k-Hc`H44ehk&+1TX2Q3ma!AH"%9DJ0P+MHRM@[Yjp kfK)kKq!b2,PZ$ULeU#F4'BCeIB)GqahcYEdH8I1Z$B"XK'k@C1Cp#!63K5K$aa, !%NE1k!j&,JeBrrreN88ciqU)IkJda1rI-Fk[+0`GHQYJJI3q0ZSBXc`539c`$,R PXX06,$H`e-40F'6rckH((0,&G)P`cLJJDc(YeihfhF'&DUe0SBHDqFRBh4[8m`U FkhcVDmCLXqXD5hm)F(FiSV&Z8E-ZGPHMN!#ITJMd!4TZS!jr6'N4P4Hm3K'cG2E IE,+jAApah6FP2@A0"CC-Z!R!-RRc2Y#j2-QaP80'pHj@H5DlZk65F6%!"Sm,Z,d `2hFe2'`XL'VVmahcCHZTK4j5["&K#bhGeHCiE,(9q*ipicIjarpKkjjbaV5#jD$ *&Dd!(*aZ19HZ*e(C1r9Xqk,5mPGTDFkY6iG0A(5IjUaSf0+5N!$IG$KmFMVh$@H Aiq%&GqZ,emHZ41N1qUP'Sr&dFP`F25Q2pU#BK'G"f3f1*qG4S$Ej8ij0hhAiZT8 r08[k'lJZHJXbUj&+P[0KYLb"T6H0#53eBpbkX8k"fB8I2bS&lB)&Q34GJ$Qj!!!: \ No newline at end of file +(This file must be converted with BinHex 4.0) :%NeKBh0dEf4[ELjbFh*M,R0TG!"6593e8dP8)3#3"$Qc!*!%meK6G(9QCNPd)#K M+6%j16FY-6Nj1#""E'&NC'PZ)&0jFh4PEA-X)%PZBbiX)'KdG(!k,bphN!-ZB@a KC'4TER0jFbjMEfd[8h4eCQC*G#m0#KS!"4!!!$Qc!*!$FJ!"!*!$FP!"$D@P8Q9 cCA*fC@5PT3#PN!3"!!!q!!$J30mZiE#VVJ#3$3k&D`#3$NeKBh0dEf4[ELjbFh* M!!'r"R*cFQ058d9%!3$rN!3!N!U!!*!*TiB!!$M4!*!%$`"#`G5i#9Z)`aQX$53 '23AN082D4Z,*X,mNZS!SL#31R"m"Z%J+Tk@balB+EfiM9#ZJMlP6%r+9Sm&[@@a @PS8$i!Z1IiB@&1$K6PE[E!3*N!#J#RUUG4p8%pB+k9c@4mePY$rNcqBEZ#M(MEi eaJD#6$X'1L!L-rC"rp&1(!f$@G8ZrYlDah(Xm)q,Rq3jZ+`XE(qIpBb[q614)"f Q"L9UbURKVNiqXB6iUHJT-d*0Zi-(8jFk(1ECFY5MiqmN6$hQ%XY1HFRi0S+b0!N cZPV4H-hAC@ITMJAXY&%E9#a22jaj[)1jjkCH+VRR*cT4,089&d1#K$&CemTMX)L $qUF5N581kc!Z#lRjVPFYH[Ih8Jfbm+0TQNC1K*)a0LpB&lN(l,%Z,lA)3!UpDhp +l1X$iCIAVkQR*lV)a@JqDVqB8haLk6,k`b[,aKfRjdlKLc3q-!5*GH"4Hl,3&q& Ta*Cl!JTRbKV4)ki6Eq1&pF$AjNI&@p9'"$@iqEkc$d@Eb0ABKLmk(jYq#Hi0c'Y JF!2!Ze!Qhkpe[m8a2II#p3bjChmYbVH&%CAS'b0#GIXBGZ0`2Q,V%(%[PBXZ0`# i0Fe@69$"G0hBm!M"QMfl$,6rc)-r#HN+1,%fj,-JR'(jU!qpIq!l',UePfk)rTB $!U#%k-!@,iqG5&"Q&dZ9-L5Vh8@Bq,a&"!a`9S9X1+6QJ,8@qX368qIUH4F,*K` kMDD8TBY&lEj+qmpJ&peBcKZ@3+V+qYYq@AD6aVU2MRjFM(mN[q'eA`c%8qV"pla #@PXBp`lq)e&dri**8+TAMZ"Ni9LVY4ZIZV*fI5FRTTkZKYAKVJcbKpYN6@RP8`b H+pRhM$9lXal#'93!ac9$11)FSL"UU35pNI[##-RaQeM,98jAjQ+&@Mmd&PTL[,Y ZR4IlHeL$PRU'[[2b,5Q@BQPMC$PjmpV*BP(Rc9K8r%G3&Ufdm90E@SeaVirA'iY Z!*'kC8!&"@%r@mmD*QJ,#RFipepp8"p$BK&4LCY1%Np2JlIkqjRQIjIJ1)2PBUF &1Rpk,%!CBH'Pf[Cq![8Z+-qMjj,m-F`jrbl[plb+T@G%iA)LDS"m`X3"Ah'2*U6 1kie#h24rHaS8rMPTXZ1E!YprHTf!FfKD,P9`Je$G(@%&[23dd4NRHa4UqdB&BEb Qk(*4f2ZIeYf(qTYX#h$,6!P9,J4C`kS"D8iSJ'SHTEb+RLi`$bUIh%Ie`Kh8d@k B$Y92SciB80LURpUc%83mj+HTKBXLDaGLCV-JedMXe2M"&R,,ej,J[r[T-T,MNd- bCZD-#CXUUD1f5fk5T%Qpd1rk8),qpi'Cf9ZVf,43,BYjcekI"@TMhDK-f%""B8K )[6,iV1GS1BaUr[G-&GqSP[HMkM+$$dRZGSj8#2XpXaZYp(SiNjD0NiehP-5**8! CPX+"TIIK(M`Z#,SlJ9hKV$@+PYU5#ebEc0DS0TNI%$%TR(pFp$LA,r3b5D-l4E1 LapfmiC2BpUU3!+MK#&4Nm"C,FA8!V'53!2FdaZL-cH-%3RfL3FU"JlQL-hJdc$Y YVa$DCbmrRLAf5kql9-Ea5Bb4NKrjQdmf0P[-C)1"ZeYaT,NFa4Q(H9TJm3X&N9( *!p0X96)LdKN-BEC-@22AA6CF$q8cfR-NI,Q3!#I,TkM0i+$MMr'ibT1(SA33*8K &'L)iQRqTAb9'(3GB2PAi10@r%K#"QQ2R+`q-34F)%F$aZN`L*()MN!!6DK1G1c# i1d'@[5e0M(SM&HU+!1pPS#5*m%PZL,h)$Z08f"PEb`KA`VkX1U$+2h%,"U!TqlC 8#)L2lNKZc--$AQk(ZVZ@cHj5,Hd5Rl3d&,pH4N!-p[j-)+4YMH,lj[A9QLqL8[C QrGKlr5iNDmHbDEV)jL3VQR2BhbTc9TALfj,fPGr)E-lfkb,D#mIepG81HPFi1RJ 1JmR3h(rU$h6q8QT!aN(kI8AT4GS,9AMj#)6m*X+8ZNlQ*B8eeYVH(LZ5rk,-hhH -6$RS&Z8!S$IB$'%DVea4T)iP-6[JeN@$3f-jLIak$3$eb$l9e2M4F@QDaJh4Crm ZFISiCmqPG5iqBU8GX8,H@$r2KDe8ZAqPfPrNfS*iJq)lee2Y$KrFre2MF2Li2ha !MAZEYG',S4C+Ck[*L6Ti0mbMcZKD*fFGd'2&CVGcYiGkVZdL[k)q[*9ZUqEK&MU $bU9M28ATK#J`bD[PPN&kih!fDDH-'%l2FM31KKS#b&[Lq!90h8+ZJKcGf+*'RbE VHHcPjd*['GHIRF9+Yar@TT1J&%E`bHS)k,DM++D[`1PBhCRb6&8qck-q3P!'!d% CJ,TPKplV0fm[-Qach4RFXBGY9lDD,ci!j!e&H(aFN6mhXBNNTJ9,09fPdm!CQ@L d56(MJ9)5@G5m#a3DHXM"J@dS$Cl@DC'Z4i&jEP9b#%QRH2HqA#Ra&QSr0kRVK$d 0,8rSJJmGTml1eDJ%hAjbme5qf6VGY6(9-9EXjUhm[5aJ@eAeqJh$')mZ$8D(m%a !B6Cd!Dl2%5U9p[XZa"*iUjH(BL@N&REUiC&6QpPE1q0hM,eVHJAGf,%(CqJJcGA V"+83E'!mYTa9Z(HS0S#%$0D`LXeb*4HN+d)@FJFqNU1rqkh,)Ikmi%QkbdN*R,E J6Aj5,X-"`ATIH,q(3N0c(Fec)eD8UJ,XJkTi5i"@,LHVRc&X9Br9DAVSJV+N@59 (NSFSEfiN5phiZ-NL8L`bl!A1ejZVcr6%4,ad3LpdF3Te'Jf)F%iMir+@CL(RUkQ Y%-XX'PZ"&`RmL8PYEYhjP!&mMXqrG0+1'#Dcq4pj$M,6l%34*"(H&Y&TNR`E%kU Rk-#Q9PME$TYTYK)5XD1$3[VaAApd%KJGHVA%f,$!8!1AB`8(Pe[@$bK`,rSJiBr DSK5,ABV!,#`P,+YKb["T%30UkNScNK+QfL,)eFR*E`pU%RS#")c8a)4bZP&0*r6 8alBCdCk0Bi4cI1+`f#),Bb8#q`,+!A2[ZLGaRUI4e+[AIU&cL$)Y+#8cMPhEC(e deS8"-YJ9eAFf$1@3!+6K-+a2CNb"QZbQkJi3R`Par2-!Qrb22@Ue9J30d*piC8) k&)$2+%%2,51&pe"bpRA[Sc(,fFIkJ5QNjkp"lmA)M6Vc"l#e5d+r[m1k0h@a%Ei IlCMb%rHq-pR9TYRDVeQk4$kIR&4QCj!!dRTXDJZH@Dp6rqaDa6YNpd%E'r+br1& 4,4(ppjkf9UEMRA(%$qh0f$Z2MMQVRY8&*p-Y"eIc4b,(pZ+rH!6)%DJZ%E"Uch` IZ-mGJ6%-JpN#@[3Cd$l3e,#XmB*K(GDi*-Y@FG`2%"d0bBK4dbq!ZRVJRAbIb34 X$I3fVUpCrV"fTQ`EAr+C!R3rVYAE00K[9jPlCVSN`40$RL,h61RZ2!FE&@[-YZR IG%dTMK'Sca`JfiKacUNV+HDJFl"%0J2SF8)H0!ASY2&f4EKBq6KbDN1PM`Ih3e+ Dd6,(9NEQ"ThF@JJfAVEZ*6'-q`TNBQ9Eq0M!q*%*QfNL3R!c6Q39RhAF&M0*JXJ 2IVQ,iXqPb+YHm8hpC@Tb,r+#aNXZSqHVS-1r$[9eSiF$U+`0D@kfD&iEch0p1qX MZ&)$C"I9B"K%DqD+3N8IYmf"#TTqq[HbFTZ(@UR0%D-$2%l4LLZ@8Eq#cI0!5-c B(mhBmSF#i#kh+)pMX62`-Q)GX,Y#R@5bFl*i36IY09i$%18dpK*CVq"j*i''"'X FJ0iSlf2MIZ!TU8blGSFGe0YM-ESS(5U`!FVFCfM@bSB%"(ZNCGSb9cZJS3M3Fie k0[*SbB1bZhYjID!I'S2Pj#6d3mlUFBakCL364Bl*D*XUc#NmE)Tp(*d$2K95Zb' p,$FQq&j(Xa@95QckNKhP3fSGNKXiK[9A28l"D-!UpcieT9C9h)f5ldcV%HcKNLE iB`Q9@5eQMMI(fC&cAT!!UrH+GBGEC86#bP-0SEA)K(e#K&a-`prq#LPalFY500c KbGZIiTC[K#AS(UFBLA8M*ZLPK$8YemJrKUqT9,-9Jp2bFSe6aN[$L`hYpf%S%k@ 0JG[D'ddHTfchCaPFp-4kYQ`3-[+@+3i1F@S!#[d!-KML#0lSa2L#IYZP8,[mAH5 $9J&$HYdI&T%,L1Gk4KcQ-e-Z%l1AP#6c,3,T,PVI&ek405cEm,,Qq-R)Ri"kX"I jj!MEAZ4K'Aid5a"J9d12`k!Y2,2JE@qCFK$fGfYd'PiNGN%XY[(m32B)r+fjJ8- HIBi6![!DPES#T'@M#"B#Za[r(BRiJdSP+U`@i'B&4Qe1G+GlFb3LK)#jYGZ`Q5f BUNH!DPLXRZQPZNIlahrb`!9$cUqmcfc#%Ef53"PL1L9G3+$T#ES)0pp!(pSG,Uq )STpQN!$X[5lS#Gj*YHEA-hpAXkX(VkSY3jdmI!dCr5laB+(-*BK%)MC4hVRlRZI &rZ6d53e+D+CYVFcJ!5'([VYedb9(%,b&D$CNYrd@YGe*hKYq98"[9*AhZd4lF+a $E)J%Uf2@)#R$A$lITTZI2e58@QNPSrQG)&iHL@EZAD(!,bFQ&i'DL48ZD-[YD21 #r)-VU'l$)qESEN3-#DVHkK2",Xq`NZ2'Imr5V#4L8Q+brbql"dr*$G)N)C1UM(J fENKab-RDI'"1B$"N$'QlJc6KTai@1iB2q+-43SEbdMrY'iUrG0hCFUR)C1$fpAd )49X1pTHd56SrBZfX'GU$Sl[hK4c4LBbj0[$*H#F'TC1-dAVBQ%Ei-(%[FC)R[Di D,V#k("e)1p8jq8ql$-*3b#GAH!2p6FN#ZT!!JF4&MlR9jSPH-J+EK#iHM0lQKM& mm-2GjJ6F&1rSU$$AN!"!EAr'05$CILbd%8rl-c+CEH12Upkeah94&$c5jkLF95i 62"iea,DU9U0+2*&,Rqk+JHlT"AlKP[#"bH"(Kle0Khb3!0ka(lSYYT'I4K21lmB El9E#C-cVU!LK,b2!UmV,GDME1YbN5*[Qr#(8h#!+CaGAmee9'fl-[U!G%[Kb-+l j18lLUe1lHjRXAX(#2'p`RGb3!,X[A9Q@ZZ8%Y-M,,D3,LNHPP@'[QSR(Gfqj@1H `*rl(aB6-3M-S*&rqk`$reTY3'DI2Kf+2($cV*LjmjjI9D$8A3q8PiM&cbqc&1H) @e48,GLY5F3m)K2@YEampQH2G18k3!0mGF(bH$+!(%8EPdEkE[M1%dZm2NF66+m8 rJ6i$DX6(L9k,,"X@JP+j`e'm*d&4NllDp,16%cSiNTMMEpp@5MLZI2!Gq)UIaQ" VKEFmrbF"ClIFef3ATm&TGfme,F#Dj18pcUX0QG82mkRA2TA6$DNAIMHfCV+d5M, Fa3,RIS&c4DlTIU&e%b1iEK%EX34*5ZEJVhmjR&9P5E%X*eXj3cCAe,m+(V)FhLS KD%h4CbqJepE13LhDY%,Z,A`XmCh3(AlCJF3MqbBT$-U(8LVSMpjjAN&2IK,E'9! `2Z&(r[J1)lZe&laU$%"!Hhf3!(JTU$f!@"6%-1MGf39G2!iABRrr0X94AfkP@Ch i6R0,!VQDipE!FTrT9PJ5PA@V5aLek89!mdV9aeJ3c'H(pAk#&EqmCa"("Gj'J*P l04b@%,CF'-cP8YjB,#bD#!$l!4`cF-K,@lJ&6jFRem1DlYAJB$[YV1V#EB`EBk5 BhihY0TLd+9R4!BVe&,'`AmER`QAc9c`el2,[mb@Vf*fXScH0U%QA"9aP5H`kr!I SK[!IM3j)9G25$aRcklMRR6qhrK,fA65X[*TVHZj$Z9jQBPScr1*#'#2F4kb-4d% *NEPKLhj0AKI+f,q+aP9,VK9N@8c%BXKqD6*Xd8jbN6l`rZUCrFQJ2CF8`DZ[8SR hKjIV8585*5(A*K0NA%(LUY`J4ZR3"(05E,HN*$D)A&1E*-LPCD#CXP),!S3Uqk) 09dYmbiN2%@Fe%$@PNb6hF+q4q$5@-9aqPQ&SeZ!HZ,pC-J5(Eid05q,SA*'diTP Y8L[rVl3djA$##Kdc%H$-BCrbrR%Z%i6Tb3j$2'TZl*-13%m28mLG*TlPNh02bk0 8DplK4Y00*dqJjd3,@E+`AVUMKh6&c*!!-VmSlD8Zmc+I+4hDEM`TeHpSIHjZrZN iAK`[,`@9"KG&`C-GKVmI@a1@J5@q(b(0&l4k$1IR&2KR6(IiKeB)#20DIk%fC9m GM5DHVKLdiIJK!+8KFYAh@+p9`(i0mZMG53*9#e*LUSaKMk@l(15(@pJZdlLZJh# q9!8k-c#[hH*D96X'$kd@q6SN9j!!9qaIFG0KNhSaSF8S+hIEBjDlbYqVHUPSj8@ 9,2V([l+[Xial6kSZ0J0B(m`h1JS+XE5[jQ3qhp)`h%$4'Y4&f"m$qA85Ei9K#0, SVPXV`j%RC+Cp'3ZQ1QT"-IHjk-kN3dIEZ&&M8)f*i`pfe"$ek[U`4&0C`1HZrCL bjmaYjVBJ(&9AkiHG2LD5&l#JfPNBbjH3!2dV4%5`Mk+cXUf[CPFAG1'@2b9pdE, T8RbQLpqM[b4VHqA2SieC1)YfRY,HTdH2d[c2!"DZmSjVc0!Y!M2E@+KK`JJ,qr2 K,I#E`&lbCd&3MA6q,Khl0+YdaepaNHhMiejX"'V%e'H&HY[h@BPr0"*C6ZEJCer q*,%[C29chTKfe+A(rTP9560Q020PB#YfPb2h&ThhM)@0%MLS@lE*KXp"lpD"p-U LUpp8XGf(,BNbT-K#VdjfG)bpV)JD@-,!+D@16f1rP[`DL8a"fhFiM&6RbFe*'Me ++NV58mhC8qT4G-pQ)UZE#"#%M$P$H"0bN!!N1eKm1#(iM"KcrkZf"9UEb0NiP%Z UlIVY#H0MmPZIrfbN%Za(JH*U%VN$9"DK++JrQALjC'ThS0lR),)*,-%9GT!!PS+ 'bMSZ!F-cCTaAXZQ2mHp*REX[5efr#@`8b3%*a5G0$f!@MTULkL(2M9T2aQpT+X@ !H+3p5*@GB4bXrh,V32%$Icm14aYIMaaNEfIHIIpQP5VYZbU@E*k3!#QXZAhjd62 e!MU`GQA($26XrQIT2kIKcUq2'ck`h1@bUXqhpmSibT-)4Hi%JUZTIarXQ3!c1Fc 050&6pA6rRhMJlQ6@"T5MDVDHepF1!pYV`jiDqmaBN!"i)KI4PB(9kKY%0TcpBfd 4LeC+2m93)m,ch)1M[`ZhrPl8U8#pJpr13T355$1C,Eq8B1&!S%ML2"(h(JVN"c" PK3N2[$3@rbkKVSS-ckArD)a+Ab#b(kpc%'k,$8UG96DD"m)m0+jl0"GMUq`5Ma3 bIi"`92cK,4I[h9BfJk$a0Z@A[#4`#P48"9+DX"'aEDiDjb6k2)(VKh*r9,Lm6Xp #M'A%BKEXmbAM,['3!&eLAAJqK-(RE08i6p3FBKFP(H!M2bk4@$BFpAXBj@L6CkL )Ur+ifpqKiBQQ,HfU8@RUiUAS6#2k3Lm[fK93(4d,UpEmm8p!--dmJmB3*Tbh(M& a)LkaLaNePdKpi6@CcSm&qerk[EeAEZR(qF9Q@ZUJ"+$e3*aQ,rYhaH%B'U$dAMU 3!,AZ+KiD'rmcfT)Y!-i6("UIqeqNL`lqa@Y0rH[#$q*aQ1$%L&)ZYL*-,leB'dN 8aAGHU,rk'bI!@l*&QFC8U+b'IEZ8J4XrD),"0$P-`1kH$eC`AMpH"cY*m!3iZ-& KjTHS0iaQSBe$DK(Ci(Y66F*+j(&K2N!YG[8Zr@'RXKbfT0aLR'[*e)D1lCj)QL8 936N#@HbmHp"&8DLhiJI-+6U4f0CDQl8ZE,6)3h3QG4#DZVkC6I*Sa+h&E34+f8d 08CZ4&SiEr184heST'iahX,!l8,IUKV&!A$rC&rC1kCV,(@QB6[X3"KCLrZi$8RJ V,L#Q22P2B,LURe2j&jc$D9q6#M2b0%S@K25eLNSra4*pU3Im`BMieQ(K83I`F56 AL'f"I44qYKG"RL4[LfD&N!"dR!Lh4FKkNY[IBM26Dq$MmErL1"*-"edU9+[#jRQ *Pre4+)@@E'jm2-UCKF8hJ-(Gm`$eFKS*dkfPSYKbml&h`Mj!pTmRZlFaRb8d[0e IPRJA@Qk9bb8)LI3ZmL%(aMdQcK6#cZ(RYAaH1&h`*"RPKdp[R%8Z0'N[K&'0kD" MI3'i4FRc$[qT--3BQ1Iei'TBr4#TjC!!ZGh4"19a+N*ahdQ,D",p!56fei[MBKh jN!#k!r8CJ3[4i5%"#R1J@YMFh(A0I*H0@##MqEB-kqK`LM'#'3I%PHDD3Xr@CA2 bprqHS4@dG,C3$@EP@6HI%"J-3lZ9TkKk-AjC5k`j6%2mj3P&Q(Y5jG*V!V8fV$Q $9ZTXQ05%`&H2`c@R2GkG4IhPrQ)j''SlAUer8e'%c`MHk40!iG64rQKfNaX,iQS -KNiI"i)AiXb1Zdi%E'&K3$+Ch5Y0LFZ$cEQh#L(ZjqJ6@V4S#GKilei6bZe`lU- %IQU[i4M89!+dJp-U$dT1Y'lrl`NH+D6er#D'Dr%dNaFGYIXjF$ifP!Lm45G*3r1 (dcek3-ZrVDRY40Qa*NJ''MmJ+P&J@DCCVJeSM,f`1cJ+TE'XBY8*JYTUI1Uq4&K Hid#`B!0iEK#$PJ*M6JahAj*F3)+0jQSG3b9M(kPC+'4GbD0Q"Y@GFIl1m(PE*6A iB$AZaqcLDC@'Z5--k+J(CfX)*U`kVc,YQQBDR+C*X($[+rm@)m0XZpjYeF4Ti9F @5GCD,K*&2IY9eLhcUe9fh8e3D15PIDb$me1Xq+BB'md[N5qXE0"FVa6M&eISKrG lSV4Q006E%IVhV()&VlQ$N@eKU"rDT1h&PIG1eCYE1N5KqpXaTj!!L$5Dh3IiDN& D5[IH,E+r9)DYVF(pGEKPA4-"@J9AR'rbACb(93ilDBm@$GADCH"4rl6FEY)4f@G R2h[2j99PQYQ`#L[-$MIUq&1XXEH6c*!!i*h@lim6"GE#VqqRbd4VPceUM8&!#,K AdK6qIH`FB@[L%dpJf$EAY)Qp*fL`3IRF6&1XJ6X5#$P`P3Lb&$UlE1pTT,*T9$S UU)8kEIZ1"IcAbClPKZ+aH'a#l+"`)A5YiEY()c10'0rX+,"@)N#2qdmb+rQ(,Il B*GB4DfN1*EDCFB@L"C4cdYNkQ*Zpe--I3fL-VEiHlip[+Lk6@f`r5EblH&VCQ!I #bp$SqpC25EE&,9GLV&,HbKLCk%k,V5DJ`k6`SGAcl6NkXAia`RmeSPYY"fAq0[b lM,0`TSj)cP"CN8Z-G85@q*!!N!!!63%U99A%Ep(fh9jRX6+FD"6Kl-0cqd#&#-6 c3$pXG(Z"BhHr(AaMCkb&`$#k"IbT#0C)RYVbJU*eCj`'[@ClarR[$i1$CJB-`3H 'Q56Ib[b8H(J[m5SU[%r'PZkP(8CZj31RHa($I1%ZTEe'UXhQR2#q5HLb"-"DS69 hDKp3KT60hX"'@fcZ4Cl!T`0jrF+3!(Q#2i[Q@Gb#ULp9Ee$Fle2N6DjhQlAdAM1 qE13065%JG'kC'V4(Qe$QC5CGp5cbTZELe[PmIfLeP&pJH)3-rl54dFP8pjGKKp5 I21c3q1q%e5S0fb+LjbI%1IB5Y2dac)Ed&ClPCTh1fF0@0+c2YScJYJT0)'2#-Ar bU84f6'q8'9+bUK$9HfbEQ$AmCU#B&@'bkkV2q36@05iaIkhFiU$D@8!m%+e$lf9 `(,V,a*MQdDC!rh0p"eb1h(TI+DA$HN%SflKj-U!A8BAZ*lRX'b#bSF@ml1UA6-8 Fa@Febc2CpJR%*'qbH$H6X%HUcS48ic86fPde#h9!`S`G%*`EHlQ8Ipq!P`MQArK (@c-eRD&Vb&Gm#U2P-j[-m2CRM0HhLYrdN!$3XHTcRHJ6HjM&@ZG$+YESB6@QIKl DHb1[L6NBQj!!M%l0jb"4qQQpj&!bq#5YR"QqBqb*mj2!i!m40B+mPFHhe&NakKM Fl0Trk42E"VXHZC-H(ebLIqNfS%84Zee&VE(U68`F31JSX3QTm8*'`f!l"ULP*I2 B*5F1Y15J*mppf#!V16(01m5+8a%I1'pED3Vlj2Ul)bdpL)TGkbBeLQfd[ieTc!* +%5d!j(Sa8`jmGLS3GG[4Y8YmHbILGe0G9VlUJAr*NLe`kBJXhlqR16`YC2VF0YL @1C@!8Z@N*G0F*2VZDmkR'A)@"6HT)@`lF)$)GaLMC+0YU8Gj5MU(FX!6ZbpC*T) JbjkRSNP90,6h5qI"HjE(r6CB5)6hGh&`RHiSV6K8kf1mX5GpSF&Nk`di5&j","j jJM@&1rJc8keiif[c`LHd`3NCC$*(H"NI3IE*dQd2Ga&`&i'XB@XKP)KUrBN'eXk +1I%@jC5DCQ&er[FC%Ar5AqH1YI(0TP3Th6&1IRbbBIfE[&'I4&Cr,C1m89JIM+X 2!DRk8+CNP&YM96'p-(hr-IP+iPGX@(%k)Cr9'PjY$hVGAl-p*[dc[VaRac,##iA *m(N)(ZKaC`m3G3kANX(Ej[QIQT+XA!hJhEPa$TCI2bEbD*fr@Qk6&GbcQ6jA5Mk FebGD+&fjR3I1C*4YHp3(LI9+@KG9VbCRk[T1f!Y$BSr-4KF3E3h6*4288mm2rh5 -q*EVe'TDA`NFf3kIC3B!+[[j2AG!a+eHh(YQKCZj4Sr!GIc"X#!Pj)$a9@FS*jF jCX`jJm%([XiEkSXKhFi#32j(KBX%G4@([06"Hi$YK1M%8%@5Q18d`,@'MBV#[e@ 533qbVlANMmES96NG"VYE&bXbG`2dh!,3Bd-93q-DIVT1(&T09X2!X3jEE1+'RfS ,"Hi`QT+1#k24A*MHm%GD`B#Hr[Z3!"cPjhKG%NYF@(LZaX,J5jZAE3hV@e2hMjX 2fA)!VL&hHaHPR9!KJT4CEec+&GrcM))FF&`dUM2@&kCcT,Kl4GaiXlTHSB4Sb12 X42Zm#Va3keUfqh1'e(H3!21)($4h[P(fRCGD'`26*68INZ3&S&QMI[4UQ0(qQqH L)-cmr(U$k'9qX9Jjd-3Yh)i9Zb1JUe'-Rb!E@83%!B2QaP,k-jNUZU8SNQ1"!KV UX'pC#XN,"6YJ-eq*%2eH4@B4ZP,KT88K&6a*c!F4arR)qJNSk8HGB"&aGfl4MY) A*EUV#peAE)f`jcBXl#Tae#dcF2CF!Vhf'FJ310c+dPFLMFJ%@F4$ZrSS6dERq5, NX+,fq-4833!)ZkJV'0IZR3[G,d2VlFN6bY35#Z@9(8LmcSYhJK"QDr-%R'Q2@2D !S6`UpG16"f#IThi2FidDiHkMCMm1FcMSh1'UbkU54V33AAaZH(R"5$#qqJX[%aZ $qBTk424%+d15mVV9i-%4,iK+1kD4e*Sp4$jQ@b`M4&,CmJlN*lIjj5$)Y,S9c[8 3KeFrM,UUX*9aYM)@ql@l3(DHGN)VT[+#V2pE'jJ!`ZmqJi('`$355Qpd#lhi*IV pFX%92`eia36ajQ![PR09L@'&419LKc#4Vfm,e6M0de`i5e)i[DPF0-bi5P*CN6@ ie5Tm!`U09Ge+,1Na%SQPI8NEB'r!0S(I!9492mUTb-++dajTbVGcfjZf*Z4QY+, lMKh%KZiUDCUN!kQU80i5&2j*UKkQjl&FCk&YUqPkri5pV'k0M`k#-D@b'RQ-k8K P@M,%eaB*NU$bYYbBXq9RQ(VAm)V5Vq)RS%M)S85Hp08IPQ9-D4D,V%25()@G&M, UpmSh(`D6KEf8XI9Za'ljXrhKBh0&2V&VXH2MP(!hSeB*LeZ(5Q6*iA%@YimZX## EhdYN``h36XQIFYmIN!$3eZe3fKpf1R6RLr[(55cG)`Jj&!5GQL*04B'Tf$P+Di) PiUE%-#41MLK%01MSfBC2!PkHjAI@b5eK`KGeN!#e+DF%RKpGa,TcI0KU*Pifr'i Q%FLIRL8LTJE'D@4B3Z988J6$A5VpFjX)2M,$qke6qBUTXQrkQlQS@M4D!D,eMV4 C+FGVEYM6(9Aj[aC,BeI6cf**&CEPC#a#%ch'KX&%rVS8d$pQ9V,U)0C#k&(F,+@ q@F`qi0KhfUAc"i(8Pj8VdR89S,$#SIF+c,E'f!epL)+`X-DMFdT3ihdF'LbM9#Y X@IqDFEBP0AL2V1S5F64LDJqrI*ZLR[5Lr3@XT5R)CEQjrUA#T+#!K$9VdfljVE" b[')c"2UM0-+'Urp0U,%BKM')IB-`iK!DFB,K6##I5Gabl&)5+RN6P2mJ9eYYqJJ (pJm,@k*Qe2PpU($(BmD`$K*PUU+q8d+R$4`ee2U*`DM)4lcS#ie*B@'U9XP"N@d D+C5ed9f(6$)lem,YSl+r2cp66$E8#0ii2F2(42@Z!-JeE#[,bS,4lQlZ9eh'm@[ j5BI5CQM2S8"UB@$U0*098'AB8%[NIkH4)pl-3T!!RDrXJF$TTdSY33k&02$m9@8 V'DS@Aqk3!'i1J4JIbP@NkeFY56F"5@TS(Il13Y,XT,1JNM2j"lmSiI#3!0$X)p, F,0m810r#c4VlbFZU!'-'DM`CY9Fd-9`P-i,VkfHFh69)$13QUE@qec`"Ia6U)GK cJ$[YiU'[Qh5G#qlD#IM$MJ-EU5p)f4`S8'k&p6-ZXDQQJd1FD@)UGXMf3fD,E,0 lRfm#SqTDpE@DbLM`INNh+BklF62Xb5Z,AddL*,AlPLlMB#eBpL*cM'I#JFGf*Qd S+Q+Q5hSFL,&SjF&jE#XHeecIaJiIMjNr!rki12(E1TYL!Y[B4$XjM+RcpLD*dLU pLFCC4'pSd&`IZCl'ALBHa5HZh%ANb'cGD0qDEMBGE#RrDZh"T3LL"q0!8QXS'JX *@b@CSRpG6+4'#m9Z,#-fb([8l,%"a(8mG$(j"55+$Y$rbkV@3#K50&bcXc*Gr86 $QUd#LUj@LX9@#6!iRNfSc`Ycd`pG,jb1rhdDcbr5aPaN)QYPp*ZQ$hpYHaG"QU- d#,"6qaI!*-QVBRBe#l-$GpFhSNUF&0K8h2l"2Q520LrJ,8Uba4bUF!EhQaeCD9& D-[NIb$F$d1%$9F3m1TeU@b!qdkTkdS*Nee!mmJXMT2YBGTF!AJcG5dbFI,ScVEa dp&IDcp"#M26ppV-feY'Nb5k4p)BdaVQpD2%IU'U)lkF(jf%PcHDc@8iG1YSmRmX C&BpbplS#!d`[I2eKGSX4k1$F$(*"5!e*C-e1YlCSM))alNi%NH"HpHY2f*5h*mQ *"5m9Me-Fe0"(SEZ1ZYI#J25qIULre5B5F+0#-Br(aHkEI'#PSU6*@PdpeIFi'dP mH(#!p-h1P'j9'S%mG,JSa2arLV"F9K`pb8VF`PIhmh'kQb!kfXq0F5QDc@R"Y%c 4'RUM,-"0iBLBj)CQIh9@+l&d$#JjT"JA@$@Ce9p3'!()L['#J6#Q5lfVqjMlDl& B$*(&UAMa6LXGEY0f5@VU)NS!r6rd3**ST-ANFbZ6!FZZF$I&9SpaXq9lT`0DNJF f0$Rj0!j'54Djm2arUkdq-2HfXiEqLTL)BRe6r!R'NGSXiqAq0#@B8a*GT0[Z[6) eDRhIXbST1D`j)KYmmAI2iAH#r0(m"l6qRBYKSScFYMa'kSIGkI%I0lamGrcK"e, lK2+4+(lJ6&jb'Z$p$b4jJM2-D2,Nk3#5+J[RfkjNR-cIaEm#"J@XV"N`mXi@22R '9,&@Nj)JXpr)GDL"k,qbC*UYb+3RKM'iPAL*%c##+R$G1[-$2H(*JPj%(IL3!"F 0@-$i+U%GILRVGffKI$Z5GlbhjXaqd&N`bCk8pEfSjM9#AG[e)Y,9'eJZPRL(dV( &XT!!2M-"S4@V9J!bL@Z!@9ET6eej8XDS#XB&F@k%af5$Q&l"0Zjc+'pmFADLS"` Dp,K+V+BC2q3f$NURAPYPCFEfkk`+Dl9T26ZmDSpI0ka$,`EBk6-()UQB%9DlNdb &G1a+4*09,#Zj*d*fh0QkcLlqQ!iSHlMcLNL55JGfE-M8L5A(3'B$JS%lV%),Id3 54TfpP(lR-XDG+k6R2r,NAMj$!KMCU!,30$V"C!$1Q)3qC'Y%9&rYF,F[(6-U-*% eYpEJkf0f%!qU%K-)l&[Ic#I90bb&fCd$*3Qc3jiR6G%[FYTI'%IqbVp#PY$QH1I hLkZ4(!kjb@AiiL%dij2B(#cE)+e'Rbq#%'"(6!lS'`S,L(bdZHY6-%@8Xh3+LIq &'5J3)f9V'HcDLmccjprmSr@+0)&K2Ja%@VUrEe''pK$PVeaG'9MehpalL(&+kh1 kcq*mE(&e+X$Jd5Fk$59LAhkLp&-im9*,9V"%2pXrVFATi#BF16d4Rqah`P+Zq!N LqlM)RBB`UkIf#jEI1VkHV+bPQ($kcGMhc#0+#C(%iRKfQ4%P[q%Ld#A,M-8@cXI (4`KTLC!![["f5'Y0hL(lY@[0l#`k'VjI3(k,!Zj!UXEeLG(e)Fh0*RFYX1VbH9F IP2XDG!Z"`XBR"PV(AY$R`AR&h8k3!$"C$8'Q`0UqH9E#dYTK!bjU*kB@Y"MiU(S H&YH,GQA"b)J0,JIBf+dEUGV1'"R@jSIE2B"2-h)6S$ZE4(P3r%k#ZkRUN4[5"j! !Vd1h%(F`X`rJGk#$KT+SBb)l!S$d!H&8rp+Pp1CH9p$&"0R*1`3FhZ6j'dH(BSF qJ4b@J(l)613-I3M"ShhD@'0,Ii%%9K"R`-a`[``dZZajAabmT9@Nq2@S,L$kTrm 91%m!-2B$m&+eJkq8LH2)'j+5()4Q!Sb!XlcUP6jA0CZEYq4TlGRI#f(CIbU3!", CrSD4jfcPlU4EiiH,AJMjD&BA+#ZBfC!!5eBET#*qqAm[S29ZlVe!"G'bq*&-641 H'F%mHimB,4(BrHQjNB#G-h-6C!FD0iiLBZl(B'ABD6B-GMYrSkGN8b@m(0N[Rb) i393QkPNP%X'1dZ9UrU4T0ke&Yaa$b@LJ!'6UUc!PmLH"[V'D"UdSD,"j[N[3@dj *!ZPE#T&5Ip)0)&6QX"2a+JiGEiL9ZY#Ija5a4LM1#b83iBfR0)GN`3rL*#1jNXQ D6D[EaAL80iF8L$FDdV#T#TqFhH,VpkU@UF`0X[IIDB*LTIE"q`TaPBQ$F@S-`ZZ (cK4*XGrCIpMEki&3[BKX2P[rq9,Mhj(L11HKfTliNJk1Q''Fe$Z+1k"((6GrKm* A,YFrhJJ95XaG9LK&4*`+,S"hR6i9N!#IU1jE'pM#&G8)iLSU#A8,J#HN4RPC0aq V#2BZmRG8HbHTTem"V@c(1THG1q(ZY@j,bKScKrF1mMA-LC)`pDZil9@fKQ,58TG PkdCiU$9TNiMYlM(pFb*&0-fm,Y3SZ03ebFJ0qR`[Gd(bE1)SrqEYc*mZ4KGDA+Y KZfUq4`QpU[hA(HX58PKaXICDYIMd@k@65a[IGRheF[bhij)cHRNUkV86fJB6b@` ZQV9aK2f$9VNAa15[#bE$S86PP5URHeVi$*[UiiJb8R@VAiKT5@2*M-j2#fU$B1) k5T'J$3f`BIX2,d(lNR%Y@E*$h2pjlk$1J89Q0GT@$%iURR!V9%0#MbXRXNdp64G 3,PjX$L)2!CPHaBISIGHAJ[)Cq5-5Sa#8)@UR00Ka#rp,8D$D@[C"1VTHEiBl8mi @h#'UjEI@kM4I"$2"$[6MlcBcDd`'9i1R8Vq89#K%AM&)&d1!L2+1SQKqpbX4Aa[ BJfD3!#[bmf"lPHa`lkS%,&bhdDEbBh$GQV9PA[TlD81C-2rY!`RR6U2E5R-AGa+ pH[amT*!!bLeb)e1XCAlLU+5V')333+,YMlKL'Q$H4MM3$Am@`G86#`GX9Mb$Jrq k5jH3!#-[+)Vfcl9@ZDmAFhd1%PdDDZNNqR(,M"eF-[j([pI'QD*JDXm[DA3'K-0 EiM#*0DN93&aSi5mV0qpQ)jbhTaSImlGC)$T(*,riCGGN@BKNL0#X#k8Ch0'MU2f $25ML4!4RT8C8F2JJ-ib4K9cc!5X8'8@8dTSSMN4DPb5B$MqT@'m,Y,CHU#V6Gh! VKmE%pZp&r*Zpr56,X)HKkTZa+H$JEpV,5eiH(5%6V"L!dA(HbD)flk-dU5pkrVk 1I0CK1cZpKl!ASA3l&If'-`a,m-!Spl+!F34M'j!$+*Y39K`H%a+SpmReLQd!8Hq Z*HZNBSjjPA"N1-(-DmqqUc3TU'&9,KI1K(a(HVDEB!J@C2+ApGi5+d24fUDBPHL bA@N@C+Pba#I!FP,k[cb@VmXMr9bl'S-el0e)8ek4fZam*@m',U'2%V(`eJ"FbK- jC#-&(j@LdZ+1pE-T$$8PLI9[9+kB20QE!0+YqUK#aLh-iV!Z"M%0PYiHA6"Mb6N SD3,k$UV`RFKi+LP33`ep'TY8CRc`"Ndh4kEDl2a4M#`@XeF"lj5*,B`EXhCCDj) 3cEF1flZQc5Z0[plC`M8QTPL[4Ilb6B0*3hK3Q)1Mbm&+B9BM&QX("0,r8eqdQ$Z Gprad1dQL#eVilCMPccUiJ8)UY)5``939TP$!p3qqKjb)H&Cj+`(fGqI%M)Q2E5c +#1bkV3!T!K[BSGJmqMr[84$Mmi%58+iD*V!(D#U*H6KG-lei@Gr6DmD"N!#R5#h L#BLJLQiZFNH(%9!l"E09`f&UB9281iMHQ4YKZad"d8PXKi1GT)KPr!GeE5m,iST M&2(6'V6G2rek5D4P`8AMaf2L!HMK!HAJC6c(D"rI3&`Ha@raJBjmeU4cR&djU`f BH+kp,`J6QfN8F[IHGL#H!UdSj8CiQ#"Q++'ji2rdcB4+k3f(RUl(D(KrJ#RR9fe '*MYKZ24Yq+iJU@14T*jf5Ib,&4I3[5YAX*&(*ZL1*pRXAD0ArfG6`*4R@I*cjJK K&)04R5m,#&0l-CYP245[$6`,-3dE2cLrNbTeb(VVKR5&1Xr36ZVL0,+'UPjNCYC 069FJDC2A5-$m6Q1[P`C[RCR4jSZb@P!T$V[cdLP`LV,ek-!)b21%l@N+kdJkY6, ,+',rE(G3615,TC!!B(HZ5R2l-r![6"RTV5`pVl@-CRYMRCl4$FiTUH8rcB@S8D+ ZDL@(Y-Dpb9PJc8XHRkVff61U!pFMR(kRQSXMbl1hJD4V1!mlI+#GY#56+aA&14L PBFCcXF3k4PVkFRCC)&KY2Il`M0qA%-)mN!!dG')5"T&aT-lA@148AB4DQ8q48$I 'd1"q@%SIVqIj9UTcIT-KLT`"mDKYZ9(@NQIHT04h*[r)b,KMYH$ZP!"h-530p%i E0,8KPcYSfRf6&81U'kT"))D9fRVA&M[I0aqGdR$!6UJm`SfNf!++-q,B+JmTb(X P6rLSS*K9cB,*EDRQBiKk+3I&[GQ+kPMSGpr'(Kjp%B9R&RUJHhP[)h&16%VYP,V 'Nql'UaKrV5a3lPJrJh+ME1b2SNUbVfr(qq(QD!2C68LPbhI+02YSHNbdiC!!'J" 8E2SLXSa2KhYk!P),SmT+,ZIHd#KJ-KreA9SZq%S*UV`LlfCfEiacK&f$2Ra6Q"H L4l[$&I+IP6$-jZIEK8Kfd&%0F2ICQ80%kNl&e'ZhMeAQ$RJ+c$ebTGP-NVSUAJ0 G0X+eFab5E`SqiTPd)re,"5FE$P)4k21%I6%!,b,!53lR"c!hXPQiRLrRDefSZe" dVL+rfa*Ncm'T6aRj$R$b&"28M2jbD1iBqYb,,riG2qMZ8,c3!qci0BY4X+AG[93 #TMS-+M(8)4$3FXYRP,LRG'EZf#Bi@3c$SJ1P!$H@aSb+R[h@f)3+e(1JcZd,iL5 1)c)"3,*G#Kjc%jmRfQPd8!9J4Qc0$%URY4-,kJTlUFaVpF+Ud@k3!%)r"V'Gj)N P3abT(5MQ`pj4R4q'b&49bqIETMjp[#,cjTZfRNqXYcIY#*04VS&UMlU4EE%&cl' 1&4!pfbr+`*)U$4%6Y$V&@24fCkP*%H54jVYFr'-N6`Ycc-M'c"4h)FB4mLCmc,N MflpEDM*!L'qNV%4)MYZFVkJp%)9#AfSC*,kEZpZ8,pAUi[Cd+TK%,54PKiF`*EN )G`DqD&#q)A4)#SV)kXX&)bET26T#)j++4cEr5(G1+k3)#GeETCfSIAjr(e"+M9@ kEDr0$d+D-B"'TFTi"&+3!2h2[UFA*`9XKS0)aG54mE2&)VM(PlLj$U9B&a4)J83 f)c1041-H!6%TDi)5H%P9E**i!BIB5+pk[0akJhK1EP8,F+bK!ME&dJ[,65M5R`i Fj(Z#6p1)`Q&jb5aE%6J+XIEEUYjG49R@NPJ$)6B`2QL!E10cPC4R`$95Q'La2!q m(E'0(JkVpHhQip*'bDcXU9S*4LkMmqa@$#,BNdik(`Im"jD'C3`3#-&HfFQ&X2B pV!kUi[&BLm8@aiYBVPVSV9TB(UcikH''GKi-5HaTmk!"IG%Ci8aBl*86NqN"4EJ F9c"jjAQp6aGmQk"A(5`9ciq*a1,icT3$mmJ+(--mXQjZ&b#pkRdRUiJj3ql'mMi ,R$N@#0@M[$[VhF22F0kK!4FaT%QN8&P3ULq9e"rB'XkjhY2)+aU#)2qDi(QVZla p)X+TBrPMK49B,L4NmS2cZF%ZTZeJifjP(&(BfUNTh%BY!Ijd)i5NVECKAj'e`FD CKja"Hi4+T%ELiec(CFe+@%&X6,k`*-m0FAYF91GU49i-qL%@'Nq$HVZ!GcY%Yi[ $J@FT"hIP#@-PEpE$a'S-LXXmaX2AVh0Y3N6'i`)c44b"+5%JlK!E3NIXBC1rMTe Q'N!J-UJNHEY&IJjPPcF%pM`"A$4f[@CL1RkaJhNNJcq"4q'D*V"BKd%C#5[T(`V 58@(f+ifR-Smf%KMQpMFJcebG*cE(X5!GS[3lLHA1iiK60@#aYSLSpbY8"8&G(G# 4#j8a4iY+eTD5M5keX"V8L&!QGJQ)ZBJbrG%eQi!9P96#J$r&TD3mj6,+M[)#2f# 4*Cp`*Y`H%@PNkkjrENP'SD1C[G,*l,+5TT[C2"`%PfMA3`MKaj4j&"09Y+Bqpf4 ANa#`fa#L,ihbPrVe%QGI,h8`3XqY2+8N%SK$Yq`NFpJ&Q$`PC9mDl8Y)%`L$`Pi 'mE-V,ImfkV"TPqIiEr#hPcKfQaXJFAKjkAcmHh1NkA+$Jb[%4%%TJ+F5TMl2ALZ TTJdDk(p$(-@cSJB6ldlmCrl@JYqYS5F15rYMQLMJL*qdipq+iekqfYE$42kJZak iZZ&U1B2LAGK!jQp6)!qm&B8@q($hLJLp)L"RQQIE3S9q0"0,k-3'Ck*U[6+bS*Z LXY)G'6l6-MY%SP&L*,p%'"mFC2@IabCS(fEMXl+"'q"P(iHVaZp#Ec!DSNbaRpH )G4k13f19J-0Sa!8FN6pr-"G"5Y8EpII(-,,$jhAm`5`jqq1Gce',pVT6c*!!GPp IKG5b+2e0e`aRAdEb"5hr"cDdi`Le"EF"P%2F8NrAKAIlf[kk,8jdBJc[AN9aH#$ SDBG9Fej4M3JLPGmQMTcaYCc1cHK08`J6J#Q5ld)4R&a)2ZLR1iRS'd(HE3f`dKE V6pqT)U1*8JMf@6ka)1e'B!lR")M`q)*CBk03"YX[#j!!`c@YR-i!J'@'kh5Q6Z5 f1f@@ZYl*p(9!dND*IF'8UYm&mj!!phT'D0e`(@BBZ%CRkqR9,V,69P8eA4YeIj9 aQi@&iA+VM)*la44IEe+VE'0QA1'S!J5KXR@2,4,0E4,E#-eRV-Dk8#mid+[k[Vb VS$-`!!!: \ No newline at end of file diff --git a/MacstodonConstants.py b/MacstodonConstants.py index 57e1c47..f3b6640 100644 --- a/MacstodonConstants.py +++ b/MacstodonConstants.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ######### # Constants # ######### DEBUG = 0 VERSION = "1.1.1" \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ######### # Constants # ######### DEBUG = 0 VERSION = "1.1.2" \ No newline at end of file diff --git a/MacstodonHelpers.py b/MacstodonHelpers.py index d2b0802..9f72c98 100644 --- a/MacstodonHelpers.py +++ b/MacstodonHelpers.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import EasyDialogs import Fm import formatter import htmllib import ic import Lists import macfs import os import Qd import QuickDraw import string import TE import time import urllib import urlparse import W from TextEdit import * from Wlists import List # ########## # My Imports # ########## from MacstodonConstants import DEBUG, VERSION # ################### # Third-Party Imports # ################### from third_party.PixMapWrapper import PixMapWrapper # ######### # Functions # ######### def buildTimelinePicker(app): menu = [ ("Home", "home"), ("Local", "local"), ("Federated", "federated"), ("Notifications", "notifications"), ("Bookmarks", "bookmarks"), ("Favourites", "favourites"), ("Private Mentions", "mentions"), ("Hashtag...", "__hashtag"), "-" ] for list in app.lists: menu.append((list["title"], list["id"])) return menu def getFilenameFromURL(url): """ Returns the file name, including extension, from a URL. i.e. http://www.google.ca/foo/bar/baz/asdf.jpg returns "asdf.jpg" """ parsed_url = urlparse.urlparse(url) path = parsed_url[2] file_name = os.path.basename(string.replace(path, "/", ":")) return file_name def cleanUpUnicode(content): """ Do the best we can to manually clean up unicode stuff """ # The text is UTF-8, but is being interpreted as MacRoman. # First step is to convert the extended characters into # MacRoman so that they display correctly. content = string.replace(content, "√Ñ", "Ä") content = string.replace(content, "√Ö", "Å") content = string.replace(content, "√á", "Ç") content = string.replace(content, "√â", "É") content = string.replace(content, "√ë", "Ñ") content = string.replace(content, "√ñ", "Ö") content = string.replace(content, "√ú", "Ü") content = string.replace(content, "√°", "á") content = string.replace(content, "√†", "à") content = string.replace(content, "√¢", "â") content = string.replace(content, "√§", "ä") content = string.replace(content, "√£", "ã") content = string.replace(content, "√•", "å") content = string.replace(content, "√ß", "ç") content = string.replace(content, "√©", "é") content = string.replace(content, "√®", "è") content = string.replace(content, "√™", "ê") content = string.replace(content, "√´", "ë") content = string.replace(content, "√≠", "í") content = string.replace(content, "√¨", "ì") content = string.replace(content, "√Æ", "î") content = string.replace(content, "√Ø", "ï") content = string.replace(content, "√±", "ñ") content = string.replace(content, "√≥", "ó") content = string.replace(content, "√≤", "ò") content = string.replace(content, "√¥", "ô") content = string.replace(content, "√∂", "ö") content = string.replace(content, "√µ", "õ") content = string.replace(content, "√∫", "ú") content = string.replace(content, "√π", "ù") content = string.replace(content, "√ª", "û") content = string.replace(content, "√º", "ü") content = string.replace(content, "‚Ć", "†") content = string.replace(content, "¬∞", "°") content = string.replace(content, "¬¢", "¢") content = string.replace(content, "¬£", "£") content = string.replace(content, "¬ß", "§") content = string.replace(content, "‚Ä¢", "•") content = string.replace(content, "¬∂", "¶") content = string.replace(content, "√ü", "ß") content = string.replace(content, "¬Æ", "®") content = string.replace(content, "¬©", "©") content = string.replace(content, "‚Ñ¢", "™") content = string.replace(content, "¬¥", "´") content = string.replace(content, "¬®", "¨") content = string.replace(content, "‚â†", "≠") content = string.replace(content, "√Ü", "Æ") content = string.replace(content, "√ò", "Ø") content = string.replace(content, "‚àû", "∞") content = string.replace(content, "¬±", "±") content = string.replace(content, "‚â§", "≤") content = string.replace(content, "‚â•", "≥") content = string.replace(content, "¬•", "¥") content = string.replace(content, "¬µ", "µ") content = string.replace(content, "‚àÇ", "∂") content = string.replace(content, "‚àë", "∑") content = string.replace(content, "‚àè", "∏") content = string.replace(content, "œÄ", "π") content = string.replace(content, "‚à´", "∫") content = string.replace(content, "¬™", "ª") content = string.replace(content, "¬∫", "º") content = string.replace(content, "Œ©", "Ω") content = string.replace(content, "√¶", "æ") content = string.replace(content, "√∏", "ø") content = string.replace(content, "¬ø", "¿") content = string.replace(content, "¬°", "¡") content = string.replace(content, "¬¨", "¬") content = string.replace(content, "‚àö", "√") content = string.replace(content, "∆í", "ƒ") content = string.replace(content, "‚âà", "≈") content = string.replace(content, "‚àÜ", "∆") content = string.replace(content, "¬´", "«") content = string.replace(content, "¬ª", "»") content = string.replace(content, "‚Ķ", "…") content = string.replace(content, "√Ä", "À") content = string.replace(content, "√É", "Ã") content = string.replace(content, "√ï", "Õ") content = string.replace(content, "≈í", "Œ") content = string.replace(content, "≈ì", "œ") content = string.replace(content, "‚Äì", "–") content = string.replace(content, "‚Äî", "—") content = string.replace(content, "‚Äú", "“") content = string.replace(content, "‚Äù", "”") content = string.replace(content, "‚Äò", "‘") content = string.replace(content, "‚Äô", "’") content = string.replace(content, "√∑", "÷") content = string.replace(content, "‚óä", "◊") content = string.replace(content, "√ø", "ÿ") content = string.replace(content, "≈∏", "Ÿ") content = string.replace(content, "‚ÅÑ", "⁄") content = string.replace(content, "‚Ǩ", "€") content = string.replace(content, "‚Äπ", "‹") content = string.replace(content, "‚Ä∫", "›") content = string.replace(content, "Ô¨Å", "fi") content = string.replace(content, "Ô¨Ç", "fl") content = string.replace(content, "‚Ä°", "‡") content = string.replace(content, "¬∑", "·") content = string.replace(content, "‚Äö", "‚") content = string.replace(content, "‚Äû", "„") content = string.replace(content, "‚Ä∞", "‰") content = string.replace(content, "√Ç", "Â") content = string.replace(content, "√ä", "Ê") content = string.replace(content, "√Å", "Á") content = string.replace(content, "√ã", "Ë") content = string.replace(content, "√à", "È") content = string.replace(content, "√ç", "Í") content = string.replace(content, "√é", "Î") content = string.replace(content, "√è", "Ï") content = string.replace(content, "√å", "Ì") content = string.replace(content, "√ì", "Ó") content = string.replace(content, "√î", "Ô") content = string.replace(content, "Ô£ø", "") content = string.replace(content, "√í", "Ò") content = string.replace(content, "√ö", "Ú") content = string.replace(content, "√õ", "Û") content = string.replace(content, "√ô", "Ù") content = string.replace(content, "ƒ±", "ı") content = string.replace(content, "ÀÜ", "ˆ") content = string.replace(content, "Àú", "˜") content = string.replace(content, "¬Ø", "¯") content = string.replace(content, "Àò", "˘") content = string.replace(content, "Àô", "˙") content = string.replace(content, "Àö", "˚") content = string.replace(content, "¬∏", "¸") content = string.replace(content, "Àù", "˝") content = string.replace(content, "Àõ", "˛") content = string.replace(content, "Àá", "ˇ") # Next, convert unicode codepoints back into characters content = string.replace(content, "\\u003e", ">") content = string.replace(content, "\\u003c", "<") content = string.replace(content, "\\u0026", "&") # Lastly, convert HTML entities back into characters content = string.replace(content, """, '"') content = string.replace(content, "'", "'") content = string.replace(content, "&", "&") content = string.replace(content, ">", ">") content = string.replace(content, "<", "<") # After conversion, any characters above byte 127 are # outside the MacRoman character set and should be # stripped. for char in content: if ord(char) > 127: content = string.replace(content, char, "") return content def decodeJson(data): """ 'Decode' the JSON by taking the advantage of the fact that it is very similar to a Python dict. This is a terrible hack, and you should never do this anywhere because we're literally eval()ing untrusted data from the 'net. I'm only doing it because it's fast and there's not a lot of other options for parsing JSON data in Python 1.5. """ data = string.replace(data, '":null', '":None') data = string.replace(data, '":false', '":0') data = string.replace(data, '":true', '":1') data = eval(data) return data def dprint(text): """ Prints a string to stdout if and only if DEBUG is true """ if DEBUG: print text def okDialog(text, size=None): """ Draws a modal dialog box with the given text and an OK button to dismiss the dialog. """ if not size: size = (360, 120) window = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) window.label = W.TextBox((10, 10, -10, -40), text) window.ok_btn = W.Button((-80, -30, -10, -10), "OK", window.close) window.setdefaultbutton(window.ok_btn) window.open() def okCancelDialog(text, size=None): """ Draws a modal dialog box with the given text and OK/Cancel buttons. The OK button will close the dialog. The Cancel button will raise an Exception, which the caller is expected to catch. """ if not size: size = (360, 120) global dialogWindow dialogWindow = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) def dialogExceptionCallback(): dialogWindow.close() raise KeyboardInterrupt dialogWindow.label = W.TextBox((10, 10, -10, -40), text) dialogWindow.cancel_btn = W.Button((-160, -30, -90, -10), "Cancel", dialogExceptionCallback) dialogWindow.ok_btn = W.Button((-80, -30, -10, -10), "OK", dialogWindow.close) dialogWindow.setdefaultbutton(dialogWindow.ok_btn) dialogWindow.open() def attachmentsDialog(media_attachments): """ Draws a modal dialog box with Download and Close buttons. Between the text and buttons a list is drawn containing attachments. Clicking on an attachment, then clicking on the Download button will save the contents of the attachment to disk. Clicking the Close button will close the window. """ if len(media_attachments) == 0: okDialog("The selected toot contains no attachments.") return global attachmentsWindow, attachmentsData attachmentsFormatted = [] attachmentsData = [] for attachment in media_attachments: if attachment["description"] is not None: desc = cleanUpUnicode(attachment["description"]) listStr = desc + "\r" else: desc = None listStr = "No description\r" if attachment["type"] == "image": listStr = listStr + "Image, %s" % attachment["meta"]["original"]["size"] elif attachment["type"] == "video": listStr = listStr + "Video, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "gifv": listStr = listStr + "GIFV, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "audio": listStr = listStr + "Audio, %s" % attachment["meta"]["length"] else: listStr = listStr + "Unknown" attachmentsFormatted.append(listStr) attachmentsData.append({"url": attachment["url"], "alt": desc}) attachmentsWindow = W.ModalDialog((360, 240), "Macstodon %s - Attachments" % VERSION) def openAttachmentCallback(): """ Run when the user clicks the Download button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: url = attachmentsData[selected[0]]["url"] default_file_name = getFilenameFromURL(url) fss, ok = macfs.StandardPutFile('Save as:', default_file_name) if not ok: return 1 file_path = fss.as_pathname() file_name = os.path.split(file_path)[-1] urllib.urlretrieve(url, file_path) okDialog("Successfully downloaded '%s'!" % file_name) else: okDialog("Please select an attachment first.") def altTextCallback(): """ Run when the user clicks the Alt Text button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: alt_text = attachmentsData[selected[0]]["alt"] if alt_text is not None: okDialog(alt_text) else: okDialog("This attachment doesn't have any alt text.") else: okDialog("Please select an attachment first.") text = "The following attachments were found in the selected toot. " \ "Click on an attachment in the list, then click on the Download button " \ "to download it to your computer. You can also click on the Alt Text " \ "button to view the attachment's alt text in a dialog." attachmentsWindow.label = W.TextBox((10, 10, -10, -60), text) attachmentsWindow.attachments = TwoLineListWithFlags((10, 76, -10, -42), attachmentsFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) attachmentsWindow.alt_btn = W.Button((-240, -30, -170, -10), "Alt Text", altTextCallback) attachmentsWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", attachmentsWindow.close) attachmentsWindow.download_btn = W.Button((-80, -30, -10, -10), "Download", openAttachmentCallback) attachmentsWindow.setdefaultbutton(attachmentsWindow.close_btn) attachmentsWindow.open() def linksDialog(le): """ Draws a modal dialog box with Open and Close buttons. Between the text and buttons a list is drawn containing links. Clicking on a link, then clicking on the Open button will open the link using the user's web browser. Clicking the Close button will close the window. """ global linksWindow, linksFormatted linksFormatted = [] for desc, url in le.anchors.items(): linksFormatted.append(desc + "\r" + url[0]) if len(linksFormatted) == 0: okDialog("The selected toot contains no links.") return linksWindow = W.ModalDialog((240, 240), "Macstodon %s - Links" % VERSION) def openLinkCallback(): """ Run when the user clicks the Open button """ selected = linksWindow.links.getselection() if len(selected) > 0: linkString = linksFormatted[selected[0]] linkParts = string.split(linkString, "\r") ic.launchurl(linkParts[1]) else: okDialog("Please select a link first.") text = "The following links were found in the selected toot. " \ "Click on a link in the list, then click on the Open button " \ "to open it with your browser." linksWindow.label = W.TextBox((10, 10, -10, -40), text) linksWindow.links = TwoLineListWithFlags((10, 56, -10, -42), linksFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) linksWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", linksWindow.close) linksWindow.open_btn = W.Button((-80, -30, -10, -10), "Open", openLinkCallback) linksWindow.setdefaultbutton(linksWindow.close_btn) linksWindow.open() def handleRequest(app, path, data = None, use_token = 0, title = "Working..."): """ HTTP request wrapper """ try: W.SetCursor("watch") pb = EasyDialogs.ProgressBar(title=title, maxval=3) if data == {}: data = "" elif data: data = urllib.urlencode(data) prefs = app.getprefs() url = "%s%s" % (prefs.server, path) dprint(url) dprint(data) dprint("connecting") pb.label("Connecting...") pb.inc() try: if use_token: urlopener = TokenURLopener(prefs.token) handle = urlopener.open(url, data) else: handle = urllib.urlopen(url, data) except IOError: del pb W.SetCursor("arrow") errmsg = "Unable to open a connection to: %s.\rPlease check that your SSL proxy is working properly and that the URL starts with 'http'." okDialog(errmsg % url) return None except TypeError: del pb W.SetCursor("arrow") errmsg = "The provided URL is malformed: %s.\rPlease check that you have typed the URL correctly." okDialog(errmsg % url) return None dprint("reading http headers") dprint(handle.info()) dprint("reading http body") pb.label("Fetching data...") pb.inc() try: data = handle.read() except IOError: del pb W.SetCursor("arrow") errmsg = "The connection was closed by the remote server while Macstodon was reading data.\rPlease check that your SSL proxy is working properly." okDialog(errmsg) return None try: handle.close() except IOError: pass pb.label("Parsing data...") pb.inc() dprint("parsing response json") try: decoded = decodeJson(data) dprint(decoded) pb.label("Done.") pb.inc() time.sleep(0.5) del pb W.SetCursor("arrow") return decoded except: del pb W.SetCursor("arrow") dprint("ACK! JSON Parsing failure :(") dprint("This is what came back from the server:") dprint(data) okDialog("Error parsing JSON response from the server.") return None except KeyboardInterrupt: # the user pressed cancel in the progress bar window W.SetCursor("arrow") return None def getCurrentUser(app): """ Gets the currently logged in user. """ path = "/api/v1/accounts/verify_credentials" data = handleRequest(app, path, None, use_token=1, title = "Getting User...") if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when getting current user:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting current user:\r\r %s" % data['error']) else: return data def getLists(app): """ Gets the user's lists. """ path = "/api/v1/lists" data = handleRequest(app, path, None, use_token=1, title = "Getting Lists...") if data is None: # handleRequest failed and should have popped an error dialog return # if data is a list, it worked if type(data) == type([]): return data # otherwise data is an error dict if data.get("error_description") is not None: okDialog("Server error when getting lists:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting lists:\r\r %s" % data['error']) else: okDialog("Server error when getting lists. Unable to determine data type.") # ####### # Classes # ####### class ImageWidget(W.ClickableWidget): """ A widget that displays an image. The image should be passed in as a PixMapWrapper. """ def __init__(self, possize, pixmap=None, callback=None): W.ClickableWidget.__init__(self, possize) self._callback = callback self._enabled = 1 # Set initial image self._imgloaded = 0 self._pixmap = None if pixmap: self.setImage(pixmap) def click(self, point, modifiers): """ Runs the callback if the user clicks on the image """ if not self._enabled: return if self._callback: return W.CallbackCall(self._callback, 0) def close(self): """ Destroys the widget and frees up its memory """ W.Widget.close(self) del self._imgloaded del self._pixmap def setImage(self, pixmap): """ Loads a new image into the widget. The image will be automatically scaled to the size of the widget. """ self._pixmap = pixmap self._imgloaded = 1 if self._parentwindow: self.draw() def clearImage(self): """ Unloads the image from the widget without destroying the widget. Use this to make the widget draw an empty square. """ self._imgloaded = 0 Qd.EraseRect(self._bounds) if self._parentwindow: self.draw() self._pixmap = None def draw(self, visRgn = None): """ Draw the image within the widget if it is loaded """ if self._visible: if self._imgloaded: if isinstance(self._pixmap, PixMapWrapper): self._pixmap.blit( x1=self._bounds[0], y1=self._bounds[1], x2=self._bounds[2], y2=self._bounds[3], port=self._parentwindow.wid.GetWindowPort() ) else: Qd.SetPort(self._parentwindow.wid.GetWindowPort()) Qd.DrawPicture(self._pixmap, self._bounds) class LinkExtractor(htmllib.HTMLParser): """ A very basic link extractor that gets the URL and associated text. Sourced from: https://oreilly.com/library/view/python-standard-library/0596000960/ch05s05.html """ def __init__(self, verbose=0): self.anchors = {} f = formatter.NullFormatter() htmllib.HTMLParser.__init__(self, f, verbose) def anchor_bgn(self, href, name, type): self.save_bgn() self.anchor = href def anchor_end(self): text = string.strip(self.save_end()) if self.anchor and text: self.anchors[text] = self.anchors.get(text, []) + [self.anchor] class ProfileBanner(ImageWidget): """ The ProfileBanner is just an ImageWidget that renders text atop it at a fixed location, using a specific font and style. """ def __init__(self, possize, drop_shadow, pixmap=None, display_name="", acct_name=""): ImageWidget.__init__(self, possize, pixmap) self.display_name = display_name self.acct_name = acct_name self.drop_shadow = drop_shadow def draw(self, visRgn = None): """ Draws the profile banner text using pure QuickDraw (instead of widgets) """ Qd.SetPort(self._parentwindow.wid.GetWindowPort()) ImageWidget.draw(self, visRgn) Qd.TextFont(Fm.GetFNum("Geneva")) # Display Name Qd.TextSize(14) Qd.TextFace(QuickDraw.bold) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 70, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 68, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) # Account Name Qd.TextSize(9) Qd.TextFace(QuickDraw.normal) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 90, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 88, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((0,0,0)) Qd.TextFont(Fm.GetFNum("Monaco")) def populate(self, display_name=None, acct_name=None): """ Sets the display and account names. """ if display_name: self.display_name = display_name if acct_name: self.acct_name = acct_name class ProfilePanel(W.Group): """ The ProfilePanel is my poor man's implementation of tabs. It uses three buttons to swap out the widget beneath them. """ def __init__(self, possize): W.Group.__init__(self, possize) self.title = W.TextBox((0, 2, 0, 16), "Bio") self.btnStats = W.Button((-40, 0, 40, 16), "Stats", self.statsCallback) self.btnLinks = W.Button((-90, 0, 40, 16), "Links", self.linksCallback) self.btnBio = W.Button((-140, 0, 40, 16), "Bio", self.bioCallback) # Bio editor = W.EditText((0, 24, -15, 0), "", readonly=1) self._bary = W.Scrollbar((-16, 24, 0, 0), editor.vscroll, max=32767) self.bioText = editor # Stats & Links self.list = TwoLineListWithFlags((0, 24, 0, 0), [], callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) self.linksData = [] self.statsData = [] self.toots = 0 self.following = 0 self.followers = 0 self.locked = "No" self.bot = "No" self.group = "No" self.discoverable = "No" self.noindex = "No" self.moved = "No" self.suspended = "No" self.limited = "No" # hide stats/links by default self.list.show(0) self.bioText.select(0) def statsCallback(self): """ Shows the stats pane and hides bio/links """ self.title.set("Stats") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.statsData) def linksCallback(self): """ Shows the links pane and hides bio/stats """ self.title.set("Links") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.linksData) def bioCallback(self): """ Shows the bio pane and hides stats/links """ self.title.set("Bio") self.list.show(0) self.list.enable(0) self.list.select(0) self.bioText.show(1) self.bioText.enable(1) self.bioText._selectable = 1 self._bary.show(1) self._bary.draw() self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() def setBio(self, value): """ Updates the content of the bio text """ self.bioText.set(value) def collectStatsData(self): """ Updates the content of the stats list """ self.statsData = [ "Toots\r%s" % self.toots, "Following\r%s" % self.following, "Followers\r%s" % self.followers, "Locked\r%s" % self.locked, "Bot\r%s" % self.bot, "Group\r%s" % self.group, "Discoverable\r%s" % self.discoverable, "NoIndex\r%s" % self.noindex, "Moved\r%s" % self.moved, "Suspended\r%s" % self.suspended, "Limited\r%s" % self.limited ] self.list.set(self.statsData) def setLinks(self, linksData): """ Updates the content of the links list """ self.linksData = linksData self.list.set(self.linksData) def setToots(self, num): """ Updates the user's toot count """ self.toots = num self.collectStatsData() def setFollowers(self, num): """ Updates the user's followers count """ self.followers = num self.collectStatsData() def setFollowing(self, num): """ Updates the user's following count """ self.following = num self.collectStatsData() def setLocked(self, flag): if flag: self.locked = "Yes" else: self.locked = "No" self.collectStatsData() def setBot(self, flag): if flag: self.bot = "Yes" else: self.bot = "No" self.collectStatsData() def setDiscoverable(self, flag): if flag: self.discoverable = "Yes" else: self.discoverable = "No" self.collectStatsData() def setNoIndex(self, flag): if flag: self.noindex = "Yes" else: self.noindex = "No" self.collectStatsData() def setMoved(self, flag): if flag: self.moved = "Yes" else: self.moved = "No" self.collectStatsData() def setSuspended(self, flag): if flag: self.suspended = "Yes" else: self.suspended = "No" self.collectStatsData() def setLimited(self, flag): if flag: self.limited = "Yes" else: self.limited = "No" self.collectStatsData() class TitledEditText(W.Group): """ A text edit field with a title and optional scrollbars attached to it. Shamelessly stolen from MacPython's PyEdit. Modified to also allow setting the title, and add scrollbars. """ def __init__(self, possize, title, text="", readonly=0, vscroll=0, hscroll=0): W.Group.__init__(self, possize) self.title = W.TextBox((0, 0, 0, 16), title) if vscroll and hscroll: editor = W.EditText((0, 16, -15, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, -15, 16), editor.hscroll, max=32767) self._bary = W.Scrollbar((-16, 16,0, -15), editor.vscroll, max=32767) elif vscroll: editor = W.EditText((0, 16, -15, 0), text, readonly=readonly) self._bary = W.Scrollbar((-16, 16, 0, 0), editor.vscroll, max=32767) elif hscroll: editor = W.EditText((0, 16, 0, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, 0, 16), editor.hscroll, max=32767) else: editor = W.EditText((0, 16, 0, 0), text, readonly=readonly) self.edit = editor def setTitle(self, value): self.title.set(value) def set(self, value): self.edit.set(value) def get(self): return self.edit.get() class TokenURLopener(urllib.FancyURLopener): """ Extends urllib.FancyURLopener to add the Authorization header with a bearer token. """ def __init__(self, token, *args): apply(urllib.FancyURLopener.__init__, (self,) + args) self.addheaders.append(("Authorization", "Bearer %s" % token)) class TwoLineListWithFlags(List): """ Modification of MacPython's TwoLineList to support flags. """ LDEF_ID = 468 def createlist(self): import List self._calcbounds() self.SetPort() rect = self._bounds rect = rect[0]+1, rect[1]+1, rect[2]-16, rect[3]-1 self._list = List.LNew(rect, (0, 0, 1, 0), (0, 28), self.LDEF_ID, self._parentwindow.wid, 0, 1, 0, 1) self._list.selFlags = self._flags self.set(self.items) class TimelineList(W.Group): """ A TwoLineListWithFlags that also has a title attached to it. Based on TitledEditText. """ def __init__(self, possize, title, items = None, picker = 1, pickerItems = None, btnCallback = None, callback = None, pickerCallback = None, flags = 0, cols = 1, typingcasesens=0): W.Group.__init__(self, possize) if picker: self.listpicker = W.PopupWidget((-68, 0, 16, 16), pickerItems, pickerCallback) self.title = W.TextBox((0, 2, -68, 16), title) self.btn = W.Button((-50, 0, 0, 16), "Refresh", btnCallback) self.list = TwoLineListWithFlags((0, 24, 0, 0), items, callback, flags, cols, typingcasesens) def setListPicker(self, value): self.listpicker.set(value) def setTitle(self, value): self.title.set(value) def set(self, items): self.list.set(items) def get(self): return self.list.items def getselection(self): return self.list.getselection() def setselection(self, selection): return self.list.setselection(selection) \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import EasyDialogs import Fm import formatter import htmllib import ic import Lists import macfs import os import Qd import QuickDraw import string import TE import time import urllib import urlparse import W from TextEdit import * from Wlists import List # ########## # My Imports # ########## from MacstodonConstants import DEBUG, VERSION # ################### # Third-Party Imports # ################### from third_party.PixMapWrapper import PixMapWrapper # ######### # Functions # ######### def buildTimelinePicker(app): menu = [ ("Home", "home"), ("Local", "local"), ("Federated", "federated"), ("Notifications", "notifications"), ("Bookmarks", "bookmarks"), ("Favourites", "favourites"), ("Private Mentions", "mentions"), ("Hashtag...", "__hashtag"), "-" ] for list in app.lists: menu.append((list["title"], list["id"])) return menu def getFilenameFromURL(url): """ Returns the file name, including extension, from a URL. i.e. http://www.google.ca/foo/bar/baz/asdf.jpg returns "asdf.jpg" """ parsed_url = urlparse.urlparse(url) path = parsed_url[2] file_name = os.path.basename(string.replace(path, "/", ":")) return file_name def cleanUpUnicode(content): """ Do the best we can to manually clean up unicode stuff """ # The text is UTF-8, but is being interpreted as MacRoman. # First step is to convert the extended characters into # MacRoman so that they display correctly. content = string.replace(content, "√Ñ", "Ä") content = string.replace(content, "√Ö", "Å") content = string.replace(content, "√á", "Ç") content = string.replace(content, "√â", "É") content = string.replace(content, "√ë", "Ñ") content = string.replace(content, "√ñ", "Ö") content = string.replace(content, "√ú", "Ü") content = string.replace(content, "√°", "á") content = string.replace(content, "√†", "à") content = string.replace(content, "√¢", "â") content = string.replace(content, "√§", "ä") content = string.replace(content, "√£", "ã") content = string.replace(content, "√•", "å") content = string.replace(content, "√ß", "ç") content = string.replace(content, "√©", "é") content = string.replace(content, "√®", "è") content = string.replace(content, "√™", "ê") content = string.replace(content, "√´", "ë") content = string.replace(content, "√≠", "í") content = string.replace(content, "√¨", "ì") content = string.replace(content, "√Æ", "î") content = string.replace(content, "√Ø", "ï") content = string.replace(content, "√±", "ñ") content = string.replace(content, "√≥", "ó") content = string.replace(content, "√≤", "ò") content = string.replace(content, "√¥", "ô") content = string.replace(content, "√∂", "ö") content = string.replace(content, "√µ", "õ") content = string.replace(content, "√∫", "ú") content = string.replace(content, "√π", "ù") content = string.replace(content, "√ª", "û") content = string.replace(content, "√º", "ü") content = string.replace(content, "‚Ć", "†") content = string.replace(content, "¬∞", "°") content = string.replace(content, "¬¢", "¢") content = string.replace(content, "¬£", "£") content = string.replace(content, "¬ß", "§") content = string.replace(content, "‚Ä¢", "•") content = string.replace(content, "¬∂", "¶") content = string.replace(content, "√ü", "ß") content = string.replace(content, "¬Æ", "®") content = string.replace(content, "¬©", "©") content = string.replace(content, "‚Ñ¢", "™") content = string.replace(content, "¬¥", "´") content = string.replace(content, "¬®", "¨") content = string.replace(content, "‚â†", "≠") content = string.replace(content, "√Ü", "Æ") content = string.replace(content, "√ò", "Ø") content = string.replace(content, "‚àû", "∞") content = string.replace(content, "¬±", "±") content = string.replace(content, "‚â§", "≤") content = string.replace(content, "‚â•", "≥") content = string.replace(content, "¬•", "¥") content = string.replace(content, "¬µ", "µ") content = string.replace(content, "‚àÇ", "∂") content = string.replace(content, "‚àë", "∑") content = string.replace(content, "‚àè", "∏") content = string.replace(content, "œÄ", "π") content = string.replace(content, "‚à´", "∫") content = string.replace(content, "¬™", "ª") content = string.replace(content, "¬∫", "º") content = string.replace(content, "Œ©", "Ω") content = string.replace(content, "√¶", "æ") content = string.replace(content, "√∏", "ø") content = string.replace(content, "¬ø", "¿") content = string.replace(content, "¬°", "¡") content = string.replace(content, "¬¨", "¬") content = string.replace(content, "‚àö", "√") content = string.replace(content, "∆í", "ƒ") content = string.replace(content, "‚âà", "≈") content = string.replace(content, "‚àÜ", "∆") content = string.replace(content, "¬´", "«") content = string.replace(content, "¬ª", "»") content = string.replace(content, "‚Ķ", "…") content = string.replace(content, "√Ä", "À") content = string.replace(content, "√É", "Ã") content = string.replace(content, "√ï", "Õ") content = string.replace(content, "≈í", "Œ") content = string.replace(content, "≈ì", "œ") content = string.replace(content, "‚Äì", "–") content = string.replace(content, "‚Äî", "—") content = string.replace(content, "‚Äú", "“") content = string.replace(content, "‚Äù", "”") content = string.replace(content, "‚Äò", "‘") content = string.replace(content, "‚Äô", "’") content = string.replace(content, "√∑", "÷") content = string.replace(content, "‚óä", "◊") content = string.replace(content, "√ø", "ÿ") content = string.replace(content, "≈∏", "Ÿ") content = string.replace(content, "‚ÅÑ", "⁄") content = string.replace(content, "‚Ǩ", "€") content = string.replace(content, "‚Äπ", "‹") content = string.replace(content, "‚Ä∫", "›") content = string.replace(content, "Ô¨Å", "fi") content = string.replace(content, "Ô¨Ç", "fl") content = string.replace(content, "‚Ä°", "‡") content = string.replace(content, "¬∑", "·") content = string.replace(content, "‚Äö", "‚") content = string.replace(content, "‚Äû", "„") content = string.replace(content, "‚Ä∞", "‰") content = string.replace(content, "√Ç", "Â") content = string.replace(content, "√ä", "Ê") content = string.replace(content, "√Å", "Á") content = string.replace(content, "√ã", "Ë") content = string.replace(content, "√à", "È") content = string.replace(content, "√ç", "Í") content = string.replace(content, "√é", "Î") content = string.replace(content, "√è", "Ï") content = string.replace(content, "√å", "Ì") content = string.replace(content, "√ì", "Ó") content = string.replace(content, "√î", "Ô") content = string.replace(content, "Ô£ø", "") content = string.replace(content, "√í", "Ò") content = string.replace(content, "√ö", "Ú") content = string.replace(content, "√õ", "Û") content = string.replace(content, "√ô", "Ù") content = string.replace(content, "ƒ±", "ı") content = string.replace(content, "ÀÜ", "ˆ") content = string.replace(content, "Àú", "˜") content = string.replace(content, "¬Ø", "¯") content = string.replace(content, "Àò", "˘") content = string.replace(content, "Àô", "˙") content = string.replace(content, "Àö", "˚") content = string.replace(content, "¬∏", "¸") content = string.replace(content, "Àù", "˝") content = string.replace(content, "Àõ", "˛") content = string.replace(content, "Àá", "ˇ") # Next, convert unicode codepoints back into characters content = string.replace(content, "\\u003e", ">") content = string.replace(content, "\\u003c", "<") content = string.replace(content, "\\u0026", "&") # Lastly, convert HTML entities back into characters content = string.replace(content, """, '"') content = string.replace(content, "'", "'") content = string.replace(content, "&", "&") content = string.replace(content, ">", ">") content = string.replace(content, "<", "<") # After conversion, any characters above byte 127 are # outside the MacRoman character set and should be # stripped. for char in content: if ord(char) > 127: content = string.replace(content, char, "") return content def decodeJson(data): """ 'Decode' the JSON by taking the advantage of the fact that it is very similar to a Python dict. This is a terrible hack, and you should never do this anywhere because we're literally eval()ing untrusted data from the 'net. I'm only doing it because it's fast and there's not a lot of other options for parsing JSON data in Python 1.5. """ data = string.replace(data, '":null', '":None') data = string.replace(data, '":false', '":0') data = string.replace(data, '":true', '":1') data = eval(data) return data def dprint(text): """ Prints a string to stdout if and only if DEBUG is true """ if DEBUG: print text def okDialog(text, size=None): """ Draws a modal dialog box with the given text and an OK button to dismiss the dialog. """ if not size: size = (360, 120) window = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) window.label = W.TextBox((10, 10, -10, -40), text) window.ok_btn = W.Button((-80, -30, -10, -10), "OK", window.close) window.setdefaultbutton(window.ok_btn) window.open() def okCancelDialog(text, size=None): """ Draws a modal dialog box with the given text and OK/Cancel buttons. The OK button will close the dialog. The Cancel button will raise an Exception, which the caller is expected to catch. """ if not size: size = (360, 120) global dialogWindow dialogWindow = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) def dialogExceptionCallback(): dialogWindow.close() raise KeyboardInterrupt dialogWindow.label = W.TextBox((10, 10, -10, -40), text) dialogWindow.cancel_btn = W.Button((-160, -30, -90, -10), "Cancel", dialogExceptionCallback) dialogWindow.ok_btn = W.Button((-80, -30, -10, -10), "OK", dialogWindow.close) dialogWindow.setdefaultbutton(dialogWindow.ok_btn) dialogWindow.open() def attachmentsDialog(media_attachments): """ Draws a modal dialog box with Download and Close buttons. Between the text and buttons a list is drawn containing attachments. Clicking on an attachment, then clicking on the Download button will save the contents of the attachment to disk. Clicking the Close button will close the window. """ if len(media_attachments) == 0: okDialog("The selected toot contains no attachments.") return global attachmentsWindow, attachmentsData attachmentsFormatted = [] attachmentsData = [] for attachment in media_attachments: if attachment["description"] is not None: desc = cleanUpUnicode(attachment["description"]) listStr = desc + "\r" else: desc = None listStr = "No description\r" if attachment["type"] == "image": listStr = listStr + "Image, %s" % attachment["meta"]["original"]["size"] elif attachment["type"] == "video": listStr = listStr + "Video, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "gifv": listStr = listStr + "GIFV, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "audio": listStr = listStr + "Audio, %s" % attachment["meta"]["length"] else: listStr = listStr + "Unknown" attachmentsFormatted.append(listStr) attachmentsData.append({"url": attachment["url"], "alt": desc}) attachmentsWindow = W.ModalDialog((360, 240), "Macstodon %s - Attachments" % VERSION) def openAttachmentCallback(): """ Run when the user clicks the Download button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: url = attachmentsData[selected[0]]["url"] default_file_name = getFilenameFromURL(url) fss, ok = macfs.StandardPutFile('Save as:', default_file_name) if not ok: return 1 file_path = fss.as_pathname() file_name = os.path.split(file_path)[-1] urllib.urlretrieve(url, file_path) okDialog("Successfully downloaded '%s'!" % file_name) else: okDialog("Please select an attachment first.") def altTextCallback(): """ Run when the user clicks the Alt Text button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: alt_text = attachmentsData[selected[0]]["alt"] if alt_text is not None: okDialog(alt_text) else: okDialog("This attachment doesn't have any alt text.") else: okDialog("Please select an attachment first.") text = "The following attachments were found in the selected toot. " \ "Click on an attachment in the list, then click on the Download button " \ "to download it to your computer. You can also click on the Alt Text " \ "button to view the attachment's alt text in a dialog." attachmentsWindow.label = W.TextBox((10, 10, -10, -60), text) attachmentsWindow.attachments = TwoLineListWithFlags((10, 76, -10, -42), attachmentsFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) attachmentsWindow.alt_btn = W.Button((-240, -30, -170, -10), "Alt Text", altTextCallback) attachmentsWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", attachmentsWindow.close) attachmentsWindow.download_btn = W.Button((-80, -30, -10, -10), "Download", openAttachmentCallback) attachmentsWindow.setdefaultbutton(attachmentsWindow.close_btn) attachmentsWindow.open() def linksDialog(le): """ Draws a modal dialog box with Open and Close buttons. Between the text and buttons a list is drawn containing links. Clicking on a link, then clicking on the Open button will open the link using the user's web browser. Clicking the Close button will close the window. """ global linksWindow, linksFormatted linksFormatted = [] for desc, url in le.anchors.items(): linksFormatted.append(desc + "\r" + url[0]) if len(linksFormatted) == 0: okDialog("The selected toot contains no links.") return linksWindow = W.ModalDialog((240, 240), "Macstodon %s - Links" % VERSION) def openLinkCallback(): """ Run when the user clicks the Open button """ selected = linksWindow.links.getselection() if len(selected) > 0: linkString = linksFormatted[selected[0]] linkParts = string.split(linkString, "\r") ic.launchurl(linkParts[1]) else: okDialog("Please select a link first.") text = "The following links were found in the selected toot. " \ "Click on a link in the list, then click on the Open button " \ "to open it with your browser." linksWindow.label = W.TextBox((10, 10, -10, -40), text) linksWindow.links = TwoLineListWithFlags((10, 56, -10, -42), linksFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) linksWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", linksWindow.close) linksWindow.open_btn = W.Button((-80, -30, -10, -10), "Open", openLinkCallback) linksWindow.setdefaultbutton(linksWindow.close_btn) linksWindow.open() def handleRequest(app, path, data = None, use_token = 0, title = "Working..."): """ HTTP request wrapper """ try: W.SetCursor("watch") pb = EasyDialogs.ProgressBar(title=title, maxval=3) if data == {}: data = "" elif data: data = urllib.urlencode(data) prefs = app.getprefs() url = "%s%s" % (prefs.server, path) dprint(url) dprint(data) dprint("connecting") pb.label("Connecting...") pb.inc() try: if use_token: urlopener = TokenURLopener(prefs.token) handle = urlopener.open(url, data) else: handle = urllib.urlopen(url, data) except IOError: del pb W.SetCursor("arrow") errmsg = "Unable to open a connection to: %s.\rPlease check that your SSL proxy is working properly and that the URL starts with 'http'." okDialog(errmsg % url) return None except TypeError: del pb W.SetCursor("arrow") errmsg = "The provided URL is malformed: %s.\rPlease check that you have typed the URL correctly." okDialog(errmsg % url) return None dprint("reading http headers") headers = handle.info() dprint(headers) length = int(headers["Content-Length"]) dprint("reading http body") pb.label("Fetching data...") pb.inc() try: data = handle.read(length) except IOError: del pb W.SetCursor("arrow") errmsg = "The connection was closed by the remote server while Macstodon was reading data.\rPlease check that your SSL proxy is working properly." okDialog(errmsg) return None try: handle.close() except IOError: pass pb.label("Parsing data...") pb.inc() dprint("parsing response json") try: decoded = decodeJson(data) dprint(decoded) pb.label("Done.") pb.inc() time.sleep(0.5) del pb W.SetCursor("arrow") return decoded except: del pb W.SetCursor("arrow") dprint("ACK! JSON Parsing failure :(") dprint("This is what came back from the server:") dprint(data) okDialog("Error parsing JSON response from the server.") return None except KeyboardInterrupt: # the user pressed cancel in the progress bar window W.SetCursor("arrow") return None def getCurrentUser(app): """ Gets the currently logged in user. """ path = "/api/v1/accounts/verify_credentials" data = handleRequest(app, path, None, use_token=1, title = "Getting User...") if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when getting current user:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting current user:\r\r %s" % data['error']) else: return data def getLists(app): """ Gets the user's lists. """ path = "/api/v1/lists" data = handleRequest(app, path, None, use_token=1, title = "Getting Lists...") if data is None: # handleRequest failed and should have popped an error dialog return # if data is a list, it worked if type(data) == type([]): return data # otherwise data is an error dict if data.get("error_description") is not None: okDialog("Server error when getting lists:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting lists:\r\r %s" % data['error']) else: okDialog("Server error when getting lists. Unable to determine data type.") # ####### # Classes # ####### class ImageWidget(W.ClickableWidget): """ A widget that displays an image. The image should be passed in as a PixMapWrapper. """ def __init__(self, possize, pixmap=None, callback=None): W.ClickableWidget.__init__(self, possize) self._callback = callback self._enabled = 1 # Set initial image self._imgloaded = 0 self._pixmap = None if pixmap: self.setImage(pixmap) def click(self, point, modifiers): """ Runs the callback if the user clicks on the image """ if not self._enabled: return if self._callback: return W.CallbackCall(self._callback, 0) def close(self): """ Destroys the widget and frees up its memory """ W.Widget.close(self) del self._imgloaded del self._pixmap def setImage(self, pixmap): """ Loads a new image into the widget. The image will be automatically scaled to the size of the widget. """ self._pixmap = pixmap self._imgloaded = 1 if self._parentwindow: self.draw() def clearImage(self): """ Unloads the image from the widget without destroying the widget. Use this to make the widget draw an empty square. """ self._imgloaded = 0 Qd.EraseRect(self._bounds) if self._parentwindow: self.draw() self._pixmap = None def draw(self, visRgn = None): """ Draw the image within the widget if it is loaded """ if self._visible: if self._imgloaded: if isinstance(self._pixmap, PixMapWrapper): self._pixmap.blit( x1=self._bounds[0], y1=self._bounds[1], x2=self._bounds[2], y2=self._bounds[3], port=self._parentwindow.wid.GetWindowPort() ) else: Qd.SetPort(self._parentwindow.wid.GetWindowPort()) Qd.DrawPicture(self._pixmap, self._bounds) class LinkExtractor(htmllib.HTMLParser): """ A very basic link extractor that gets the URL and associated text. Sourced from: https://oreilly.com/library/view/python-standard-library/0596000960/ch05s05.html """ def __init__(self, verbose=0): self.anchors = {} f = formatter.NullFormatter() htmllib.HTMLParser.__init__(self, f, verbose) def anchor_bgn(self, href, name, type): self.save_bgn() self.anchor = href def anchor_end(self): text = string.strip(self.save_end()) if self.anchor and text: self.anchors[text] = self.anchors.get(text, []) + [self.anchor] class ProfileBanner(ImageWidget): """ The ProfileBanner is just an ImageWidget that renders text atop it at a fixed location, using a specific font and style. """ def __init__(self, possize, drop_shadow, pixmap=None, display_name="", acct_name=""): ImageWidget.__init__(self, possize, pixmap) self.display_name = display_name self.acct_name = acct_name self.drop_shadow = drop_shadow def draw(self, visRgn = None): """ Draws the profile banner text using pure QuickDraw (instead of widgets) """ Qd.SetPort(self._parentwindow.wid.GetWindowPort()) ImageWidget.draw(self, visRgn) Qd.TextFont(Fm.GetFNum("Geneva")) # Display Name Qd.TextSize(14) Qd.TextFace(QuickDraw.bold) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 70, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 68, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) # Account Name Qd.TextSize(9) Qd.TextFace(QuickDraw.normal) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 90, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 88, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((0,0,0)) Qd.TextFont(Fm.GetFNum("Monaco")) def populate(self, display_name=None, acct_name=None): """ Sets the display and account names. """ if display_name: self.display_name = display_name if acct_name: self.acct_name = acct_name class ProfilePanel(W.Group): """ The ProfilePanel is my poor man's implementation of tabs. It uses three buttons to swap out the widget beneath them. """ def __init__(self, possize): W.Group.__init__(self, possize) self.title = W.TextBox((0, 2, 0, 16), "Bio") self.btnStats = W.Button((-40, 0, 40, 16), "Stats", self.statsCallback) self.btnLinks = W.Button((-90, 0, 40, 16), "Links", self.linksCallback) self.btnBio = W.Button((-140, 0, 40, 16), "Bio", self.bioCallback) # Bio editor = W.EditText((0, 24, -15, 0), "", readonly=1) self._bary = W.Scrollbar((-16, 24, 0, 0), editor.vscroll, max=32767) self.bioText = editor # Stats & Links self.list = TwoLineListWithFlags((0, 24, 0, 0), [], callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) self.linksData = [] self.statsData = [] self.toots = 0 self.following = 0 self.followers = 0 self.locked = "No" self.bot = "No" self.group = "No" self.discoverable = "No" self.noindex = "No" self.moved = "No" self.suspended = "No" self.limited = "No" # hide stats/links by default self.list.show(0) self.bioText.select(0) def statsCallback(self): """ Shows the stats pane and hides bio/links """ self.title.set("Stats") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.statsData) def linksCallback(self): """ Shows the links pane and hides bio/stats """ self.title.set("Links") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.linksData) def bioCallback(self): """ Shows the bio pane and hides stats/links """ self.title.set("Bio") self.list.show(0) self.list.enable(0) self.list.select(0) self.bioText.show(1) self.bioText.enable(1) self.bioText._selectable = 1 self._bary.show(1) self._bary.draw() self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() def setBio(self, value): """ Updates the content of the bio text """ self.bioText.set(value) def collectStatsData(self): """ Updates the content of the stats list """ self.statsData = [ "Toots\r%s" % self.toots, "Following\r%s" % self.following, "Followers\r%s" % self.followers, "Locked\r%s" % self.locked, "Bot\r%s" % self.bot, "Group\r%s" % self.group, "Discoverable\r%s" % self.discoverable, "NoIndex\r%s" % self.noindex, "Moved\r%s" % self.moved, "Suspended\r%s" % self.suspended, "Limited\r%s" % self.limited ] self.list.set(self.statsData) def setLinks(self, linksData): """ Updates the content of the links list """ self.linksData = linksData self.list.set(self.linksData) def setToots(self, num): """ Updates the user's toot count """ self.toots = num self.collectStatsData() def setFollowers(self, num): """ Updates the user's followers count """ self.followers = num self.collectStatsData() def setFollowing(self, num): """ Updates the user's following count """ self.following = num self.collectStatsData() def setLocked(self, flag): if flag: self.locked = "Yes" else: self.locked = "No" self.collectStatsData() def setBot(self, flag): if flag: self.bot = "Yes" else: self.bot = "No" self.collectStatsData() def setDiscoverable(self, flag): if flag: self.discoverable = "Yes" else: self.discoverable = "No" self.collectStatsData() def setNoIndex(self, flag): if flag: self.noindex = "Yes" else: self.noindex = "No" self.collectStatsData() def setMoved(self, flag): if flag: self.moved = "Yes" else: self.moved = "No" self.collectStatsData() def setSuspended(self, flag): if flag: self.suspended = "Yes" else: self.suspended = "No" self.collectStatsData() def setLimited(self, flag): if flag: self.limited = "Yes" else: self.limited = "No" self.collectStatsData() class TitledEditText(W.Group): """ A text edit field with a title and optional scrollbars attached to it. Shamelessly stolen from MacPython's PyEdit. Modified to also allow setting the title, and add scrollbars. """ def __init__(self, possize, title, text="", readonly=0, vscroll=0, hscroll=0): W.Group.__init__(self, possize) self.title = W.TextBox((0, 0, 0, 16), title) if vscroll and hscroll: editor = W.EditText((0, 16, -15, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, -15, 16), editor.hscroll, max=32767) self._bary = W.Scrollbar((-16, 16,0, -15), editor.vscroll, max=32767) elif vscroll: editor = W.EditText((0, 16, -15, 0), text, readonly=readonly) self._bary = W.Scrollbar((-16, 16, 0, 0), editor.vscroll, max=32767) elif hscroll: editor = W.EditText((0, 16, 0, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, 0, 16), editor.hscroll, max=32767) else: editor = W.EditText((0, 16, 0, 0), text, readonly=readonly) self.edit = editor def setTitle(self, value): self.title.set(value) def set(self, value): self.edit.set(value) def get(self): return self.edit.get() class TokenURLopener(urllib.FancyURLopener): """ Extends urllib.FancyURLopener to add the Authorization header with a bearer token. """ def __init__(self, token, *args): apply(urllib.FancyURLopener.__init__, (self,) + args) self.addheaders.append(("Authorization", "Bearer %s" % token)) class TwoLineListWithFlags(List): """ Modification of MacPython's TwoLineList to support flags. """ LDEF_ID = 468 def createlist(self): import List self._calcbounds() self.SetPort() rect = self._bounds rect = rect[0]+1, rect[1]+1, rect[2]-16, rect[3]-1 self._list = List.LNew(rect, (0, 0, 1, 0), (0, 28), self.LDEF_ID, self._parentwindow.wid, 0, 1, 0, 1) self._list.selFlags = self._flags self.set(self.items) class TimelineList(W.Group): """ A TwoLineListWithFlags that also has a title attached to it. Based on TitledEditText. """ def __init__(self, possize, title, items = None, picker = 1, pickerItems = None, btnCallback = None, callback = None, pickerCallback = None, flags = 0, cols = 1, typingcasesens=0): W.Group.__init__(self, possize) if picker: self.listpicker = W.PopupWidget((-68, 0, 16, 16), pickerItems, pickerCallback) self.title = W.TextBox((0, 2, -68, 16), title) self.btn = W.Button((-50, 0, 0, 16), "Refresh", btnCallback) self.list = TwoLineListWithFlags((0, 24, 0, 0), items, callback, flags, cols, typingcasesens) def setListPicker(self, value): self.listpicker.set(value) def setTitle(self, value): self.title.set(value) def set(self, items): self.list.set(items) def get(self): return self.list.items def getselection(self): return self.list.getselection() def setselection(self, selection): return self.list.setselection(selection) \ No newline at end of file diff --git a/README.md b/README.md index bd9c976..3685185 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ System Requirements are: * 32-bit addressing enabled * Internet Config installed if you are running Mac OS 8.1 or earlier * An SSL-stripping proxy server (such as [WebOne](https://github.com/atauenis/webone)) running on another computer on your network. - * Note: Macstodon is not fully compatible with WebOne 0.16 yet, please use version 0.15.3 for the best experience. The following extensions are required for System 7 users, and can be found in the "Required Extensions - System 7" folder distributed with Macstodon. System 7 users will need to copy them into the Extensions subfolder of their System Folder: @@ -89,7 +88,7 @@ That's it for now. Maybe more features will be implemented in a later version. ``` (This fixes a bug in MacPython 1.5.2, where the build system is overriding the creator type of the application defined in the RSRC with its' own. You can still build Macstodon without this fix, but it won't have its' lovely icon!) 7. Double-click the `Macstodon.py` file to launch the `Python IDE` application. When the source code window appears, press `Run All`. This will launch Macstodon within the Python IDE, which will create a bunch of `.pyc` files in the source directory. -8. Force quit the Python IDE, because Macstodon corrupts its' state and won't let you quit normally... +8. Force quit the Python IDE, because Macstodon corrupts its state and won't let you quit normally... 9. Drag and drop the `Macstodon.py` file onto the `BuildApplication` app that comes with MacPython. 10. When prompted, select the `Build 68K Application` radio button. 11. Select where you want to save the app to. @@ -97,14 +96,15 @@ That's it for now. Maybe more features will be implemented in a later version. ## Known Issues * SSL is not supported at all, because neither the Classic Mac OS nor the ancient version of MacPython used to build Macstodon know anything about it. -* This means, in order to access your instance, you will almost certainly need to run an SSL-stripping proxy server running on another computer on your network, and configure your Mac to use it. THis is outside the scope of this readme, however: - * I strongly recommend the use of the [WebOne](https://github.com/atauenis/webone) proxy, which is what I develop with. If you are also using WebOne, you may need to make the following config changes: - * Note: Macstodon is not fully compatible with WebOne 0.16 yet, please use version 0.15.3 for the best experience. - * You may need to add your Mastodon server's hostname to the `ForceHTTPS` section of WebOne's config file, depending on how your Mastodon instance is configured. - * Also, some instances (i.e. `bitbang.social`) require that the User-Agent is configured to something modern-looking before they will accept connections through WebOne. This is known to work: - ``` - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Safari/605.1.15 - ``` +* This means, in order to use Macstodon, you **must** run an SSL-stripping proxy server running on another computer on your network, and configure your Mac to use it. This is outside the scope of this readme, however: + * I strongly recommend the use of the [WebOne](https://github.com/atauenis/webone) proxy, which is what I develop with. Other proxy servers have not been tested, and may or may not work correctly with Macstodon. + * If you are using WebOne 0.16 or later, you *probably* don't need to make any changes to WebOne's configuration. Give Macstodon a try and see if it works for you! + * If you are using WebOne 0.15.3 or earlier, **OR** your Mastodon instance is running behind CloudFlare (i.e. `bitbang.social`), you will need to make two changes to the WebOne configuration: + * You will need to add your Mastodon server's hostname to the `ForceHTTPS` section of WebOne's config file. + * You will need to change the User-Agent to something modern-looking, for example, the following is known to work: + ``` + Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Safari/605.1.15 + ``` * You will need to use **http** instead of **https** in the server URL for Macstodon. This is a limitation of the *urllib* library in MacPython 1.5.2. * There is no support for Unicode whatsoever, and there never will be. Toots or usernames with emojis and special characters in them will have those characters removed. * If Macstodon actually crashes or unexpectedly quits while loading data from the server, try allocating more memory to it using the Get Info screen in the Finder.