From 833e43a98c3530d849f9052226a635040781fa38 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Wed, 19 Jul 2023 23:06:42 +0300 Subject: [PATCH 01/27] Loups request --- .../discord_richpresence/pypresence.zip | Bin 0 -> 8353 bytes .../discord_richpresence/websocket.zip | Bin 0 -> 48208 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 plugins/resources/discord_richpresence/pypresence.zip create mode 100644 plugins/resources/discord_richpresence/websocket.zip diff --git a/plugins/resources/discord_richpresence/pypresence.zip b/plugins/resources/discord_richpresence/pypresence.zip new file mode 100644 index 0000000000000000000000000000000000000000..250d8e2e340019b5f60fd2fed65ec0505cfde78b GIT binary patch literal 8353 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%0Avs0xm{uZumdn=WS+r% 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 Date: Fri, 21 Jul 2023 11:40:52 +0300 Subject: [PATCH 02/27] Delete plugins/resources directory --- .../discord_richpresence/pypresence.zip | Bin 8353 -> 0 bytes .../discord_richpresence/websocket.zip | Bin 48208 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 plugins/resources/discord_richpresence/pypresence.zip delete mode 100644 plugins/resources/discord_richpresence/websocket.zip diff --git a/plugins/resources/discord_richpresence/pypresence.zip b/plugins/resources/discord_richpresence/pypresence.zip deleted file mode 100644 index 250d8e2e340019b5f60fd2fed65ec0505cfde78b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8353 zcmZviWmFw&leRbR?k)i~9xMa~I#vgTc z&27w`baff++`VE1r23gq1JAGNf^^`2B{jIA_V{alM4%OEPfnd^?XjWame9UFmyv;} zgmHvzzaMWO&ZmYq^WUiJNp9>hnKj}$Q@ar09$@J{>VinK3m?P?MHw{jtl@y*n>KeV zLEwj8rE0{cYc9Y}HIQSqIoQQ+oz9k^-~di!()t-2^pe*OG*7pW0z zKiXV$dVJy)*W4_4Raw?rD)j&@|F39#*3CJ^g$4k+F#rJEe@DYW-_h96%G}t-={+6= zYU;MD?5N%wx;<9J`7&5!VjxJ1*O)~KebNvlpa6)z5uCx^dR|KbRbnMP_pPH(3Z}q0 zT?s`eG(FtWla+1lQ?4@XC~sAc6iaj=+dzIH*X+(L%pa$`0S^|Z@=*j{5SFA7R~o|7POw-N3s?dwoTd#EvdhZJb0z8_UxAa!IQX2r~qi!rfvNoSQg?X z35e6_3hA#GW=Cam;1_R4BG>T?krmxH+JyL6w07?sWRsCcX5K7+_u>W1CFG=NC<5Ih zJD#5rcA8=(_)`H}Gq^@fC^}0Sq(r@B^A62oJ?ut*zGPT`S z^QVi3s|Kx;1gfZjMP|9^FKKVzO95Ut&oVsLvWvCvF=>H|R&|ZuV40kPp_ig8pJt_+ z5v$i<&BXvzK*eV1wY_wu-hh4vA^S5UpAlkk99IF85TcRGf>iR>}>` z%}M7W=i9Sy%(~JXGkcvUh@%=5KuplRQrAW9{EdlEPgOjZPJrvmUyJccU=qYqw$qkn z^wBANDE(SGbydIgg7SlXQVLAU2r+NPy`-hV(s@Kmp|Q947=L%jx{?%)Aq+ ziB)OhBHe{;UTOfvAUx{Cr({a;2GY74Uma4&40NdmvE*jaM7ZuMpK&vLt6EG#OfPe1 zpBD&N4uk59Y+HB58^=BMgA-%k!1@~rxHI2nHw0i#vPLXx1x3X$4ci2>_~X1;o6RCD zE^rWbA~k`v4PunOH;oY9O2 z-me?^OQb+CEVDZ3u;N+5!q{5$wjeYS1=Ex-Oa0NhElK&mscBOZxKKD@`qh6^b1Z5+ z36Kawry#5LJ`z-vAAQ4~I~xqPm7es2XD_y7fQ@A**;Q#SCvllreaW{~S$e@l+vQb? zN-8J`9qYbNY<1ZNk0@%{L#niy-f5aq?5@)rZgs1}L--~q?(wk2lrx%T^+BR5~s4>)(3 z2;hux+37XMutmaarez03lZAQP$<^z?z|w$&?5*kadfdE5l1qf>fJ2?mSxCY|7(I-> z8Qcy&3fGfCFuP`Q(Byuz89cR6BGgFl8tpvQQXSK?E%a|TW4fGb%ph+f=|k!dgZELF zwk;s9A=`%aPb?vXH;&CBF)A`tty@D-)V8f|(0b8ZSfgrD^Mm=dOJi{O`Wcluy+v-HnsMz}+<3lBm4zG!EL zo3mTDW6BvV^AQeHQT&-kjHj~nYztWdPo!)l(w!JuED#Gidq{ooS{-#lAM&&|fUu&m z!zz(0+#{w`Be+Dh=Bp^=QfjD3%;VqIlV1mDkp>Y_Svwpf1tO|jK79LGr8`an*XaHD zhz4Z2n_Sg9Av!Xf$~NSTYO~VF5vE)OW8U}n0I#de;>{;@aS9r-&2B#J#~KRm`LQ!U zwSkx_6dAsFg6Jhj%ERxI9T7mt6EfkA$-(rN2k=?_j!w!49)irPYjlvc!%j0Lg?rML zM&{Y}Au!PAIZqn@K8J73-ZKwQk;|-?H`%t+U-l-3Yr{n>I|g?Et{#fcQen@;KI0)X zzWF%kp4Z{+K%fZ7>dX{2eK@RNZ?8R3Qd8KCibX@TWGOcvz0T_Rc(JG{sD;=ENl*zB z{s#DWH7T>N86klM0E~G6fWIp4KWgiLD#@w3rtJzlruTi-<}}P`iHVgLZG;8?LIeLd zYwBt+4_L%+NoHwCGNlx|Y;;c>uHr{FONAJNF2U&H^r1W?lYu-u(_*UMWAH(hEO7^5 z&P|e|?VZoXkRPi8@y80($h@*Zo%{nc8zs{a3(T4NBIu%U3&!VF7dw$&3W#J`7;)yb z&9uQ4P*ig0BZA!$$_yj(fUmyi#G^9bf2Gho&( zU~I3Tjyq;b%lqN=v5E>B6-NHXOlO8GC7#IpzCfCv>6VQCy>IGHi< zh%?bkw0Pa|~wE1tP$FQ+y!%mX146Q?S zSMRcJGq4szNad&!aJD3uq@FFxutCj zVn$2$Y!n)19L%GyxfBUWeFi4qP9xxVl6$25Ij_x=cj?)AQUcoZO`@}f>1FC2;;SJH z-VGVFeLrZ@FuP7atXQ#?v!uc`P`n`cVz2dG)z24(?4g`q0 z9X?;2o)4gzsIYC9FY6of0B&c6R!G#y0=P!9>ovTn70w(kCy;=`?F3vAKmE%6FP(%@ zqW;*B97io0cd1dAy>|83sH0`9K-)QlEX?+aLe~Tk0Xr-kpG`IKe z7RCc%XrA&@@6^{UAEjVXjy8*y*-3`jWcv9o)8oBpnQNffyH7*Xh~u8-uVyWKhjPQWk%A}=kqY4qoXe8MF&`#;9 z#B@HUy>N$6RV|xjeW+g9usLFC;l{gwyPi~LWuf!+&Sb`7xO+K-MH7T?{o%Pw0~~Zo z&?+u=*A;0G>GNFY`4j(V6#l}vnb@uIVua;v^Ic{YvX7E(kaBk3RW9CcaVMhn=MUGD zJ|pC-Aa}IZYSB9%tf}G$q_S#}-N1V#;{$Sn1IeR&b)Y_^R9Cy>lmepVu@%TpA@2yq zcp~$I&((3w`gYka#>(moj~jW}y>~26;94R1VM*e>3Nz978QO}cL}ZXOXGBJJ0@ba= z31At;bvGqZTmlAseFchwDYYL#n$v8!0kd4W={$g9SfM|3*d{{%nzD((f{&4oiE5HX zB4PPp1L{?x7&#?(%=1&!dG*dFflqP5raR2>#X`KSmX`1cX~2BT$_t*&)9^EsopoPy z&gEo~l%VAir%niZI$kYMMT*QhE}0EXb~SfV)y_7AyJu!uZ)6$zX5936WjUP-Sig@*%ONv3J{hJSA|4nYdxxr*z?rF z#x023yqUz*;U2bFVjcTYoL?_ zV{opTzVl?ftntca8IHS+{e(mg_ud^C+mlD zA{}f~NPVU81yb>>82`Z_BnI{nG3))F{)Nluw~I@pkEa#iNG3-Xh+ihgefIaX+dkWL zq8B5CF)F`0)Q0Er`DWei6?(ZJ{_wstE$mP`%ww5$CT}*=Y26xEA(}RY_-CuQ(w-3uMjjz)> zRkUc+S~TBm!yBy1zw@2d>Rb5dpM;(1(KbCDefhVbAZ9X)NCyW1D&9rKAML5Jo1w9t zlew*p<9mBrktk(Jz>GS0{u|SA<bw_v(S0sb&Yze8Wa`rY&&hP z`Ch}H?Q3cn+YUZ;xxWq|m@uq%uH)Aif1c3&tRicXc;~ZyjW&gv(&s>v=rn3@+}F$ahS2;Ier4QysC@-69Wj#_Ji#SKijs?!G|{=7 zIwbXHc}ZfN1(XI@i(ZaVI!_Nh-UGHpA3TkXaUjs}IvJEr1ev^ri0wtVSt;<;8E$8; z;v70cYqY06r`t96bx z@i#eJ*{PNu&~B~rT=x3jQ>sTev%~yderT|-|J=o|PEw&^v@0dbmFo;vuo7xZVz*Hr zm_2Ox0ZK=Ub;JizZlA>`qIAfo5eM4X{OvA5raMrb^g`sS4kDZOP^vHOU`f#WE7-w7 z>aSH$c4G&&syX3zL;U9zC$55_N%%uc5BuHQ`VL-;{mN(**=`+~ma4L_6Zj#^0U^4i z24{*{0t)xO&RqlRG+VP@xVx0HZZp8dG=tAokA{wT3Ica?&5b+B?)N>@T zkinLvuBt_g+jsb8$_Dz@@WxWMk40~5+B)1ZE+Q-o#0@r;7Yf~(L&%U9>LYrwq3V_7H4uXrg`XN| zf?&ghSF9*Olr*;LKi@hX#Ul8B#+>Cb@=rjRtFuHP9D#B^esKY6HDj%+5FU*=EHnk_orp2{}p#S=|J z(7hEAr#!*8h=(hMk2O~lwU6Da)h)qAPa0rT#F>N<9sj5h7$!oaOhBv}hSuh-`T;o_ zaa_{UO`|uilr#c!g~fRz8l97ZGklP##t+GkQ$rC7;yii6U! zu^Vnj`b5p8cP9dGdG)pic~wz`m6IP24e)NDgmvePK&;U zQnc%KSR}(C*&-BucGZf1D}c+wp9q?swH~^I%NCAGUqNUQ6VgJ@@JDK?!2I_iKXo)S zaukcl9k!^Vyg7ojVrPP>I!WKr#=8rB6ENPGYTr5Dti743@0@|l+iXKH#b7uuTQEGHrBUVCNesR<^W4>`+Tj!bP{qS+grL|8G+%4X@|e$s#5&ax~3!z081B6)w2T+>~|Z zNnedE$0&0AMmcDUA?`y@8YPFNgDCn%QcS}?(LbKWP#8ZtfHxHg5-vcziRjMR&_$1f zF#tEVsb*9|mzwo}hW|)rpvzdapn4t8JbQ z4_Aj+S~3X1>h8>$_P9{g@~zMlxcIL4xS}EUovd7D5@{OoAE+*R3|R@;g{ybzbHSXj z2t~>X%mr_)NBZc+58x-_=%&8zd{*(0m#w}nWDsxUp;7qS%iMC8AGl0(DZhgVVF0R4 ze{SkYOFQgG$!<%T(8=67z(%o@SaI~C)gP5bN)5p^_-?qhs#t2?PoHMvx=1*XZAuEo zJY#1^>dQa4pz6?-!}*28sTVTL@Yqk6T4Vor!xwcm#Wy?saP#qO(QQT>)9wI?asFLh zUO;)#QpYV0kAb(otNMe_ONc~gb9}6xYolbLW^t;u!k~tS$GI zx_%VrdieBapz{~LE(83U!O_eyA(p$W$n*Lg9hj<-9KxEz9g%-9XQ`%gI$C^68u3Mm zcP_5unTt4h(k@i9^jNO;ZXd!No)vxTZ9Y#S1D4&xF3s;y^m3sjD749hO^G}=qu;LsREd;J4|qSAK3?oU3M*S~57o`lq6sWCE~FQeN+gM@Qs~+t1Y%|r9=ddGkDzo-f1mXP ze)?!_dx5#$w)O*#(+aQn20CRLo~0IiP|x2!Wwuo|EHD_P)J4%3dAO);{d?@_X1s+#6r1dkj=+-eev5CRVO3O*y+QsPXHFww zQto44tlAHC4F|aA*CkH~RVp6*1q!OGRNl%1Q}ZiwJ~y|QXbx!Ctlw+0O_&?Y6=A^J z;{Id>B2-Xnys|`A%^Sv%Jw>2V+9c;5_}^Sy{T_wTBroSkJKsGjXA%y+UnS3U-|81_ z)(*?gc`yP4&PGEBnsJzdA60%Jj+D(CY>6`)x@DG`74vI}6WX-D5 z6RrC(h~~!nf*bon2$tzg2kw1yy$Q30+c*jIgghueycXp%_?ZaN#@s1WNXN3?k)P0q zpeVCgvGKgJ030GLyFZCSjW&|(6@tn^i@fSNWg3DD*$xoaK|kgqrA$E>bQE@uwZS6e zm)19;IGb;=&zT~0fg6hi0}LIvaN}GCQBGOn%lsb5Ksq%?<>gO4--fj<5X9=DVct?> zyHL`CIeQ4)U@=2e23>7A=wH&-(;0OZ=V4)}P|TweSI%$pA7lqa!b!Ovf_yF(*p%NB zo!foS3fOSi8qzPzVo#hL)IP6#@lT;0!2wMJjq>0A)>3j&Ln^)_efst9$<7G_0KCty z{%JQlJDFSkC8QFSHY`_|P`&SU$=YGKW4=~*VD%EGT46S#_rm*|;Lh=J&^8)Tv{+NW zZny%aDz_=Mi+~wto#)<1zdm<1=9(x25$F=w#?G3JwIJdFlnEz|b>(1&cj|EwrM7aVsjDsM!;gNFrQ@&e#u3Up&SaW)@>H2oE(T!_nZzFYo zL@EqQqALCHKE3wW1S#6*k9@@t``P~ZIX$sVC-B;YCpL>`n)hX+1 zP8_^?G=73xHZ!;guGbVW1x_0qXsMrf@8vRFC9P;n1|eJ>_=`YMr6utGLL8{eS5=B~ zQZhDGADDD=cc)sDB7{;*4=S%{84zuuDv)3>x=$F3O+6MPiwD`8=}6|pGAL6)Jd)PO z;RSuC>Pp(U8o!;J93?69bZ?O`LF6Xc3zz0$&*)(HzhV)0i8tDr6kj*tBZ+fg@upY8 z$dn&TI{&F-H+;)?$i4a1C1wY_EXdvKcLJf5G;rlKnE{K26W>(tQy%!ew#zT3n}D3J zTqkAbb8`*Hz-fKB!=y{iZ7#wc4@#q>52@UKpmAAKAt)5gUilIj)A+~22Y!u?A^@5J zJmW^~y*Cj>eET;SX*m1M5 znVw>6aUMB{cW zxQ?yqhJUS}<{f=;s)~{x3WYr&EaU)_NBjNmse^%I{(t}c|DIUCXFA}0c>UkUKRot- z%&-4s{5j|S>*oJrAippFVEk+9`zPg3fB$ca*83;;56XYs{yzzSHuQfJDp3AK_*Z-X zC*{v3>Tik<`oAduXsP~W{8=ggW~{$E4gYwT|M@-OeK+|hPI)O{5EK9a00;m8 zj5XEc!T_QTKmY(jU;qHr|8Ct(44v$ZtW2Eg_4F)kEu8iA=xV*dyIF>!Q6~_}pgoz4LtMi-6Ui7|e(uE1pnv8^h(7w?-i65}3MUh395hOM z)o@i8GWy2tL9MDIk9L}aQwvI{&gX@0fWB`%uBsK4&b|a@QVkq=J3yObdU{|bcaj9L z9e};M1^~K6#=F@bIaNgO!G%c73=(yR!T{s5B`3z}D&4%+FYv03*2R*+Pzp-X0P|oy z!5a0qXWNQv1kc0Uz!R0ClzDTp+9O-^Tk?B^U!+jXRiTcpGk($@+OIm5@s&&ISdL!9 zBagnkxw6oks1+)ggKQW@A>Qre1i`93=XISibRs%c-3VUK$?})iVY#{lq+2lsomt(u z@;Bv=1y*wPFr8INlX^kIMK=E>qQ;{`de236V^p~Ges;L44{&MGHGNR!q1LMpIy3f7 zseKsIEx?bT#@$or?Zp6$;1Ld(;e*`}#e-ehCshJHpnlx93*qo$RZffxw{Bd46*$OY zRKoz_4aFYbyDJpiC`)l0&V;@1a8yVdPh9T1#;0~RzVzP!4V6hPemUJXI%T{aqCX5A3U=_ zqD(|J!)n+H7Kkj{kf%DKys=80McEdj$@;SrXE&b?;{4Z-k!2L+gPXI36vcuw0amd5 zT3R1MtGj6<4x_CVp+EwYr-R~=YypvkhvU?)819Mk62Azvn8*TyjdY-a9$20L)_|{B zzcZ`Uho@+w`pq&-u9kv=zP`N1j1;*X8aW_1RXy!jy3Sw6c6{QO8~H>DWt0-<{cHk3 zP_k_!+Q2xy?VuJzc6kAx+@pelXrdHmzevXWuh}GyJY4Tcmoq{45A-`HSb$Fy14^lY z(#NC3YnaJ;9AdKFMtd4;d~RiS;rV38&t}b+FXTLT7%`*ANAmqPvYx?w-d|FkKi7`e zbpIB~P^N*U&xg1KHw60uWW>y3Sxc3TVrNJ8E;2s{cLHpMjlCwVd&E4Q9P>PT<%E=1x}E|NY|NO&ij)Rsm=lK) z<`HEU!l~(hBnh&4#p1``#uVB~uSj~KP5mzha?QbdZSj8nq4M1hIc6{v>c6C~!6GU0 zVP1U@_vs!h*wY=(p;XglIp=({3o5#$Q+$zY2)Y@D=hMT-XEXcl@eYG0=w!FkI6eUQ zEtpJc-0x@3we4Ni*c{AY8~?xxsfsAsF?^9SZ4$?~;jyvc%a6ZcZRU8;M5b?=Z6Paq%UWF7~1&*^vKS#}a{M|rjb>AOBz)h3w7T@wb zsG_W`qq}ux%vn4Rp1vPRm_0gNzk(OR=TI9x?BuWcow@S4)x!4Mv!uh&fvMTLv0z2R z?2rAvAK7zY$aJRNnwTv*kgpBZ1`v11ZqJs<-c>sJniTpINt3-DS~A?~V$JLRDV^c=aO1h{y6jBo@8`bl-Vp5kFa_}F?fx|E{V-kY z=6f;?VNsmrNWJNpEz)Y)IW2fG-ZW_g?$(vkiwat0_;fPtTz^;q3RxT+~AZ=O5vPvh%t5#7m#x<-y?_lP8j*j8jUkOIe{bY~ zQzFmb$X4;5vPQ)tar$dt;WI3k-{A@HbaeLaPE*9E4*4-EBmHQi_aX^3j7UMe)<;gg z48aLH7};-(Y8?>g@#augBiC1I-Beba^ZJ=R&#Y-NBqbZn!-8 zH))x#sGVq2uTDu(kD*%Jrcz*|qpF3R(53J}5x80D9W&@F9e-j9D#F0VAT3=TTNF~c z95#zk{25nV7w#r$>CV1tjU0Xm#83o}DnQ5QUnU5mBZnD48Qe@5wZw#>0(6dbTnzxn z!2m`O^uxY}e&<{5fmU4*;OK)HR@m5m`MM{w+eM#or>p2v;}M7%*6k~kNq+e$!L7${ zL2Z(WEi8E2`H~#!guPjLj&g-xY?VaPV|B@|^*QJIu=GrZcst41T;H!q4$k7i=pGq6 z>>9knTd!NyU|5eY)?5zyv=p5Oea@)v1d~;-GJt#=Zy?TljhnjsCn;)2k1;NVhD~!g zV!=#Va0^!sr+wh(gJ}bB^1 zOc%g_1RQ^eWY&-$uIppIL^p`n1Ddo;n5$_b)wCs4#gMLhZk`ygnkdAV`bc*eW%lU+ zUSdX7=@SDDo6R+yET!f4_!0Wy@#-7FdtQrI$%?}cK)T*#dGT&8FZN<;&_RR zFMzLA-@_eR$NHhkL(9Yt%q=MS>X30-DHg8Nom zRAG5R1I7UmFh(Rv#@%L^h{u$86cc;V$EY$*(ekrNN~Ydv90a`oK2FWIo1w;Nqn}&? zR%~l4>#P7&YdI2&)TGH+ccT$f*FW4fqfI#A@*$RtBMLNWQC#afWeEkzXgD%Inh7AMz=xfxm6Z2L`EIxu|-Z4?KYf0R`t zr?jLaki|a}dhD7e+j@D6<5TkIU>>4k#VY>zRgdz+F;31wEsKR%X)@Tf5g5ERXGZ4Z zd4WfpLKRoJRYB}GZp!*Bq}WmWjKgsza>)_~O=V|~Nf{6R;n~gnP`#V6t+IhMs25@a z1jw*Vu*Y1>Z=C*zOB~t^hI$eF+b78kysKO;vaaUPYO5R7-SH<_VM4U)1*?%`7-dx% z=EtWcOp;eqlLDU+Yg`!!7@ za%AWqYK~;}I98q02j32bFL)h0|*8PqXglgm?SwpS8$%vTq*4)1O@W z9*Ti7_yks%OR1;#<8aXuEmWzPvtlQ0XsxUMOVOxPNBajKSJm0pN~SO%aWC!1C9R7J z*f3vKi1z$_LNKB}{V154B1P)AG z)eWs&6{ZC2w{gxTlz*49JgH;F;!VlnZvXC6LTLRd>5m85lch!T-gw-~E9RY(%R=?KPZ43HFA5roiBhmw}(t!Rb#lybx! zr_Xm!neg?2>}RptYy65wI+S1-IL=2vT1w0}Kw3)8Cx9geNdx>sv^{pm*Qkt_N+ZoD z4i5V(AjNZ!=74ETGWr^;`8XWA%sqbQp-*+oifTlO5}cV<1!H&WX*|wnPw0k3G!9}~ zYV>=kp@CT=e2Nonms^Zdnx${y0*;yZ$@WLI*TQimMdR5I9?L~mnf46UgznwP-D}4Z zS}6n!)%|i>| zzF`F92o22#1My|ofbX%v@M5-cnOUnm9JhYIM@GqLqtOA3^UXVuPC}@9%RaVNpe&LmcA`EpD zz0wXdm(Ps)!crgZvrQ&CiGuXZv>}YZtZ#abi=1iuFTaRK>lDQm#^hm$=CIPNsu!xz z^Qrjfay&KGN?BVqatPP1r*xruHUhJ~PPhJGwTJc}Y793}3!S=Grm4w&&w3AnO< zRG9?$aTsX6wS7Z+d7UtHdK^`sa424c+LNjhQ!5R)DRB8AaLwY+3?3QUIw zok!``9WJ`=^qkJ?^TGgQF$CSQ6P5W63h_D2@_iwA5VGfEeQ5Rgm>x)F_OMSx*+S2b z&8ybJ#Y`0k5;ABM8az+Cgm<5ILWPjf&@k}q;p=IeS|sA&H#T<%+#{422X@} z!Ts<6mG6D;C0ow+H#7|cDj=5wzdM>LR*W*yvKO832lGTu=AnRndpcsWP+M*Xq9l01 zuI3N$e>40&AX!cDe|Tq25dZ-3|H<(7_Wwor6&@QqZ1%;y7ix$Tp%R|RLoOSI_ey}; zBkdpUSvcz!4=CbwEs5mj7t4+|dR1q^zpoh>y;N;SQqqf;0op)yqOhR@ds7a2ZPF}l z6jZ53rn9HYwDCGpxq~bU$)$Q`{3@4$Bay@!X`#l0Qzyi^51L&5^Z>)({wiHNjd0j1 zSI%go$}1w-CHK@vMD96PD8p**BIRmTjtJ}6LygvDpEG{lzSSF-gv(PNI>}hrPCGiN zTJF7v4cr7KLuy^xcc$7UC`xpfl7o*h&bcaK;}X1==C!N0uO5ooCx_lEn-A&QFkUN! zMvu(O;e^)Ixnbw-rs}p52gfH<3n7wd?2OXV9Y!DzAsa^Os!`r`#0pflKQF zXnjhvGY_6H{z|i9-vbfI;`*frNNfTDJ(rg-zl$5;8McR5lR`t62|z)`Mw3KGvNeFO ziBrR*GKuUN&MmT)Ofm(E(H=ciAjT>~{}@27Fgq#i$dP(Gm|Pc?6SKKIyS6O{Y2{dv z2G3l{^2d*?s;H@}t|>DCi@Ghg3kXLzRJoIwbM4 zR&lEJbHwn;#?4XCJKDrXszePPPMAgI0ZAJGeTD3X-w)ib&Z{Q;xx3n1(H=Lor94~T z+(x&vwzjjirK%}zSEngD+gd#xnL69rgDopp-yiC)t(BK5#G`(^^kBbn=we(5>3A1! z#@ii-Tj|)Ws67R(L=&bkRFf($U58erxbAWJZYN#q#6}X%$R-hb88!)2sgB1~W-`Dl z5kv|hlm^JIJM0Orxu@SR-Md#Lh`!$L^ndjB?cjsZP?|fB9pg;$OM(UbIwq*}ct_7L z@0ze1O=;Jf#^2Fw4k?iu*-SyvST@UeXz7-@qy6z}5*cHt*Io}s+EmWstiLW_*>uIs zk%=@Fh2{WgCY=8iID~BQ8CgIQ7W^DCWX012U002NCkXmHJo@j~Pl6&(;@DG?#k-S5|NBz1xx<8VV+AF3%&S zMckWAsY9M{N_y3T$`?0E5-`!*?|gEP?Q_v`9msaCsy~OSw=P z_#H=)gkNON;REp+xB$v9guT6|@SNT%59yl=#X|Q2aYIQhYKk_``w>G=oY1GOEuFtU zeycEVC(Yep!Q%&=1F~^ZUBcWHK!?i_M&L|wg5|Qo_aH*v&y5v%-Xl1-vtxuBJY)bK z2d!N_t{*NjP2@MUs2BU>MkW>=QbTrhc{x?bbL9W=I|8*I<2AqjM3G+1CFYbM;_)8& zrOfLG@r=fRjo*}(WV<~2yiKI?=mip{5`-HVD9&>O^$Hv~SFg_+(1E!36=<27o-2|-WC>s0fLnpM1hUq*)vLo#|-8WY_oB{$qI%tM}w7A7KsIW z4!(=1x+q=2JIqqBAp^-tIjc~1A=n%~5(0_9&9kB(5*`d++CH1`2F=tCYZsSnhWjkj zA5!s!T3V24stN^w7i&P1@U^xxp=6WDCiyAIZ{dL38Noma&`2#kPxF*YNM#D`aVKd6 z+%{Z5O%yWU7Sd5SDA=RquybazrWZKk^528yC9Im{%tW|@XEZ`fTFf(M)G74L>kDin z0K+n15z{hHtqZoCPL3MEU!8ybe7$LTlSj8F?bd`Rs% zE=!8Kcc&;_B8L9H`?d*iQ_T@dltgX({_(2GSj|}__;T*C@HHts2JVq5Bw0) zN6yE~bU0!jrJv57!IyG0QB7Q~W6O2G1g{;QKg|#w${FM>BThROHWa*2P~I-1#7r7h zX|sWkap=Y@nDJvG0xb&cT;*4QOudImGJ+jk<0g#Fq+JJ}8mI3m8LKgyj;5uLwMCRq!zNdU!cJT{30r{_dfy z*|z<-yZHEiQE!o1Kuv?QGYkGsWx)6TYZ`M-l^mr;5b%mhI|wr+2t zuT?-r?sD3YaSC+epI~HUJkSm;U}&YBZz;*{VcL9SLwIFX^aAxpc5LE1)3GTlOd6je zn3_{Ge{-o3m*?<~TG%*jxZP(9+KumMFxj6YcaUK8Plq!qSpXX2d|IO!j*U3Q_tL&I zA9V!i0+#UX*0*9CdHJBs>4(?^+GcE3Yu84+_Vm}D%aNL>#WRd5aoFANbqSHy>6d@c zJ9)Lhvo7o=uhpQs8&bA|3p;WyY+k!e8TlFV0Kg02r64<5dIBgL~_`TO9O**IGN06=_IL6i%0rsO9mW7-K!Gx z1BFLZ;h{q}%*293DR!`ycJuiXZ!aoNFz5Uvh%tm*5sBSJBB3&L+I+zWyM!y|jpkvE zVPZ)`^ZBU}!Iq*YEJ*~cTp~Fk3RAgipmd6{Tik#Ja`9wjA0alk%p$ENHLu?4cT9K) zzGf^0>q}6cH_ViZ1Xz7dqoxL$pwDvxJ3qC}O+;kVkGr-8fXn6?3(kU6hA!CbTcn`ktMNqber zNeae78j{x>nt5ahrf4?N%mOi2d#o`Tjb+enb+B~4dqn)s6=WEzxX}jYNpS`&(03;X z!7A`Z*tEqt5}9TPx&bzg?HmXPZY2zdnE_WfY!w*_dlh{vI5VnIk8443cX;l@52uriBdysr^a_(&`eHAVr}JqsLgx^kS~2R zNo8G>9xt$U-5AAOTj?BW6=G>^Z?IGTh(AWmzS|S)Uo#dAYmq2U0&<-`Z4g9(43F7D ztNpnlFQ2`5RV#krY-VT?2(R18khT%y)?edFi#-||yR*q*{1sON3mK=ZwZ3FccHD?O zG}v}0LZL;b=>*zGFqVQjZnrM-$J7C-ADCof72|e2Cs-VG&kj4IiW_RxW5}hAaMh97 zPYHBOf~;KNaN(oV28cVef`Io*>CB9 zlapFXVM-w3ZlAl7)W4702+e7h@oCI(KX43s`>{4>5#jdHLYT(nD2-6B2h{CWe9PB% z)mhl#WAV~AViGha6p}O!#i1>;Rj_rgOS-R!EV%A?s2?DK1FcfkmEMgtCqLs^I@$ND z`d1%=4LCkk*4*bToXJRdbV~4T!R14Z8n>KlB3oz~_i;5CXpa&!w8d&<_yR|R2kNYQ4$-C~heh&sZ=$J`8S!2arr*oiM?18DA{ub)wE2bs z?Sh##+jy+iKnUK*pf&jv{L|+JrAtI%jhjN6UNY2!t6XaB3I*(E82-v-)kk{~OelBq zKT5n~+A;b141x#jI8=ZOsve&Lz9OL?u<3JHDd#x zq~#pnwJ_|xq7m~)b{@18dWP?s|Bfq74AEqYv+9<()A@CVcXlpE)XKg|mw)SQdFucT zDMzcB$)kTv5&hZ`$=b8?vBTVl=syaZ}} zZ#iJVF-A z!^QBfO=T;8Zl}^XXuLe#AW8v^CGkRm_dY0KwA&n?d;=}TS6DXClVT7ktuDKBl&~Dd zGs+GnhqVh+H9(1tgw^DnbQm0Hp<75hP{iN2Ms#63d%RrdIdKTJTN<|?sHPZb3ypLI zeTK}!;t{6_B~~t)>OUGOX7$b#x^Fs}nG!>xBeaz^>N*c6ClNX*Zp(w8*E}8wQv$EK zj`kg-;Mri!_tm70FW$}q8#xPqj!A)YF}tUeiB?WR!cW1Es_!M{W=Od-i^G+01;|d!&8=U1lw^*`RhHAa;P>P0={8xe=9E4;l!-mr21t7m&){DyU0(2>>LfrA^Eb6WV|cQhOif4CwL zgBd5Sz9|rdXTQtnVAhsCQeqmmIhSpb4FjNNOCRSca1UlQ_qrYP$&fHx@Wm=WlUP`) z7-$`a$X$)py4^6PUU6KE{fFgP0HtV7MKh7e4|JG^~npex&PXPu9&Q82J$+ z<8(B5?_1-%qlz8;v`fKmz_Je>pX0%IhnYwO?ud236TqDJd$q&o%Ir6f%+aszAxe)0 zjZLrv3(Hoop19;SA0m~(B78j-Y(cP4jOt}=^pM-#ot)e}(qD!f(3%!)tL^G)|27Sl ze0&6HmA+UCI2!u~n`T@*nvE z@!F*ui_oo=JdTaoVs@1cFjd%_G9L&-F7#=3%!Ef10U=4Tf{d#uFVMyVi4O(yY|}uf za=h4wbZbwW>_lGk7q(3l^Ro>kPrm0fV}p5DS?&{c$Nkef*$?p_g?IHEmjij5P{|V_ zpAtRVgVH4y^pyyRDC;c|Mx^Q^69z{9wfxKn^Q)t?bVIT{NmJ$h{4~#~P-1;}dot8jM+0);uRty{c=F4S#1}@`&FDc6c!UTOjrQb#NZ7cDfgU zIF+J#Snm-(+xBN<2qStF?i;g0w67*|yg+LK#t_IzmH^w;V>>%Pf_deVEx4)gPH|mB zer-kY=>E5$vnpcJM^uTwDG^srCUpmE&N=KX7U+xmFJ}Eq3LNSu;aB1-5p4HOEFxuy z7JO|o8vknwIqy9R>C++Gk~aTzNt_%imNR|&Qk)G-wl-LO+Vz_NU&yv{OdY{>Ib3jQ zk-#xuT>gyU1+^^{-Wu6-&l!WY&yU0ehY#jXue;6sVcR4}7}7FDu0z_`b+X_E@6Wdm zFON>*vkrj}mlQ;LXqTh1q=JIJ*;G0+Vwy@B_ctWbKLPUhT42rev+FUqLNRjU$e?4| z2T}|VQJmSNvF1xvj53JJVDnWED8A?@f_t!)UTcAS z_x(1@uFu4zyJ@$vZIS5mlR3q-zP#WT3O1;i&tUB`ZSr&+E9G-^YpI`Xmkv|mv>Vei z=8vib(~YjK68vf(z#~J2Yy60G2IjPn%AOfp<2;4s1mIi`Ql1Uh#uqsBF)7EZ5WgPHwm9??J22 zcTY2w%Fo@u%+S>X~-0bZ=+jfexYM4xEt#4+7H!9f$!5Zb6GivgDopedJQ;%>eI%IFQhmqrm zyWDdilh+&{)^_R%2wA*0GD?$VVpIGOjdHd)xPHFIkuQDx)@F94HcSRXrZ5h~x<)cc ze@f?@nFa7IWcw$<0WCM+2B_O~0-9!B1f~7}D{fN2APyNE5HgN%&pWyu7>`h;k_$;^ z6s)*z0=ifXvEh^?Fhg6WF1x@vgSJDlpURkjVBEpMNjb%Ij5;NM@k~-xG(B7cdt&E`2USXH<&7!@V-$wc3$Kincob0w7v*J<-ZIfT{)O`qIGyyy>Y=Hx-zU*OWt!oV29cyVkajff)KIX#%>-*rIOX=ld-BN{ZKk_Z#6dt`hitEEJU zK&&Tev{f#lCFInwq`Ga@hc2gq3G?V-G_v!tzk{;JE%q9E$~PpPJ{z*)`o=VSIxP^# zF9-#9ZP>A5JCTFo6}YumGP#6HB#7wN`v;JCKd{AcA$GG)EdtjvM2VNmAf1h0 zT0Y?dT-nYRz-sNqj&(tOJCpDHm=m!A_1P@7dKjhVUe%dyJ-T@JRnHI3OludVi?}(> z+G;!ZfmeYWKr8Nl*)a%y=2_o)dvfu$;7|)6!cqmk+DJ6^vnl?|Y9*S+ez(eE9QPCFuL{ z!LXCw;R?r7gd)W6oXBN+Y1K<2$6e*$=QD-o;j+|K_H{0{%w1R?`1>JlMiueV0OI>> z@Ij^-C|HW{gK9FFT~6fz#&b0G8x8l(0m$$N`~P;8?Z!A{1u_6YtnB|M^BLJWn*7&L zCp=b8TcfrAZ30>^!6j;{IVLi8SJ`KyuQ*~(J6r2YSZ<8Dt|PT2cnZXI_zKUnIsbkD z*1_F6imSU%XSb)|TZMtt0irdvrb5<8}D-{yc#}LPgTr&Q?9h{DP+}7MY|Du zcH6AU(U9V78p|bx7qm_{LS=w@#nYq#{mSej;S1JN9a7PDi&*7~3}^Nr|0FbQd$J^2 zH37MIeMX|)y2Jw+wyZn;%BhZq4a?T<-n;;^q9$DTsz$gC?1l}=_`@V<786Myh|WH8 zX`8ZlZl+vPUp><{T=T}NTqbg}`8+0tJ0z)~&-uN+stvJw%`8+KD-i?;=+>EJL6W#k946)aNlq;&*6HZh zSi*@IFMbLjjH42PX~55Ey|p&wqtkluf}mA=U(Xk8+p_Er9TQKg#3F*J@O0$p>ugQc z$ov0e{9GX#^y+lKWsyz@_6=s<(B8q`fZL?rV*4BjOyEs+Yj2$ zg&5Pcy2>R*1T(1Omcagz?vtPB%xpO?8*}JQ7uMTe%sG9XKD=gjbhmf(wx>%~m|Z<) zX>IKErB4?(u-;x1bhf|xf2=(ojHW=#z(L{gTR0xzU`EP0;bc7-2($Yf9q&Eskqo+{ zgyPAyRJ};nDLv>Ok)rLWA7pe;jaCdtx=411iBjBe!x`}*FMzU#1;Kv zp!CIL2@O~lS!O0^rjTFMlOK&d8e{l|4nPd1xaQ$WfA0ajN~VgY8j8|}3Um|Ft0!b0 z&LF=}eLxeN*aQD!PorN!#(P$^(CioA{J)sjRT}n>V|%60YLW=lCZ!{iD;vG zg8+E?)JVW;_VxAk=`u70*EWDg*I}%YD8B0dB>mHk*obq=8S(y>S(AZp&Q!=mLuywnkcO!lsL=YnbJiY2-IYJ0i<}L9cqSn4i~rlMf+0^jzZCrv+b6(TWROiH3o#SGy5Kjkw6bn zLt6uLFeqGtY$^+lp$RSGT_EjUq)7~ec5!RS^;DiQw@C~=jtQy}uoZApLw{*SICAH8 z?%VvtipX*!g&#(kFhR*-c4nrc{z}gh0M=lrBPeJGG76C?f=Fj)Ftz%rn2oxF%V5Ye z6J8Lw9O5vd8OW|f)E}t`C3GJ_c;O2pQOc;-`;*i|U%+lCA*jtUXP=-B6@h4$295NI zu4ohs`=Z(qxYPK-?mUphSU6$-F#{Mt3|huVIA}i;B1xfvIAEm-GSCP&;&89J`X-ea zR+~xt5tmHSo6gn zVjrxi`?<5O5;!cH+PM2*Mh#7w;+7c&t^?MlI+gOntvH`&V02)RB|sCj7Rn*}M|!-tJO|L8cx@0Tn&A^si8@mDrtpb%Qc(5Sj19sF z+lQ5b26y^q;SLr_kOsIW>ycR zoRvsjoBTn2#hCD=X`^g_nJA%dtR=C#8{n7<^s;umK5c#%=Bhq(ym$ED8jJk_;)5$0|4rLP|R@)FT?mz7w+*0OStB zs4?Gy4BgZTKli7$<=|5c&aiP0*Ltj01RIsjZ8AxGX!Pmy8vwW?*?`J|HDaN*|EIwC zFJvHjEFJJ9QJdG|jpT6vQ;B6x8)~|@_z;Tu+e`WYhm1^d?NKmYrJjJ$LW!HB@O?Z> zS!?-JaR7l$VTiM8WOc9oG$)j!-cmj$^Xqi=q{!ymASR<={||jdsVa~GfkMTi-CZN7 zbr>**+{&i%Z<2Av*nk*f>(r6In%2#FW^kPGrcm=d=BNN7K-DIobp4D#hBhh0~J0(CvA@8TP-4aZZUf*Ogcj!&OEV626 z!*(+zAS*Qg%TQ4y|F5VcQ^vd>=kdqZ@72F!a`bqffUZ9mzce%Ia0GCKmz6(zq7Htg z=1)IOufU_jXki|u$jKktlafXF)m_qhV67_t=|(q3>n6e%2grFJ%vj5Fnm9FwO(5E^ zq_~OHYCvbgX5<>Mg>JUc5h*nxWB=>E-M~)b1@b0;#w%cm>MyI}!cD~pj`?VAP`GYW zs6fH_O;u8pc1ai;ZLH+$9w{*I@{TkM^6k_-~cncadXQ`j%bZtB8J+;PdZZexX1xaWeX_@EZ1$tuU4YH>DqVT7{G ziBo0oU>mUcIR|+vVOtM{>qZ{O;{gc^+;@YgdUb{lRb{2$1C%E5=~qIYdRknQoP7SK z)~Swg!}YmTKwR5w4`v4?8#HrGDPRqK^>`JJs!qi+PLU}d?W8yjrr+E#M>R8I9Qm;4 zUb9bxrBaWysHFAe10ojE0rt*+)&g-8AufophiCTrw(z4j`^0&h=?@fz;?+VBA{yX6 zG-s3o)R?OeZ6ypBdYL^ot+;T--u<{#mc}9w^e^Y)?*1}POR}QIrUHrUn1Mn_KH&6H z4GH8)oN{W0=k;fs$n_u2EL=~)*RngxXpzfib$7S6cV}w!fnQbVOJ7w}9t>G}(|U8Y zrhFpZ4Ah+eu`Dlc@y7Tn;C42u7gw>o5}e@A%_KWLaM%La1L|~ufUOp7nmWwz*5wv2 zGmCRv<+cEGwCsmT4{db~PTbN7X}77jeTD}deLW~H7%+H7Fx*fLl42!ZGj#sF?(FSd zo!#Dcs8hARxr%Ic`Y34BmlXnEmnq3>OPh}5)=q7yAUM)(2q9qEfGgh_ zbR9bKnkrxi4SF@Kg(W5I0HssFNvPc4*liEBxZ`Un|G0D+7j6CAOxz$hd%xj}da`-i zdY=26I3H+h`+1@k*nJ$KW|*g|Tt_^Xs8Rj@Caxz&?w`S@bynIsx21nA{|A+oO1KiYHhpc@=x&s4$7LG zG#ZP$!c0!W#(>Y=R3uj-$#tB~QgZgA!NL7Ad21zGT6J{OjdJ|nN1&SIk_Jk*v+#f-?2pBG~P@?%MT6{Kt&#Ij^`pF zIhXU}M69k5O&7<3V+V=HWGc9hKw6mb7IJ{a)k=`us(vteI#N~2GvGQi5IRMq`jJN$ z*VUre>MH?JEwfzDoXUlELM75V``PhL==D1K46|Bt7-z>LqJ7RWN`xchov`cO+9};| z=)bk;_;`ttRdmjjFsL_5+VA?c?}33rTg>5$-(=g}%qTmv1-k8{o3>|E z+Jwg4&|3`5+DxTZB_Z6c!v&pgKThkZ6sf0hUkMWw+tryzY8x-U{(xccUSz=Sp4JnP zxFMEsR^JT4siPH=MgZHEuGaIui@PK5jlS| zb!dBu@cO@df*)x?y*$$%_m?}Y-0o?nQ~+`xzi;=A`^hk}kEa*Ut%9|=qkw%GfE2#O z$5Y}#Vt3?@aY}AJHfFXxi_{2z%OIEUW*v2f->MX1FGLrls*&bZe`!SH8ND@QlLY?u zj-J1lLr<7t55bxZ%d-}a?iZp?MfOUYDWyq=gijPY)+1DC0$@E_0L$73;xBB!acSh?H=C5?wtySWrhJ-wZ8CO=dj zTN;lJeYQNC4<}h$l*fGfyexo%AfQN9Sst0xXc$3%H){=c38O|2I_w`%X0=MfY8@ES4*wH zmS2`*);hAUlRWJ}Ybux~UC!mQP-b!B2vBMj(0LB zTPwO|UC1AzjP$P-?XyiRK@vpWjO`BI4M2KDi6c;+D&j6u58ClrWob`JoFjWak=*=} z^rD4WrCJmwlo`G{=EQ{OD&KQTYCYE z2UN|%bbM7#WNdz7=!H@r`mKL$)sh(p6^C0&16TPv^;>V!&1A5XiYIJze;{`16uJ9& z)NB$|{uPN$6uK$uoj_V_76)GHD0v$2aj*w|(k^To(&ea{pXf_ub&0l?vtjH&+;V?l zK|0*}V^2Orfaaz{bgg)lO*ci3dQo1A__g?kFQY)Kx(1J`s(nL(Y%mAzSCMG#OO;ff z$qpr&6){mdQpZgaj>+qjtq5eQ6zrLEH5Pb%j%0SI!2g@%Yh@g|%=mauy*c{N?&x!* zzqytEZNtI%^#=Y42g_YeIq#`A&Xz<}$m6hU$wy{O2@xJ1*c0wgn}O-z5>9xbWEsXp zIjyn2um02_`T(Y4bTu(-*qN`;J6RLo*CDUxJt6FXghR^^WD7RlyK(PT>{<+xIro%> zXUlJ6&HIWuvynM7@EZC!@Vz^1*R|_A(12cAIP!77D#;GZrJBs=KhUYG@Tl3lp3wI| zYiy{f$Z}+LU==x4By(z+O zgvJHe+~BW>`DyZ1 z5OTbE>D%Mo{;EaBsQKue!tDzCx@5EF-TUQiRK6To&;j^5>myM=8iUEaj~Vl)kn`lW zZjZPX0s!UFz;U@;JZc9WH4!=iOaCX&v)bqFQ2U#;N0ji#wb_iFn9J^WQh+;!e!tzf z60k+K;jtxC=bb!Kk+xPz%zI7{0+>rm_ za^e5`f%|`Pxrw`xiM_LhovqV;bz2PZEgH}#<88#Ap}dmn5O!*OeT85ze|w({Pp)+07@N`L zTKDW6NBvugN(!=+^*+!YA{VJETTS5PfI6!AOlDt0<0M7*Wgw%Zu3dyI=qgr8!b zntIY1Ux4%wi>nCauozgah(z}DnlSe-duQ&zz}byGbM{CAsEHe6u5|5zCs)Se6>X9E?#o{p4=Rv81a>PtqifboehtWnQmO9-i{-xW@+Aewi=P;b8?+rz{-NMs4ED|q|f2` zgyhsRru3QI{;%vJ>%16sUb1M9KroeU8!Sp2ZV*>2e)}nIzWCO=f?w!y{+@P(N(i^} zwZyi{Dwv|?NByFs{b&P*gwCKr zUh`w5@QOk2)j;M`#E^xAddp4*eZW~g@R4y14Wyg-G3rp1({QprwTdLRFbc5MAlM+8pV$ZEn_CR=M_X)dWqYmm2V z?tw6pbvdm~P70F50nfk3eFatPtuK`Z7#4~lh|nMN%^ z;9K?+=%djcQxn<0toI25YEo@p+^y48O@!S^rlgcmVK4?kHHRUIFTDrv69*|H8f9|_ zSz<8OhL5x$6lr`evW()Vq@kKo%AGN2H&lXCQ$=^%=q9>e&X?RepDJeOXI{@$i7F!1 z^F{{dOc~AtOH*Bw$f_wI%)e`l;J~KcgOBsYMNaZ{h1x)N3#g|JY9!zX(H1c zLC{L7B5N@;O<$tR3?RlZcLqVZ>HwC|)c!%NQ;mEKV(r%u%n0ZYNHrIXU=3B^*;(dq zG!1DzTM3=D>fOynrW#qs(QLY@F~g}A3$v3CPq}2P_fbyiN*AgCR4P%F`J)>z>CAp9 z*G2#b`@;@B{-RiMVd@H|jJz$;vx=%DPqJwdl6gK+#adcW!V8a z07h_mxY(Gx2Z|yGoceuo2(6$SQ~Qmh6H8Z?*W*b`@fnwd9bLLC!+;f%bT)Ll?kqU& zzyTHblLml%8$rMk~elLEZU4n9CnO1=31l z3|s1i7AV)7Kyp$XJ`vDM$5ms7ScJ?G#Tsa+JY@gi@}?0R$S}8IA3hzh&GGcdg+{ zF7BD<);#i>8u!l<5S-VIc%3pzeo$ReM3ptNY=tuK%t2@^CUGU;aaNu28CeaKQ_B+C z++UG~(&SjeCABjr33QsVymBG=%WJ-bhRO<`)vQ|BxQ>L`k?sZ0ZN|YF(!all%NB1y zG4;24h!I9S7A=^${=gCG-E4%lId236=BMqoFkhr&Rgu=rJls_TZcauGWGleMpV^ZE zrJiX?e#Ffs^H?0m6#{f~-O1bF%T2)2T6g$YFDWb$2?8_$HxlQd|nDxy=K z`Z5GvK?g8MSV=0v*c}~bc&p5izcw#TeXL`t58Xj438aA~AeHTMM{f1GO^GBC%bm&a zL0^_o*$5N9lFgP4SFH05p@9p-9^S)fb+ehudG-l~<6WZPLDn!nJ1IonNff9mL9vgy zpht!q09Plp`!7QwTnN^1F(P@=?>W!Kgk;QC#n8rYSfMQ`ZqzRku@+nThI$d(SM$S$ z8IMYR0BU!%^r^>wfLEvV+>RhsXCpa<%X(|0JkLlX+xc(5W5%%wMdq;xK3(ufBDtoD zW(x)fgGTjpfeLFlpBrF7e32L^w$)~D$=84Adpo~R7vlUbumsiJl5?N1xl8Wko==XA zDp=!0SVY9e(%-RqUt7p<>IKn(R$~qC&4>SvEzJo&7FfSRTryk!HcA#c|7MqA1UgW0 z-@qs*d#~JBvg0qcOfI2}awvRoLA-#muEbL`?R-EMt!k|yH7(6bTJG2;@VlH``Q2vz z3$2*C1yG^#g^goL*>^*kS3^6zDj^&=tEBw=t}#@>u==6?J=O1a^U^B+68g zMsjKM#%z5mh4}#N?I1m(uSRK<`G&WuD;!G2U7!jr;#zP?pgwUe9Y*_h$dhWvHH#uy zecekYVr}P{LoRK7ccB@d`P?7i&(ekUcy} ziB^C)aSRCVi+MCwx^VLIzEja%tDW*b`Ou!!_PR85E_F44e(^Kn_xa5}BwPBs@LKGU z#=K$?vae&dzE|cg9rj-B-5D=a90F>-11Kw1!e)6P9ZQ=_u@}aK;CHxs(O@SI^}!30 z99Ksg$|=C z0@k1;pyYjsEgo1gN8>u=%Ba$|UT}vXdaLTEjp3p5*X*~u@nQMkw%x>gapQi*?kTZhgBE8|K_cvcA!&O%?jm5SsvczH=G-gzqUd zG|5t_PDAx1B2SNT7rkSavv>o*ni*6uHhleCzVeVF?t2Og06;yw47jcXaD&vohBxap);DHelMi@t$cs3N8-S5oM(UEvK zXFuNdmHH1EHGEuw0>BQOa&FK>UA@k3G^t{Bz|#TY4uuWw*f2YB@$eGUYzCW~OV+s`b<)UWh~Li|LyomZ%NU*a^RF2v@M|Q# zD@w>+5i%z9#_JC6wrvxRkHz9iV3?A~V=~+-H~!=VtLAA#n0LbaVx1hfmZwaOFHN+0 zB8~OlzofwCsEP!n1GqxKBrjOw7qEyi-D$x&Zx}p0ODI#hz;X0waJiiB5`XD2XI?E}mS;9?-GB zLU{GGEZI!lN?bj~IkBCmqK;t8Mj`u07pQ$M8w`#pl&$lggknSFAhPswNqT8qi2Z)` zJf2a*fr?wyxbB351p+55dMpQ#eC{OZAnanaK73EiaC8$!A6bmo9Qqf)xRl!xh03f2Jal1!-_iw{T4@!}ee_xlh0Qk;RTL$*s17u*?AfK}vjb!s3&T}COREi$f zdUCN8HcUt}JZ)rftiO|vlgXggQGXwmNfb&M`|z}3w#6*Ls!~~=LrqHq7zZdj`$n56 zOoEKsdup92G*?!gCtR9F>5AZ-=i98S_Iq>Z}&{+^lbYle)>>BH>lV%fL zFUJ>K=$bAC4hbnrg`d0onk=+Q?WkYH$qV^HbY$LOJTP$Fx|ksL z$$asnGmuF^g$qN1obiY`Fvu^^0?+(Kn-9&A5TfOYNOlVZLkk}21Mg@m!IWj127;jm zZiZBJ7a^F^9Op>YC~^^;<+J*Y=sVs5d4*~!g$N-dtU_+mhX^bINr)*-Hnd4bgpmi4 zG$JJcXJ^SKCkcw`X3IG9Odnl9uZ=tMF) zZEkT2ib6bTX)$q2&`}Xzi6#9KP-~6aY)kj~_HgX+CV%WPR-q?xYs)w#Eud7-@z(>I zRjgIE(5DGCI+uL$t(6jWhGPctyaT%I+aNZrKk%bjfQP(ZcXSt%S=%FeLcQu#>@wPw zv7z8nOd#fyfti{*U|3KR*wjAoHpymI@Fw)-=)Pm5u+2Gd(L;ECLp-7^j@xsXV{sAV zTLPho&hSkNxwYDC^0)RVf|h2D4fR&RBZdf)`}xqmlys{RlU~W#tN(Z8s(~1+yLnsEuTu5g7U6@W9rG+`A7q5*_&Wiw-KL|hA zDz%$d-sU{jLJ~$3<_U}ubckU>O)~Me@fMD(mRKrBoec^kXtMVOLITB`>G8Fr@Bu#8 zJvHR<7UXsSsg=BPZjRdbn?5qTZcXI0jTw{WUO*egt-jZl{mL&Jb%^X6GVu=tnfpX0scA#=GT|CV$)evg%pyU{-oW8^)pr^QH+%9^GpW+If z%HSmed+R^|(ThQ30|=gqlnJuJpt$#IaGvAJp)f08GL;n#0T!lzF<4R>M;DTU6z7NV|;~dMy%^Rkc)MKD?$ofEYDD_r=bd9GV*rkEM=0D$QgSC!zK#Hei zt1*f+E)ONt-*#HeF^G{M4`(y!c~Ndd1_T}Kym_=+OC1U~b_%B)AkO?-@^pIt7Q=7e zEO=Y~VMyh+Gi(10ij(&bdDI3X^m`ci!SFx)J0{KWLdc=j^2^h zQjXLDrf~;4(l=vH1iQl7#rfB%x#=ZWnp^)SX@=k zszBXdBDqPXo!K?14b%=)4TFDbg7j(a(W)JybLw@I8Asygd=w1EG!3y=aOS9(%|pm@ z*=?qPC(_gZ!ffU0O`11lopo*up}@a-ZiEdL*LZSe(b*R`I^A`PDl{BRlHFjJUPXQK z3Fc-aHipl_m5FwpMst@QAD;|pee_kwuQ)C1=vgC%#z8>Krd|=NAxPs(v9MWS0VM-h z4jMfz^@ULMV>c_wi`LHGLq>G`X7u6QySY6_RI!sX()Ip%h z6iBFS3<~!uhx_I05IBn3W?YKFkM>P7W2^mrbe!JwTQK^H|lNeWR?db2?g5SIV4pET7LdaIOJEO&N8KoHS(S>r+X_` zzw>$Kv~GmQS9CG_92kB08M)~hzh2!DR(IN&*vGpR*B8^Z?SQ&ayDIWEu8LAlrKAJw zyH3|aa#1m8_+0X#1utcVO0kjdixKVca$h|aN4zT~;8+|puhv7 z2j@s5;-qEy>RY-#$%_$Hp|V0unu|9D4D?%^HvcA)wujY$4}_Qo6&9v;v^7>KTQt6R zNt8%dzx3_|Y#2o2oT!M0W4GsLt9APGI3tc>7z6)|xo73UsET*6Lt{`_iAu>ugZ1{vI{YhPV4B7wC9gL z2nzQKeyvKk)b1E$eOJ9fc+Vjy-qvyU(gHr%KKbNV#(dj}D95w3duSYlgMcRdoQKX1HZ}gZA;=F2N3TS{ll)rC!zcl+e`n78sy)6c_?k7WdnQ887c&Z)s!MC zaTM$xj}^C=W8T57y=irsl(#P$f3aP-Y@9!trXVs}gtJ4>Bk?$KGP`|JLIpf9#l^*S zJ`w#YC~Dws&vQkxAq*v`T#;T2>LWi+cRS6HYOud4IbI%Yd>?=7_v*OQ(-xZhZOz|5qUXz~ZlP;7=W^`lpWdZ_2|q_U7i6cIH2+ zVUEg1>;@Zx?}<9hD-veQk->{?6q{pj&Z(Sz0x~igtx9$F^k1Xw8CVG2&SYvsnzQ$pcOeRrax48Op5urRe<`DkbMWr+c z!) z;sTHZ;_yfh5+X7;DIEWCVyB?uru91>49kjc<#Jau_mkW+GAmlq{k3N>^riiOiUg zmf;_t1i90&>Kvx|zw_|==|#Tx?tPtxm)o-GtRvhz$&;sr;N0DI4(c&V_>{#$1HT2s zyUZ-3*=lqPL5?_tz> ze{w9E2q9RD1WwGuibz57L#Yz$_CS^B>7VV($Le)BbMS%YvWXsbpEf%1vyj%#>xDTU z{IZF{bKY;f?&q;;x{Mh~ga7mN+8FbJZajJNog=+#%y9>2RI?<>@ax$&*IgLBZ8@Lf z_oMh0ZwEYCS}uy8pes1biOCRtDt_*-1M?L3po8wt;pAp8=bV;{jfP~Yb>mgLTXI8@ zfYe+R`svVvp{8aqIz=$nKlaU~+zYcwEN+*}^|Y|IW9hQBE;K0FE^E}&AABNDS{X4G zFfg^z%If=LprPe_9fhFHoc-&DwPm)nTc>ZKzb&Dvo9T3^Zcw1Kyw(lqb5hbZa%Q_t zj!)=!Rit)rkv)1Ik7|eiCYnIs5OtRvH(i9iP`-*nL~`{Ecf7FrdsW$4`qVdO3A%mXXJu1b)HMZygAytb`S=>8C7~r9w3V^*+6%Hk_4{SoZ*{fzDbc!cn0}ah zhq)&QbCnET2{0uQI&Lj4(2PY@2yH1qVn_MNQ<{TB%_y zf{V}=b;1fL_vS;-h&AkOdt!vOk$RvigMWxvpZ&2Pi_vP~Ma73^=gmhF_ncybjuK%{cH}{EfV3AwJWkueiBJ zUCvL59*v~1hg}_?+!Gd(X*DAe@0L=lDXIs-sW`!aI8(6IP{6!{>ARwB(}~cc$oGQ5 zjJFBAmO8Pg5+@ieHhtNS?C}iljCnAHi~VWCeI@ibX&|kZ z6iiN(D+c!Ck=z!Uv$bdGiu;>y9qGA@w9H!OGgqtK1e4PDo6eSz!& zwoYX+;G(MRGRFL+h~8Mix*s5~aTR~2JDhhh=&pc~UUQao&5TOXMtDfg@NFbenS|s= zXk28VkKcMiyr+3xG+vb<>TJ@qty$YzqT~kE4_Sz5#H=u7#7RzU{Nc3M_ z;n8wmk>Po}-MXGC=PPXwg=0V51ZmEXB1s;)h%#Y*N#~6}ovSO!*TfSt@dxcszAHBv z;$e0mRWg&3U?rd`l^X?iu@$>GVLBE`zS1OX3L(aQK6)x~Z#>o=k$oL!_3CT$_7=wQ zbQJ4aD1rlmC##G!uvai@(v4G7kn8JYu&y_mXC5(rDRyd=EQV4`YWGco_}+ICk7@%w zq@B4zV#gPFQdP$!_-qEgwv)Oysq9@gm=9Nt2EUXJvL*X{>MRV4#Az-t?GCAaks5MX zRe~1T=k&eJ&4AFsDt4?l`>M-#fs-U9(>%9FTcpmyTd^;;qv1SKci8J;qGv3lVVRvc zDL8ht5XFka2Uk&MMi!$qR*;RLInwMo39_=s3*obez<|CqfQrJ*#YrWqkcJJgL4m*m z%4o_ScVc&nI-+w;O1+~JK`qZSM7HjO8ip=mf1C&E7a!~OZ&wxFoY!tN z=BWHUdA0T7uh4>huSH+u;w!>tePpvDM<~HApr{rtFbKmGR#auB1UIy9UzVFfbE+-F zb61>lUqy7Y{uH+$Yim(ZCQ<)MF6&T5CsI4L)+yvG>xR3YuF(BQ!z-eHL6>f)Ui@U^vDx?USlS*%`NPa+_% zlusfIm*M!?Wbkgk@jt~%$JRoW_@6U*=w7Gd3c}7D(3Zf)uK8yQw#b(@Z+XlictZ*n z6-dkHjltSQ}c?`16tvO!YEbo@7Q<^1YAA>quY)X)Q!Zj> zrn|uN?KnXW=aHwI7zbe$q=xG6SZtG!`swg$S8_01&4l?jfW~lXh(eTo7Jqp~tTN=* z81|Q{Fj$+SCoU3MDkZ0@-}3F#<0xYD7IW~M3ikV8lhC8 zT$Y!#3*#zA^pa7@gla84LX~sO7!tz#IhOwSi6)U6gBFY61H15FNyv@8Jx#cE7h7<4 zyVef)ymW4szhTG_W*>g4ozTX~oYieQzgh41Ketn}2OS@l4^5Ql|5$+n&Vy$$&CA~>WLM%u=KiRiFwK(_f00MQK=USHztDf(ZCSd(IJG|`DM z--ch9Rc;DEtxMh03Wh&gWI42b5Hz|bzI*GN1odPg(ThVhp{VY zYx4gZb5OXk+{1wXFm`@oj(?N1bak@%nQAc_w*PQ-zH0Ru$@5#9T~k=j_TkpN&^yRP zW2h5}+}hMp0t6?TWkt|b6C*Ev{bD8|Rg{{K%34I?ym^>(z-Xu{W+HGU*-T1(!)3q9 zQgpQ|w5U0Rvrg%O;5p>< z_nzDMHH(_o1d@D>kwgUHqlV*?m}?Zv?Gm6wBx~*m%EnqQ-0l5!+{Hv7HF{J_1w!o% z=WW=k7J;51y3+;F!2)B6sigke3RmH8SPsCiZCu$@RP^bW$7pRzwsc(Xpz?mY@c4PO zba~IJg(a##9%v04`CT3&1fB+NfV!U0M5?7;&uieZD(1M!oOroOZ79-#X@^$w}s_V^C9!`J(^#Qr+-cy0uPGY};8IwO^EWs#phd#8#v#c@Mfb~xWz>p{0nBpx>| zJH>{>cAr~?gc}f9l{1^n6H{7KD0CTYINZ(eK;8rpg$AvL5(O%=>unPt9v?M=VBfB( zBK5^XBhrz2XNw|A-5r`r*0;k93dLU( z$3FTgE^+D;44QTLj1jc02zWyaO(f*wk-ggDUnRY{3E%1a;WEQdm(xsKeF3^0@xUo6I>ZGgd5jQ$bMQV4}GuvJ^hjGoPy}w zc!3L@rN3+QJKem#YRz58=v*-9UAJg&U0@ADn_&A`9P<62Bad#ucZxhH??uPww-9&K z#b4B+>bzVd)$J9kkn&3_?a(-CFGELR5A=D*QB@a1Cs`NYe%d}=xl^|K6eiP9NJ74Q zQ)B#1#(!T*Z6TqPi7G+0Ys#ZYAGcUTOOm@|Wi0-1PcbPPr5TE)rcUh5;FIJADgz1C z)rjLZhaxs_paJJN{?^mt-R^Bo{mM%iWB@bSV)PA5u)xSFi>*wa+g~YwSeGcx-5!T~zI|hy1kakTAAOCGUiTI@SL zb{M{iu#)A|&&Qh6(Gdf}=bjM*H)cNmyaS4-V__cRc0TDK5C1Rt_Mf1`N072y1{X^x z`T}C8g6l1xwTLX1QqVi<68?3HB43{TEWZaBaBAC7Ll@YR5=&;+xKQbwyL&wfO*0QK zGDqPMQ|3*p$Y?4mxdoZKN&`-LBL`|j@XnjIS}`iBF8g?nYq;o(nupf4Xg}<^ zvlj&+4XxZ(ZsFW9hVs}@e&61~apC(<$YS@c$t71SbK(~qt$7L@l^*lou7JeLDVuW? zk^DQK#=lrmPSu!g)0r|$H%yXa<~O!Ims=tAYH?x?q&_@xhEEc-Ze|Q5C-?+xHil+s zlp3bmc3V->AE`C(&9-twRTNyt*`un5i6>@EbN>Q*=!n7;X9y2r(-LRuipj$UJ9icB z9>(}l_1$zFv~{n-S;m=o3*M*vY~ixuR>eK&)Y;h#m5vIJn(;Db7XMk z+YqLUT*?&D=9h^|S%vIOF&!2#Wq2$w^=L*x@+Zr4e5Z9#KfQ3Sj%s1=R-OaHz;=1y z(FCSH{M@utD_V^m=_V3SmQlo?(MdRPOB5tL%V?9YBr1IA3r`LGwHc*jNjP*5SpBaN zq;1xdNhc081N~dl&+Efo45_#WgqsohPQ4D8xeQ3KMi#=f&~s}Tveg(qYS8ZUFlok| z7InmU%R?!jd+mmNvuF6h*Dj9`j(Y}$z|eiBcs9%7gmi6bn={Rxf4iN^6n)1`ng*=k z!T57yJ#8Fky@l;$*$-Nm&uzgqs(qomY&a|V@WQLYY%RXPMByY=G$8kZZu6bFO|}$_ zQfk__4#b117nIG6U5ERuw(FYr)(XLIfovF-nOfK(&`PZ1W+h6-cNZk;C2ZWcs-(gc z{AoV`w*_AT7-);3)#Yq4!p6&%XE-7geDEecIPs<;C3yff;S7ApD{Tv720-HF`0Gep zMcq|Re&#Iq3u%*|6DL0Kqo_pEn}sht;(H*`I$9IhQe|_;VmGEpHK{Ijb$XSqpdge1 zQ!@;f-abB6eD99`UtOmNYQH+;Pr9xQ2LSNnJ^y>U?&@M`^AAn@Nd3oovLXC9Pe7kw=m$$pc%z7jP>tyn1#AdqP<(bSDddq3nWmV;35fyC zn@b!9O|p}damQK{f5}*AibPPVS;T$@WWdi=JaoQ-ansD+jFliZ>Si1Ds)wj1Nob7- zSdxlRB5|O2&R{07-VXp~Sf&61UDE~>N&os(E~X?6h?zGd!1<0Jik1X-k($O2lN=ZP zG@^MLZ0p+V&vS6Yy!28dcAA#wiP zdEJu-kLQi2D;Pd_d%Mu2H4m1M3+(o;+%Ei?;T2>nU;n*B=)a7)UG9LCJS>pg=8?AK zz@%Yt=()_IHZs#=M{c=>1j3l{;$w{n(!r+hz^X#A0-;u<&T@KkrL@@^wZ&-b&G4Id z*=Wd<=#G~kw`kq1WM`;%l}ADsVc|zY7LU-l&zk-(S`h*t<;gun%9h)_j+K82bPX=D z4mbp9MzfIY6wI6g2w(XcP*ZN&oTCTevb%_LK)}TU@}fgo7jH^h*XbT6R}wIG-vzAw=6;X38kvzi#iH9v#It&zFMPi+04*Xn1(5&brOP)MEL9 zFcDdksaBH3BJ;QKnMkW@pzVXYIUpxRkuJGtl7pY%7Jhh4f&pUlzE+qgt zmM2V43}$$L6?ZY{bMU&-%XpqWX+4)PDUZ|B0DjSbSR&5Rsd(IGd9S#yZk z2zDYPuR7_VaOuub$-{IEeL3wd8Hz7v_(@GC;+y!*`1(>|i=s#|$qf}P<9&U+7 zfjvHZOG}c5IZgAHC@GCygooX1--aQ+sKgN~fmGIsR%69cE+N9Fyy(5Eal?=MHrqBb z=3_^?1%R%#;$=i~Vz>x)K3<#5VqWDPiM_LicVCs84lC<(hwqx7Zsjd>e6Y7fzfNJd zpZdcT=$<_&-=CDV#precV=Q2m2?-!0K{nMOWvuo$dvUb(Ss&BE9_${vCIjZLcdtfs|Tyd>&gWvAlI>8Nl^~0U8ts( zhbd5c4%S*mvMIB7dq|1k&Nu?Egz>gW`E$=_heEqd9?g{H6fN0FdELv+;k#}yoYWx` z?)GS2yftDDSmrmq+A4_XiiaU%b zjT%uDl5ZrgrW5b|XWOyXvlV&Ht7!D-?FiNIjO)*Dq}T^8C{3!h#KW^h$U?U0XeO-w zxSX?uHkr}zpZJ`g91p48ZT#wMYwa$Z#6?`GR)P+bA)G{0l#%$-d6Y@CFgciKY(Kn| zka$DFwi#if-+fyf%%^~2Rw;ivzpYXku@Wb&w@r}>J3Gg=O*_wu)|YPCyrCe`ypDY3 z;77X#To(647K@oO!r<3o0st^Nw-ECzaWV8PU8y$Eb%AxCft=%_Gc^%v$pv15Pf4o^ zgE^)oX`yH>793SGkn{z}E>R|uI}34AhO}O+S(4_ACws>C$MsZp7d9_Xbm_yGhuAgV z=+oAt8B^Y-Y=OJ9*uu-G0Y~g(CUdy{&l`VaILZQP6p#L!jIOyZ$<-ymMEb{ za;@`kGm|O&42eF3M8*UWBzE~gD{1oBLCq&_mmB8?v5Juv50(#*s$K+L57o1hf zWL$AdXPau-$6E~uIv8EC5*HlvF*TDZm0l$V;N+*@#ovLw)0mY;=`abARHQr%b7@du z-h;HIx)zAazimfbQvZ2 zF5JLPQVP3+3Q8PsK>Ym6s&N)fj1&X?dGo=N9D(up1~PEr<4qV`aV*oAE`1%m{mdrV zyNH0~dl;uIN0bX+BVtDw4i%bFBbA&Xc{$ifFa5OS;~o}@q80ZtW!gxq1fEd?Ef|(H ztg!rtoD7cmTXb&`I?2WJVh~!e89-eJ^cgB4N4nT{>(YQo83xwewc!dbG!iw>Le&o- z^RIt$@BBR}h^|Hpv`gMG$-u&Ev(eNCZN|iB^W)$vkMl&Uljy;bOXdtp(ICG4eN??^)J3xQE$| zT?IcQ;0?|vf;g4RlTk-w(lDYU&||~7l0!d5e~{3(X}LhE5-yQKb4dy8#-IN!3gL}U5t!Ot0K;pc}X0mFO3qXTI9rS1>XrJ($J|m?P>r;RoIi-kAO{@?GIASbn2#SMzX`~i}M`3`-t*}55 zH3l8ip&Ibte3{fITms#tH$#nlINsP>l0-!+)O|bnjAsS8HqFOshQ9$!p_XIR9B#fb zIO*nV%TSguT0Z0tzIvnuu2rFqA^7B!!3p{3 zqAi{925?dL1XHhO5g#9KOG{xB>hI<{%x@#Y&6Y8{kZS7u*3%hZd)YA0d^4SrCBQ<|})=tqFXacxP z+RA7|Y7_vEWgz7K-no9U{N9ZS-5b6eb`1Bpd=s_(B}hj_Yh@vkcFvh}SSTP5hE139f#~|&kO9=qBNeAkA{{ps%2%of zyMAFy_+jC1CAg}gOr5*e(s+K<6-Hrq(TP=?l6>g4N#Z$_4I0pCv#J3 zTyC)!8j=^-PcGmfy+^fdV#EAPdnY#I%x4i&hKjx*1H9nmz?AHQRqX@m`gA;~f9@*t z&w8Pos!A|IUnHnPmy+6cS24WymT-;WF#_#*g85AKU$a?36LqXmvYm`9mCxHu)~O3K zQ6GXbHOCaglBfLL{i=Z$q&xW)2=?z$;v+UVqgM;Lr0y$KBQ5lXl>B!OTS3pCwj4j% z{6A5ubX4|i#=O9M7EI0HpSM#l?@wDW?p;8O&w;f=*NlaTI6KCHyzWral#B)|8uy~# zKCYjr=#Z+p*KkigTYvh?!F_}75StgTpTK1+7hY`Ol}`|B2lg1nSxS20BzkQGF@)DE zuX>ZP7Q0??6K?<+n?cNZbiGqVcL=)7x*v3dMLoc@JMwN?n6P~k2ctJt8V$-O9%jKj zf`}5VKP=`jz(%s2AiI|=Xw-tFD9XgqxQ7n$H2P<(04ax&TJzITKS9K4Y0WAq|0opE zJwZJ4!O-y+Rz|@Q#OwR@bhV$uAS~|=;HZqbqWD==EO0FinLoMk@1DnD?c`6dE>JMd z&7_G%kw^E=kOT}6Jx=HbarmuYvqddb9#ET9sSOI(s)TvZ48x9LIeqEXS(_;pzxPp^x_`$$lC8zB2 zz;Xc2b)Z7xnrO%Wtchit_U8-tx%!8Jr?@Y+e2a6ol8?u?4+zrx2{s4*e3K7oi|KXu zVipK&iP;0%8z>)|;VCZ8qs3GH;k*ei1Q5jJA-`6+tP zxdANS=CxotsbO|$=!3dDFj^1@j2;A$*M^7;26&x;4dNjS0kq;^n{G{qAg3DwM7~Qc zI3*1x9FX(tzxob-65XGZt$*JC>xJ<1>3^51T}+)_oaz5p9QX6pzuo;4%KeYK|J!NU z|Mv&~mB#X)!k1y95b&GHWamnq%MNxw;&Z;(V}C1of_^9r7Pn3+4P}(X@{7O|JJg;&1q1 ze90I+esjH;^wg;wI*703z&?#dkGfHfzfG{nvFALv{I_OW#CNQx$R7&|{vRx1`p1$7 z2uR3T_C;nD1{MYt2zgnkY58e6T15zoar!a&=^4daiAib7X^Ba>qa&y3nF(oGy0OWb zCM5`333@3RKqw(hh?*{)zKXHm4z;z$PEJ;W5kI>6qE-QJq52i3xfKN!38tCeW>O{< zo`sGbJpr`z>W+SPRz6}>v&BTzpa^9o#LVV(jsF>R(qzd>fVihZvgYqtVCYdo5BMMA)No@uAWWTD8;*oqHJroD8 zLtyb1;Gt|x&#|?UPqQklRb-Vcj7&RyL>TfJRlF+%l3J`}eMBAM_dxYOg*JNxIRkz= z5}#tr6u8KZLjSM1`kxaojH#^({pYavT$ z;l^D{7J(F|os^AG#9|tch^+3zd7*vfr3+B&qRwChvxmEx4ckWaV7c@293kYfiBR&t;;unZ5BO@Ff(&?Vbai~`k&o~Ey~`=E||E<%w8#Al}lcE9XCOIpuzj!wf)E zmfGpL+k>_5R}T@nJnBYCsdbNA?J-v)#=0UHNTPvp0KSP9B#6~hxjlnB26hIqKISiU zT0eESj_H73WWRM=rSIVz&a@@H6A;|K3gu^p>$Cu^!5G&b;&hOk|hsF<9vYT>b ziFABMDKFfb++$-Ga&4a9%@_Y)UuPW^$C|Wp+}+(FxCD2CySux)yE_DT*AQHSy9alI z`{04#E+2RA?&e;;-R-|-&guHi`_4JtJyTWBQ@gm@l{V?}k9p;KzEn`|{qya5MY!i^ z_e+A3>^>%wxI>pWO$XPk$*h+RAAZ7PNFA-T>D?Y4OCT}Uu7QAHmYVsjjsp$RSuH}g zfK`#E-D-MwicWQ=_PE*ydk_~EFiwRlJUJNknL2HRjg`5>C_cKt*b18*RfasX`tjpJ zAo|CNXNu-c10@m#iv_2p*N$$ed(KhgzP{`2z+cx3b%g~2ieLf)`cFTC{=Av}V~pg9 zrcLZlBl26>pvRqGUDo&nHxgon>mX-x0cXh_v5_GxkIK1Tc%^@&Kv40RHS* zT}e>~&2z(XQ^{aNY1Fcz)~qke+N-9}3mH@=9F?l0J#GG9N^@}4r;s7V23?kbcKJm6>dbF%9t<9|zv>Id!3o{OvOW^P8pfDGQ8+Vy#_d=NAf@}iQWd}Gj>7e?D(US!+(aBFV0C28$kjeKvWyqaK zs_FsXq!)mwivqJ%p@B#;;(nY2!+oH;ICA}%kHud#NC@a0|64%k)PuE zar$`6*_}VIgdN~|1$*zn;dz1#an?=plPZ~#8)^ud3*&wwxORE_tG2f)73_UhSWTgE z{4_Wz?D>!}2re;IB2AUN2aIr3R$6uQmt~$`Hs9Gr_p;)LLFJ%vaC-&&kjOuA<(O}d zPSL{G&Gs1MY`z3;nR8*qLMY7~$4iwQ@^XPn5wxI1bm=eK{6cNuo#`}pixYdS(@6$R z#$!df^Z`BLpZd~ zg<&OoWSus>7?^&tGHN#K@UD@+*~f()sJC=1ZHL1^`qT1^i`}d<47Uqx?Cp7}9QyI9 z`e@S?Y5oW4*WRno9ub(7pH~ARharJ_6!R@8JH^fMNX+{6wS9OEsAY-M2&BLcN5eQh zK~3g%;udp+F#@(dE3FWi?};o6+YGP)A@KP~7!M{bNgNbz`M^X^-k70quUV0(9Q=hbb z0dh;KIu4TXkHZRs^#rhwqcVAs`>d=XucizJU<(X8XZ*r?}fI4|_x^q?Ijx6C}&u)o#U3IP5z z;k*E22^tHC<&G;xQOwnfKB+clwCDC3J9$i1fQdol9WF7AA=-XYw!&kNc;=|C`Q_g$DuM3PgzK|KD% z#!tw$jGlRIpkkt`Kz(K+kjx0}glnO#Dnm;3;K# zkGUxcj-p_I(lUVy~uFdEO=5Pp>hUZRomWtX7h;l!kToB z8qXn3i9n5_XE6}j6YiM(jsO=>~>Z@ zhR}O>?s3P7J{1dk^+uMe$k_bmss`&_8hoUk0iUc3Jy^j$@A2Bypk!0Yw3LG3++g%6 zRWBBNtt3BOz03N-d(Pi_MUeWH6MhiJZ_862goUa$tkh^;?gsbya%5o%+MrJ>$eA*p7fQg#^-Ve!X*ovYlZlbJ; zkkiEN63S3nw*k2ZvZ7X>@;#Oke-uEMXKDz`q0d|AIrz0j#NjpK)4}en#;u&5+|}?j zzdR>Cysy&mRfE3XqSx)ialm@;Q^Xf8B_Akc$TC&BQeFt)J+_BQavdfP>>cALW)#zh zLRYxnKoHIF6qOp>dBw-xI5*~TA$<+Vp;q^6Yae<0?&LmlnjHw;-1JBs%@?gq##jvK zZ~Z6b)3^(RE4nWS8(k+GT?AmyZI8p5^?n+ouxG)>TfGoXZRoX1z`Kdy{FH0rWO+1Cw`2zX7e`LYS0E1@ z7Fudzr^ojF2dTN(pK(RGk-_+Z0ox5zYUf$a&mS$yFquz=(C!PK$9&hpy%PxuB&Tyf z6Paw({48O~3bsNgiw;6SzPeC}R2L0!F=wvB6&(vBz}h&eCVDW=BF74b{DQoAl!~H2 zus|wWX=F|^<2%WS95G-+qs+^wVzP!$`=O7*3edKTmD;3ivyl&00wf=*w9&>l3o3Y&Bh8PEjmh8JtXM4!~i7~iYOl2WYs*&s4r;havBKRUS;{Vvf8DcMBe7LR-PMy(3_ zk|&mP0aB?et=0sJ!`~`8@W?LE*n}cB zmg*@aTmeTK%J6Nj5>&plxefKYH8+x?QX+IPg%62yoPv^2c<6ATet z5s6s$BNi;IpkPIK;^s%uG#GB`#4(|}pQZ!?A}NVl!&M*xqst zlTt~YGaAg*71Eal3x_vBr@+J}KPC6f;mq4Jp=7`cvCjhS`*}#5Idegh!_06F$Ed@c z%0kdTTKOxKW;=eSMIN+7mibu-LMM+rs{%@moVwWndH_z~Z3)?t*Hh)y#}HIbtlh-G zu<~qBjoZD6!WHzQVYi5TW#NKw@&fODd_9Tt<{c>+cpKuns6qv6X}SFk=v={ZLA!|! ze2@g4i78t{no z@&-TO1A6^VLBB`b)=2^DNQ27nIiaVZXKM&+yh5Iqok7s8*HcRvw_ltt*8=>mEbcv0 zXD>96eI5|JV1E>kej&eT`pkHHJ54)sZMpC`zsgYVC{z6O+NmsEgcO<(%LJD{55fSa zW70cS?%eW(thf!snvy=&hk6mHdAs^!{mFw5Iv$v^<3Vs^d4H`qEPXj%`2F z>U@xdQe`>;jHsC4sXaMCg0UDC=d5WU&3wP{xLyU8fb`-zEc45LJ8!MYj{n!=ii)iG zPAvR(2s1J*mvQzOE+&Uz7M3A;h=ZVD%d4pvt%hkVE?;u~ZTg2`iMsef9t5?=Pnq2qMMERo|Yt?B{XKWiMp%F29?meur)OvRU0L=d2C<(^H5)6g}Pl*$A!(wsT z!N3+PGKRzf>9Xk!UDzx3GOE^6fk-pzVeC5m+Lk+* z9Dm&ULFf|)UPn19!cl1jmEnLvSlttqT5#ujfZ zC{nO6k1lk?U?Px>>75}tO3K%LPzUQ>6Hyd9Zpj5|0g_$5k;!5i54^D2u>t^2p|QV^ z%kvsK4MA1nlf2#|-_Nh2@F!?3@G-)hR&V2&`|oK<0s{guOme~`g^@+#{3SUcavfpZ zHk;98gu+PO7_s5CXHF7Q6j);-f+2575I}JYR=9Y{xGEE8_blK&vKnQbw5vYs_-*iv z2r4A~*arx_UN@Oboze4OrFAW&ktKZBKa&1h?$G5X!p}K}EzO5OaplS5FihB5H2smf z1i?Thp14u;xIQIOGSLEO)6rT_+Y~EwpBX3&`-t~x{!8vTXMUW%h+rvs3xD;Y!_#IV zuMsrpk0!SPgNQ>J_+N_xb;WTo0)&wY*_$)eu5nBx1Y@+UU3!RtAC{X&1PpU3cKoAz z{eKM`ovxMO$}Bihy^K21^m})$`J(aT$^VKMHZW$j0tZp2E`rKy7s#k5Iz~_~@^@N@ z%sNA52FCVJ#lGXIasT|pE9Ba2j|kkj?O@1CyuWG+a1#+PeR^t4L;=*(D72bSnjcHX@;kYEDc?1E>%c^|1^sl^t7)eEi&{%#rxX!&%rFFc4Ba3}89edox*u~ER4-4Ss(7i#xBnE8f*${~SWvasu8^xM zF{y^etKp7Pu0+oFa8y(z(aa+tDzscyXNA`6vXV(>@FiGSwWcX+4RWW<0|qnCx~00r zu>OkqNEBdMB;&KojC<8awels;V99T7tQ|UZOuxGA2@6EvQ1^UyC16^Y2D59gjAWqg z!7rcb>KXZz0qD6ZC#*g0$v;Gw+jMO(*LM*vnMzmE=fHj1%5E#JA`4fJA6mcHSn22H zJ5M3{=})EF<8pC-lcgt$Qm1hCqZZKb?yp%3KK~gW(utmTQw~HW{JPju8?0IGC30L- z*EeIZ%+2EbA)-ND&D0#J_O;K|F~w8TG@+jLZPYDTB((G4F$csXaLFe{0Hh7+CCZtY z&jpc}UB7P9Q+;SCM25~U<|W#eBP3Rx=Uo2l; zF>Y~rwr;owZHJ{tj=Tl-shXIt%%`shXtNl3Y8@X>^QPwB{)DrsNArk-g(KtvyhP#9 zcyPXs>^(N)$r8h!U7;Ln6Am#me1%Ds^(;^*)&2%4-;GJGoY!D0UpbXx_V~?YqH@6TEy(thRrU>X%jv1<66|+%`L;$MFR;nw3Jx;n$8FV+L!s4sI}l+)euU* zkV&cS$|?jX3zbrg0dO{|m$FAL!JghvejpQh;zx(AL-jyc=#IRe( zDN1#QH~*^fXT)?Ns$_S<^Bj*jm#f;acyLth$9U>yVC;=ccLdn$ld+uL{B+o)F4Z`~ zsEe^~o6T3yOY+w${2f&7`D>&aWt;4om#QWsz>G`vtSlq+Fj|JdLHcYPiA$8L12EHV zh0lc=S95k?Tgp&{^Kp_67w^pTo|0P&__{d9-brSMPLSDd%=F9zIP9XI*>H@+i?YX> za?RQ79(%H3*)}<%;F?2PDqdEXEFO77daNs+ztFd6!S>jbZ4?*otHu2KW+)b)=;z_B z?UA~iT~rKcFY1IAs5?-IktNZbhAxGAJT1p{5Rhr;47GKT_HmJhoZgQ`cc}arZtSb|{lTiQD_vD4Bwu^a!xbe5w!LuF z`l`$DU;t6@aO6f$wyVTxf>!}Q;XA$L^)^sW+7%bzq$RkReJ9~EO~}8uz!U~~7cunA z;l|V7E82r9;X5Y7%(DuLr3MvLr&);af0f1`BXzl!CrRP!Sg4+z$z&5hJ8h0Vb=aMH znYU+jl(OPM?y1GG+mkLjgvVR&c%vkPe{d@SPcWmlh#Aj|IuFnnxjsz zR7Jo)9Z#9l5;8U*6J~O@5b)Ue=w__vG>|E1-4Nr9!v$!29I8&nT%H{13LACA7aFMW z?TP26I*Pd2VeG>DLR>+tH~O?v4Z8NDCX#)mECAP#iqp5W0(Rc2Ty{Prpo-g75_X*7^tlfBJH7Xr?t7TWA?0{bPh5{=&=VSQ&a$Ev* zT0Q~k^RFTHHl-u~ym6>|o!EpD56m_pLm))OP+?F|XQt!c9)FU-yS}QZC+z%<)LR`C z_0Q0^)rYNB5jny6hvN+@dYf3Q`MPHJZW}#A*8xFZXfvExC@@ufFKNvguA=Rn1BJDS z)}vLxfS6Uy&z8%Wt77y*pc5wklxN?eSa=urU%} zJ8yovXJcHp+{oOmGIR26p3`QPhtHbzMm;^(<^@icPUEB&d*vuw*8^C_iQQ(d>fR5$E+}#?%pUZGF(DtGexHdUe3>n%0Avs0xm{uZumdn=WS+r% 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 Date: Fri, 21 Jul 2023 11:42:54 +0300 Subject: [PATCH 03/27] Delete discord_richpresence.py --- plugins/utilities/discord_richpresence.py | 862 ---------------------- 1 file changed, 862 deletions(-) delete mode 100644 plugins/utilities/discord_richpresence.py diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py deleted file mode 100644 index 3ebccb02..00000000 --- a/plugins/utilities/discord_richpresence.py +++ /dev/null @@ -1,862 +0,0 @@ -# Released under the MIT and Apache License. See LICENSE for details. -# -"""placeholder :clown:""" - -# ba_meta require api 8 -#!"Made to you by @brostos & @Dliwk" - - -from __future__ import annotations -from urllib.request import Request, urlopen, urlretrieve -from pathlib import Path -from os import getcwd, remove -from zipfile import ZipFile -from bauiv1lib.popup import PopupWindow -from babase._mgen.enums import TimeType - -import asyncio -import http.client -import ast -import uuid -import json -import time -import threading -import shutil -import babase -import _babase -import bascenev1 as bs -import bascenev1lib -import bauiv1 as bui - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Tuple - - -ANDROID = babase.app.classic.platform == "android" -DIRPATH = Path(f"{_babase.app.python_directory_user}/image_id.json") - -if ANDROID: # !can add ios in future - - # Installing websocket - def get_module(): - install_path = Path(f"{getcwd()}/ba_data/python") # For the guys like me on windows - path = Path(f"{install_path}/websocket.zip") - file_path = Path(f"{install_path}/websocket") - if not file_path.exists(): - url = "https://github.com/brostosjoined/bombsquadrpc/releases/download/presence-1.0/websocket.zip" - try: - filename, headers = urlretrieve(url, filename=path) - with ZipFile(filename) as f: - f.extractall(install_path) - remove(path) - except: - pass - get_module() - - import websocket - - heartbeat_interval = int(41250) - resume_gateway_url: str | None = None - session_id: str | None = None - - start_time = time.time() - - class PresenceUpdate: - def __init__(self): - self.state: str | None = "In Game" - self.details: str | None = "Main Menu" - self.start_timestamp = time.time() - self.large_image_key: str | None = "bombsquadicon" - self.large_image_text: str | None = "BombSquad Icon" - self.small_image_key: str | None = None - self.small_image_text: str | None = ( - f"{_babase.app.classic.platform.capitalize()}({_babase.app.version})") - self.media_proxy = "mp:/app-assets/963434684669382696/{}.png" - self.identify: bool = False - self.party_id: str = str(uuid.uuid4()) - self.party_size = 1 - self.party_max = 8 - - def presence(self): - with open(DIRPATH, "r") as maptxt: - largetxt = json.load(maptxt)[self.large_image_key] - with open(DIRPATH, "r") as maptxt: - smalltxt = json.load(maptxt)[self.small_image_key] - - presencepayload = { - "op": 3, - "d": { - "since": None, # used to show how long the user went idle will add afk to work with this and then set the status to idle - "status": "online", - "afk": "false", - "activities": [ - { - "name": "BombSquad", - "type": 0, - "application_id": "963434684669382696", - "state": self.state, - "details": self.details, - "timestamps": { - "start": start_time - }, - "party": { - "id": self.party_id, - "size": [self.party_size, self.party_max] - }, - "assets": { - "large_image": self.media_proxy.format(largetxt), - "large_text": self.large_image_text, - "small_image": self.media_proxy.format(smalltxt), - "small_text": self.small_image_text, - }, - "client_info": { - "version": 0, - "os": "android", - "client": "mobile", - }, - "buttons": ["Discord Server", "Download BombSquad"], - "metadata": { - "button_urls": [ - "https://discord.gg/bombsquad-ballistica-official-1001896771347304639", - "https://bombsquad-community.web.app/download", - ] - }, - } - ], - }, - } - ws.send(json.dumps(presencepayload)) - - def on_message(ws, message): - global heartbeat_interval, resume_gateway_url, session_id - message = json.loads(message) - try: - heartbeat_interval = message["d"]["heartbeat_interval"] - except: - pass - try: - resume_gateway_url = message["d"]["resume_gateway_url"] - session_id = message["d"]["session_id"] - except: - pass - - def on_error(ws, error): - babase.print_exception(error) - - def on_close(ws, close_status_code, close_msg): - # print("### closed ###") - pass - - def on_open(ws): - print("Connected to Discord Websocket") - - def heartbeats(): - """Sending heartbeats to keep the connection alive""" - global heartbeat_interval - if babase.do_once(): - heartbeat_payload = { - "op": 1, - "d": 251, - } # step two keeping connection alive by sending heart beats and receiving opcode 11 - ws.send(json.dumps(heartbeat_payload)) - - def identify(): - """Identifying to the gateway and enable by using user token and the intents we will be using e.g 256->For Presence""" - with open(f"{getcwd()}/token.txt", 'r') as f: - token = bytes.fromhex(f.read()).decode('utf-8') - identify_payload = { - "op": 2, - "d": { - "token": token, - "properties": { - "os": "linux", - "browser": "Discord Android", - "device": "android", - }, - "intents": 256, - }, - } # step 3 send an identify - ws.send(json.dumps(identify_payload)) - identify() - while True: - heartbeat_payload = {"op": 1, "d": heartbeat_interval} - ws.send(json.dumps(heartbeat_payload)) - time.sleep(heartbeat_interval / 1000) - - threading.Thread(target=heartbeats, daemon=True, name="heartbeat").start() - - # websocket.enableTrace(True) - ws = websocket.WebSocketApp( - "wss://gateway.discord.gg/?encoding=json&v=10", - on_open=on_open, - on_message=on_message, - on_error=on_error, - on_close=on_close, - ) - if Path(f"{getcwd()}/token.txt").exists(): - threading.Thread(target=ws.run_forever, daemon=True, name="websocket").start() - - -if not ANDROID: - # installing pypresence - def get_module(): - install_path = Path(f"{getcwd()}/ba_data/python") - path = Path(f"{install_path}/pypresence.zip") - file_path = Path(f"{install_path}/pypresence") - if not file_path.exists(): - url = "https://github.com/brostosjoined/bombsquadrpc/releases/download/presence-1.0/pypresence.zip" - try: - filename, headers = urlretrieve(url, filename=path) - with ZipFile(filename) as f: - f.extractall(install_path) - remove(path) - except: - pass - get_module() - - # Updating pypresence - try: - from pypresence import PipeClosed, DiscordError, DiscordNotFound - except ImportError: - shutil.rmtree(Path(f"{getcwd()}/ba_data/python/pypresence")) - get_module() - - from pypresence.utils import get_event_loop - import pypresence - import socket - - DEBUG = True - - _last_server_addr = 'localhost' - _last_server_port = 43210 - - def print_error(err: str, include_exception: bool = False) -> None: - if DEBUG: - if include_exception: - babase.print_exception(err) - else: - babase.print_error(err) - else: - print(f"ERROR in discordrp.py: {err}") - - def log(msg: str) -> None: - if DEBUG: - print(f"LOG in discordrp.py: {msg}") - - def _run_overrides() -> None: - old_init = bs.Activity.__init__ - - def new_init(self, *args: Any, **kwargs: Any) -> None: # type: ignore - old_init(self, *args, **kwargs) - self._discordrp_start_time = time.mktime(time.localtime()) - - bs.Activity.__init__ = new_init # type: ignore - - old_connect = bs.connect_to_party - - def new_connect(*args, **kwargs) -> None: # type: ignore - global _last_server_addr - global _last_server_port - old_connect(*args, **kwargs) - c = kwargs.get("address") or args[0] - _last_server_port = kwargs.get("port") or args[1] - - bs.connect_to_party = new_connect - - start_time = time.time() - - class RpcThread(threading.Thread): - def __init__(self): - super().__init__(name="RpcThread") - self.rpc = pypresence.Presence(963434684669382696) - self.state: str | None = "In Game" - self.details: str | None = "Main Menu" - self.start_timestamp = time.mktime(time.localtime()) - self.large_image_key: str | None = "bombsquadicon" - self.large_image_text: str | None = "BombSquad Icon" - self.small_image_key: str | None = None - self.small_image_text: str | None = None - self.party_id: str = str(uuid.uuid4()) - self.party_size = 1 - self.party_max = 8 - self.join_secret: str | None = None - self._last_update_time: float = 0 - self._last_secret_update_time: float = 0 - self._last_connect_time: float = 0 - self.should_close = False - - @staticmethod - def is_discord_running(): - for i in range(6463, 6473): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(0.01) - try: - conn = s.connect_ex(('localhost', i)) - s.close() - if (conn == 0): - s.close() - return (True) - except: - s.close() - return (False) - - def _generate_join_secret(self): - # resp = requests.get('https://legacy.ballistica.net/bsAccessCheck').text - connection_info = bs.get_connection_to_host_info() - if connection_info: - addr = _last_server_addr - port = _last_server_port - else: - try: - with urlopen( - "https://legacy.ballistica.net/bsAccessCheck" - ) as resp: - resp = resp.read().decode() - resp = ast.literal_eval(resp) - addr = resp["address"] - port = 43210 - secret_dict = { - "format_version": 1, - "hostname": addr, - "port": port, - } - self.join_secret = json.dumps(secret_dict) - except: - pass - - def _update_secret(self): - threading.Thread(target=self._generate_join_secret, daemon=True).start() - self._last_secret_update_time = time.time() - - def run(self) -> None: - asyncio.set_event_loop(get_event_loop()) - while not self.should_close: - if time.time() - self._last_update_time > 0.1: - self._do_update_presence() - if time.time() - self._last_secret_update_time > 15: - self._update_secret() - # if time.time() - self._last_connect_time > 120 and is_discord_running(): #!Eric please add module manager(pip) - # self._reconnect() - time.sleep(0.03) - - def _subscribe(self, event: str, **args): - self.rpc.send_data( - 1, - { - "nonce": f"{time.time():.20f}", - "cmd": "SUBSCRIBE", - "evt": event, - "args": args, - }, - ) - data = self.rpc.loop.run_until_complete(self.rpc.read_output()) - self.handle_event(data) - - def _subscribe_events(self): - self._subscribe("ACTIVITY_JOIN") - self._subscribe("ACTIVITY_JOIN_REQUEST") - - # def _update_presence(self) -> None: - # self._last_update_time = time.time() - # try: - # self._do_update_presence() - # except (AttributeError, AssertionError): - # try: - # self._reconnect() - # except Exception: - # print_error("failed to update presence", include_exception= True) - - def _reconnect(self) -> None: - self.rpc.connect() - self._subscribe_events() - self._do_update_presence() - self._last_connect_time = time.time() - - def _do_update_presence(self) -> None: - if RpcThread.is_discord_running(): - self._last_update_time = time.time() - try: - data = self.rpc.update( - state=self.state or " ", - details=self.details, - start=start_time, - large_image=self.large_image_key, - large_text=self.large_image_text, - small_image=self.small_image_key, - small_text=self.small_image_text, - party_id=self.party_id, - party_size=[self.party_size, self.party_max], - join=self.join_secret, - # buttons = [ #!cant use buttons together with join - # { - # "label": "Discord Server", - # "url": "https://ballistica.net/discord" - # }, - # { - # "label": "Download Bombsquad", - # "url": "https://bombsquad.ga/download"} - # ] - ) - - self.handle_event(data) - except (PipeClosed, DiscordError, AssertionError, AttributeError): - try: - self._reconnect() - except (DiscordNotFound, DiscordError): - pass - - def handle_event(self, data): - evt = data["evt"] - if evt is None: - return - - data = data.get("data", {}) - - if evt == "ACTIVITY_JOIN": - secret = data.get("secret") - try: - server = json.loads(secret) - format_version = server["format_version"] - except Exception: - babase.print_exception("discordrp: unknown activity join format") - else: - try: - if format_version == 1: - hostname = server["hostname"] - port = server["port"] - self._connect_to_party(hostname, port) - except Exception: - babase.print_exception( - f"discordrp: incorrect activity join data, {format_version=}" - ) - - elif evt == "ACTIVITY_JOIN_REQUEST": - user = data.get("user", {}) - uid = user.get("id") - username = user.get("username") - discriminator = user.get("discriminator", None) - avatar = user.get("avatar") - self.on_join_request(username, uid, discriminator, avatar) - - def _connect_to_party(self, hostname, port) -> None: - babase.pushcall( - babase.Call(bs.connect_to_party, hostname, port), from_other_thread=True - ) - - def on_join_request(self, username, uid, discriminator, avatar) -> None: - del uid # unused - del avatar # unused - babase.pushcall( - babase.Call( - bui.screenmessage, - "Discord: {} wants to join!".format(username), - color=(0.0, 1.0, 0.0), - ), - from_other_thread=True, - ) - babase.pushcall(lambda: bui.getsound('bellMed').play(), from_other_thread=True) - - -class Discordlogin(PopupWindow): - - def __init__(self): - # pylint: disable=too-many-locals - _uiscale = bui.app.ui_v1.uiscale - self._transitioning_out = False - s = 1.25 if _uiscale is babase.UIScale.SMALL else 1.27 if _uiscale is babase.UIScale.MEDIUM else 1.3 - self._width = 380 * s - self._height = 150 + 150 * s - self.path = Path(f"{getcwd()}/token.txt") - bg_color = (0.5, 0.4, 0.6) - log_btn_colour = (0.10, 0.95, 0.10) if not self.path.exists() else (1.00, 0.15, 0.15) - log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" - - # creates our _root_widget - PopupWindow.__init__(self, - position=(0.0, 0.0), - size=(self._width, self._height), - scale=(2.1 if _uiscale is babase.UIScale.SMALL else 1.5 - if _uiscale is babase.UIScale.MEDIUM else 1.0), - bg_color=bg_color) - - self._cancel_button = bui.buttonwidget( - parent=self.root_widget, - position=(25, self._height - 40), - size=(50, 50), - scale=0.58, - label='', - color=bg_color, - on_activate_call=self._on_cancel_press, - autoselect=True, - icon=bui.gettexture('crossOut'), - iconscale=1.2) - - bui.imagewidget(parent=self.root_widget, - position=(180, self._height - 55), - size=(32 * s, 32 * s), - texture=bui.gettexture("discordLogo"), - color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) - - self.email_widget = bui.textwidget(parent=self.root_widget, - text="Email/Phone Number", - size=(400, 70), - position=(50, 180), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) - - self.password_widget = bui.textwidget(parent=self.root_widget, - text="Password", - size=(400, 70), - position=(50, 120), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) - - bui.containerwidget(edit=self.root_widget, - cancel_button=self._cancel_button) - - bui.textwidget( - parent=self.root_widget, - position=(265, self._height - 37), - size=(0, 0), - h_align='center', - v_align='center', - scale=1.0, - text="Discord", - maxwidth=200, - color=(0.80, 0.80, 0.80)) - - bui.textwidget( - parent=self.root_widget, - position=(265, self._height - 78), - size=(0, 0), - h_align='center', - v_align='center', - scale=1.0, - text="💀Use at your own risk💀\n ⚠️discord account might get terminated⚠️", - maxwidth=200, - color=(1.00, 0.15, 0.15)) - - self._login_button = bui.buttonwidget( - parent=self.root_widget, - position=(120, 65), - size=(400, 80), - scale=0.58, - label=log_txt, - color=log_btn_colour, - on_activate_call=self.login, - autoselect=True) - - def _on_cancel_press(self) -> None: - self._transition_out() - - def _transition_out(self) -> None: - if not self._transitioning_out: - self._transitioning_out = True - bui.containerwidget(edit=self.root_widget, transition='out_scale') - - def on_bascenev1libup_cancel(self) -> None: - bui.getsound('swish').play() - self._transition_out() - - def login(self): - if not self.path.exists(): - json_data = { - 'login': bui.textwidget(query=self.email_widget), - 'password': bui.textwidget(query=self.password_widget), - 'undelete': False, - 'captcha_key': None, - 'login_source': None, - 'gift_code_sku_id': None, - } - headers = { - 'user-agent': "Mozilla/5.0", - 'content-type': "application/json", - } - - conn = http.client.HTTPSConnection("discord.com") - - payload = json.dumps(json_data) - # conn.request("POST", "/api/v9/auth/login", payload, headers) - # res = conn.getresponse().read() - - try: - conn.request("POST", "/api/v9/auth/login", payload, headers) - res = conn.getresponse().read() - token = json.loads(res)['token'].encode().hex().encode() - with open(self.path, 'wb') as f: - f.write(token) - bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) - bui.getsound('shieldUp').play() - self.on_bascenev1libup_cancel() - except: - bui.screenmessage("Incorrect credentials", (1.00, 0.15, 0.15)) - bui.getsound('error').play() - - conn.close() - else: - remove(self.path) - bui.getsound('shieldDown').play() - bui.screenmessage("Account successfully removed!!", (0.10, 0.10, 1.00)) - self.on_bascenev1libup_cancel() - ws.close() - - -run_once = False - - -def get_once_asset(): - global run_once - if run_once: - return - response = Request( - "https://discordapp.com/api/oauth2/applications/963434684669382696/assets", - headers={"User-Agent": "Mozilla/5.0"}, - ) - try: - with urlopen(response) as assets: - assets = json.loads(assets.read()) - asset = [] - asset_id = [] - for x in assets: - dem = x["name"] - don = x["id"] - asset_id.append(don) - asset.append(dem) - asset_id_dict = dict(zip(asset, asset_id)) - - with open(DIRPATH, "w") as imagesets: - jsonfile = json.dumps(asset_id_dict) - json.dump(asset_id_dict, imagesets, indent=4) - except: - pass - run_once = True - - -def get_class(): - if ANDROID: - return PresenceUpdate() - elif not ANDROID: - return RpcThread() - - -# ba_meta export babase.Plugin -class DiscordRP(babase.Plugin): - def __init__(self) -> None: - self.update_timer: bs.Timer | None = None - self.rpc_thread = get_class() - self._last_server_info: str | None = None - - if not ANDROID: - _run_overrides() - get_once_asset() - - def on_app_running(self) -> None: - if not ANDROID: - self.rpc_thread.start() - self.update_timer = bs.AppTimer( - 1, bs.WeakCall(self.update_status), repeat=True - ) - if ANDROID: - self.update_timer = bs.AppTimer( - 4, bs.WeakCall(self.update_status), repeat=True - ) - - def has_settings_ui(self): - return True - - def show_settings_ui(self, button): - if not ANDROID: - bui.screenmessage("Nothing here achievement!!!", (0.26, 0.65, 0.94)) - bui.getsound('achievement').play() - if ANDROID: - Discordlogin() - - def on_app_shutdown(self) -> None: - if not ANDROID and self.rpc_thread.is_discord_running(): - self.rpc_thread.rpc.close() - self.rpc_thread.should_close = True - else: - # stupid code - ws.close() - - def _get_current_activity_name(self) -> str | None: - act = bs.get_foreground_host_activity() - if isinstance(act, bs.GameActivity): - return act.name - - this = "Lobby" - name: str | None = ( - act.__class__.__name__.replace("Activity", "") - .replace("ScoreScreen", "Ranking") - .replace("Coop", "") - .replace("MultiTeam", "") - .replace("Victory", "") - .replace("EndSession", "") - .replace("Transition", "") - .replace("Draw", "") - .replace("FreeForAll", "") - .replace("Join", this) - .replace("Team", "") - .replace("Series", "") - .replace("CustomSession", "Custom Session(mod)") - ) - - if name == "MainMenu": - name = "Main Menu" - if name == this: - self.rpc_thread.large_image_key = "lobby" - self.rpc_thread.large_image_text = "Bombing up" - #self.rpc_thread.small_image_key = "lobbysmall" - if name == "Ranking": - self.rpc_thread.large_image_key = "ranking" - self.rpc_thread.large_image_text = "Viewing Results" - return name - - def _get_current_map_name(self) -> Tuple[str | None, str | None]: - act = bs.get_foreground_host_activity() - if isinstance(act, bs.GameActivity): - texname = act.map.get_preview_texture_name() - if texname: - return act.map.name, texname.lower().removesuffix("preview") - return None, None - - def update_status(self) -> None: - roster = bs.get_game_roster() - connection_info = bs.get_connection_to_host_info() - - self.rpc_thread.large_image_key = "bombsquadicon" - self.rpc_thread.large_image_text = "BombSquad" - self.rpc_thread.small_image_key = _babase.app.classic.platform - self.rpc_thread.small_image_text = ( - f"{_babase.app.classic.platform.capitalize()}({_babase.app.version})" - ) - connection_info = bs.get_connection_to_host_info() - if not ANDROID: - svinfo = str(connection_info) - if self._last_server_info != svinfo: - self._last_server_info = svinfo - self.rpc_thread.party_id = str(uuid.uuid4()) - self.rpc_thread._update_secret() - if connection_info != {}: - servername = connection_info["name"] - self.rpc_thread.details = "Online" - self.rpc_thread.party_size = max( - 1, sum(len(client["players"]) for client in roster) - ) - self.rpc_thread.party_max = max(8, self.rpc_thread.party_size) - if len(servername) == 19 and "Private Party" in servername: - self.rpc_thread.state = "Private Party" - elif servername == "": # A local game joinable from the internet - try: - offlinename = json.loads(bs.get_game_roster()[0]["spec_string"])[ - "n" - ] - if len(offlinename) > 19: # Thanks Rikko - self.rpc_thread.state = offlinename[slice(19)] + "..." - else: - self.rpc_thread.state = offlinename - except IndexError: - pass - else: - if len(servername) > 19: - self.rpc_thread.state = servername[slice(19)] + ".." - else: - self.rpc_thread.state = servername[slice(19)] - - if connection_info == {}: - self.rpc_thread.details = "Local" # ! replace with something like ballistica github cause - self.rpc_thread.state = self._get_current_activity_name() - self.rpc_thread.party_size = max(1, len(roster)) - self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) - - if ( - bs.get_foreground_host_session() is not None - and self.rpc_thread.details == "Local" - ): - session = ( - bs.get_foreground_host_session() - .__class__.__name__.replace("MainMenuSession", "") - .replace("EndSession", "") - .replace("FreeForAllSession", ": FFA") # ! for session use small image key - .replace("DualTeamSession", ": Teams") - .replace("CoopSession", ": Coop") - ) - #! self.rpc_thread.small_image_key = session.lower() - self.rpc_thread.details = f"{self.rpc_thread.details} {session}" - if ( - self.rpc_thread.state == "NoneType" - ): # sometimes the game just breaks which means its not really watching replay FIXME - self.rpc_thread.state = "Watching Replay" - self.rpc_thread.large_image_key = "replay" - self.rpc_thread.large_image_text = "Viewing Awesomeness" - #!self.rpc_thread.small_image_key = "replaysmall" - - act = bs.get_foreground_host_activity() - session = bs.get_foreground_host_session() - if act: - from bascenev1lib.game.elimination import EliminationGame - from bascenev1lib.game.thelaststand import TheLastStandGame - from bascenev1lib.game.meteorshower import MeteorShowerGame - - # noinspection PyUnresolvedReferences,PyProtectedMember - try: - self.rpc_thread.start_timestamp = act._discordrp_start_time # type: ignore - except AttributeError: - # This can be the case if plugin launched AFTER activity - # has been created; in that case let's assume it was - # created just now. - self.rpc_thread.start_timestamp = act._discordrp_start_time = time.mktime( # type: ignore - time.localtime() - ) - if isinstance(act, EliminationGame): - alive_count = len([p for p in act.players if p.lives > 0]) - self.rpc_thread.details += f" ({alive_count} players left)" - elif isinstance(act, TheLastStandGame): - # noinspection PyProtectedMember - points = act._score - self.rpc_thread.details += f" ({points} points)" - elif isinstance(act, MeteorShowerGame): - with bs.ContextRef(act): - sec = bs.time() - act._timer.getstarttime() - secfmt = "" - if sec < 60: - secfmt = f"{sec:.2f}" - else: - secfmt = f"{int(sec) // 60:02}:{sec:.2f}" - self.rpc_thread.details += f" ({secfmt})" - - # if isinstance(session, ba.DualTeamSession): - # scores = ':'.join([ - # str(t.customdata['score']) - # for t in session.sessionteams - # ]) - # self.rpc_thread.details += f' ({scores})' - - mapname, short_map_name = self._get_current_map_name() - if mapname: - with open(DIRPATH, 'r') as asset_dict: - asset_keys = json.load(asset_dict).keys() - if short_map_name in asset_keys: - self.rpc_thread.large_image_text = mapname - self.rpc_thread.large_image_key = short_map_name - self.rpc_thread.small_image_key = 'bombsquadlogo2' - self.rpc_thread.small_image_text = 'BombSquad' - - if _babase.get_idle_time() / (1000 * 60) % 60 >= 0.4: - self.rpc_thread.details = f"AFK in {self.rpc_thread.details}" - if not ANDROID: - self.rpc_thread.large_image_key = ( - "https://media.tenor.com/uAqNn6fv7x4AAAAM/bombsquad-spaz.gif" - ) - if ANDROID and Path(f"{getcwd()}/token.txt").exists(): - self.rpc_thread.presence() From 9f82a400fd0101eebb181c7db05fb514532d56e5 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:43:16 +0300 Subject: [PATCH 04/27] Add files via upload --- plugins/utilities/discord_richpresence.py | 918 ++++++++++++++++++++++ 1 file changed, 918 insertions(+) create mode 100644 plugins/utilities/discord_richpresence.py diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py new file mode 100644 index 00000000..7ea0615c --- /dev/null +++ b/plugins/utilities/discord_richpresence.py @@ -0,0 +1,918 @@ +# Released under the MIT and Apache License. See LICENSE for details. +# +"""placeholder :clown:""" + +# ba_meta require api 8 +#!"Made to you by @brostos & @Dliwk" + + +from __future__ import annotations +from urllib.request import Request, urlopen, urlretrieve +from pathlib import Path +from os import getcwd, remove +from zipfile import ZipFile +from bauiv1lib.popup import PopupWindow +from babase._mgen.enums import TimeType + +import asyncio +import http.client +import ast +import uuid +import json +import time +import threading +import shutil +import babase +import _babase +import bascenev1 as bs +import bascenev1lib +import bauiv1 as bui + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Tuple + + +ANDROID = babase.app.classic.platform == "android" +DIRPATH = Path(f"{_babase.app.python_directory_user}/image_id.json") + +if ANDROID: # !can add ios in future + + # Installing websocket + def get_module(): + install_path = Path(f"{getcwd()}/ba_data/python") # For the guys like me on windows + path = Path(f"{install_path}/websocket.zip") + file_path = Path(f"{install_path}/websocket") + source_dir = Path(f"{install_path}/websocket-client-1.6.1/websocket") + if not file_path.exists(): + url = "https://github.com/websocket-client/websocket-client/archive/refs/tags/v1.6.1.zip" + try: + filename, headers = urlretrieve(url, filename=path) + with ZipFile(filename) as f: + f.extractall(install_path) + shutil.copytree(source_dir, file_path) + shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) + remove(path) + except Exception as e: + if type(e) == shutil.Error: + shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) + else: + pass + get_module() + + import websocket + + heartbeat_interval = int(41250) + resume_gateway_url: str | None = None + session_id: str | None = None + + start_time = time.time() + + class PresenceUpdate: + def __init__(self): + self.state: str | None = "In Game" + self.details: str | None = "Main Menu" + self.start_timestamp = time.time() + self.large_image_key: str | None = "bombsquadicon" + self.large_image_text: str | None = "BombSquad Icon" + self.small_image_key: str | None = None + self.small_image_text: str | None = ( + f"{_babase.app.classic.platform.capitalize()}({_babase.app.version})") + self.media_proxy = "mp:/app-assets/963434684669382696/{}.png" + self.identify: bool = False + self.party_id: str = str(uuid.uuid4()) + self.party_size = 1 + self.party_max = 8 + + def presence(self): + with open(DIRPATH, "r") as maptxt: + largetxt = json.load(maptxt)[self.large_image_key] + with open(DIRPATH, "r") as maptxt: + smalltxt = json.load(maptxt)[self.small_image_key] + + presencepayload = { + "op": 3, + "d": { + "since": None, # used to show how long the user went idle will add afk to work with this and then set the status to idle + "status": "online", + "afk": "false", + "activities": [ + { + "name": "BombSquad", + "type": 0, + "application_id": "963434684669382696", + "state": self.state, + "details": self.details, + "timestamps": { + "start": start_time + }, + "party": { + "id": self.party_id, + "size": [self.party_size, self.party_max] + }, + "assets": { + "large_image": self.media_proxy.format(largetxt), + "large_text": self.large_image_text, + "small_image": self.media_proxy.format(smalltxt), + "small_text": self.small_image_text, + }, + "client_info": { + "version": 0, + "os": "android", + "client": "mobile", + }, + "buttons": ["Discord Server", "Download BombSquad"], + "metadata": { + "button_urls": [ + "https://discord.gg/bombsquad-ballistica-official-1001896771347304639", + "https://bombsquad-community.web.app/download", + ] + }, + } + ], + }, + } + ws.send(json.dumps(presencepayload)) + + def on_message(ws, message): + global heartbeat_interval, resume_gateway_url, session_id + message = json.loads(message) + try: + heartbeat_interval = message["d"]["heartbeat_interval"] + except: + pass + try: + resume_gateway_url = message["d"]["resume_gateway_url"] + session_id = message["d"]["session_id"] + except: + pass + + def on_error(ws, error): + babase.print_exception(error) + + def on_close(ws, close_status_code, close_msg): + # print("### closed ###") + pass + + def on_open(ws): + print("Connected to Discord Websocket") + + def heartbeats(): + """Sending heartbeats to keep the connection alive""" + global heartbeat_interval + if babase.do_once(): + heartbeat_payload = { + "op": 1, + "d": 251, + } # step two keeping connection alive by sending heart beats and receiving opcode 11 + ws.send(json.dumps(heartbeat_payload)) + + def identify(): + """Identifying to the gateway and enable by using user token and the intents we will be using e.g 256->For Presence""" + with open(f"{getcwd()}/token.txt", 'r') as f: + token = bytes.fromhex(f.read()).decode('utf-8') + identify_payload = { + "op": 2, + "d": { + "token": token, + "properties": { + "os": "linux", + "browser": "Discord Android", + "device": "android", + }, + "intents": 256, + }, + } # step 3 send an identify + ws.send(json.dumps(identify_payload)) + identify() + while True: + heartbeat_payload = {"op": 1, "d": heartbeat_interval} + try: + ws.send(json.dumps(heartbeat_payload)) + time.sleep(heartbeat_interval / 1000) + except: + pass + + threading.Thread(target=heartbeats, daemon=True, name="heartbeat").start() + + # websocket.enableTrace(True) + ws = websocket.WebSocketApp( + "wss://gateway.discord.gg/?encoding=json&v=10", + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close, + ) + if Path(f"{getcwd()}/token.txt").exists(): + threading.Thread(target=ws.run_forever, daemon=True, name="websocket").start() + + +if not ANDROID: + # installing pypresence + def get_module(): + install_path = Path(f"{getcwd()}/ba_data/python") + path = Path(f"{install_path}/pypresence.zip") + file_path = Path(f"{install_path}/pypresence") + source_dir = Path(f"{install_path}/pypresence-4.3.0/pypresence") + if not file_path.exists(): + url = "https://github.com/qwertyquerty/pypresence/archive/refs/tags/v4.3.0.zip" + try: + filename, headers = urlretrieve(url, filename=path) + with ZipFile(filename) as f: + f.extractall(install_path) + shutil.copytree(source_dir, file_path) + shutil.rmtree(Path(f"{install_path}/pypresence-4.3.0")) + remove(path) + except: + pass + + # Make modifications for it to work on windows + if babase.app.classic.platform == "windows": + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: + data = file.readlines() + data[45] = """ +def get_event_loop(force_fresh=False): + loop = asyncio.ProactorEventLoop() if sys.platform == 'win32' else asyncio.new_event_loop() + if force_fresh: + return loop + try: + running = asyncio.get_running_loop() + except RuntimeError: + return loop + if running.is_closed(): + return loop + else: + if sys.platform in ('linux', 'darwin'): + return running + if sys.platform == 'win32': + if isinstance(running, asyncio.ProactorEventLoop): + return running + else: + return loop""" + + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: + for number, line in enumerate(data): + if number not in range(46,56): + file.write(line) + get_module() + + + from pypresence import PipeClosed, DiscordError, DiscordNotFound + from pypresence.utils import get_event_loop + import pypresence + import socket + + DEBUG = True + + _last_server_addr = 'localhost' + _last_server_port = 43210 + + def print_error(err: str, include_exception: bool = False) -> None: + if DEBUG: + if include_exception: + babase.print_exception(err) + else: + babase.print_error(err) + else: + print(f"ERROR in discordrp.py: {err}") + + def log(msg: str) -> None: + if DEBUG: + print(f"LOG in discordrp.py: {msg}") + + def _run_overrides() -> None: + old_init = bs.Activity.__init__ + + def new_init(self, *args: Any, **kwargs: Any) -> None: # type: ignore + old_init(self, *args, **kwargs) + self._discordrp_start_time = time.mktime(time.localtime()) + + bs.Activity.__init__ = new_init # type: ignore + + old_connect = bs.connect_to_party + + def new_connect(*args, **kwargs) -> None: # type: ignore + global _last_server_addr + global _last_server_port + old_connect(*args, **kwargs) + c = kwargs.get("address") or args[0] + _last_server_port = kwargs.get("port") or args[1] + + bs.connect_to_party = new_connect + + start_time = time.time() + + class RpcThread(threading.Thread): + def __init__(self): + super().__init__(name="RpcThread") + self.rpc = pypresence.Presence(963434684669382696) + self.state: str | None = "In Game" + self.details: str | None = "Main Menu" + self.start_timestamp = time.mktime(time.localtime()) + self.large_image_key: str | None = "bombsquadicon" + self.large_image_text: str | None = "BombSquad Icon" + self.small_image_key: str | None = None + self.small_image_text: str | None = None + self.party_id: str = str(uuid.uuid4()) + self.party_size = 1 + self.party_max = 8 + self.join_secret: str | None = None + self._last_update_time: float = 0 + self._last_secret_update_time: float = 0 + self._last_connect_time: float = 0 + self.should_close = False + + @staticmethod + def is_discord_running(): + for i in range(6463,6473): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.01) + try: + conn = s.connect_ex(('localhost', i)) + s.close() + if (conn == 0): + s.close() + return(True) + except: + s.close() + return(False) + + def _generate_join_secret(self): + # resp = requests.get('https://legacy.ballistica.net/bsAccessCheck').text + connection_info = bs.get_connection_to_host_info() + if connection_info: + addr = _last_server_addr + port = _last_server_port + else: + try: + with urlopen( + "https://legacy.ballistica.net/bsAccessCheck" + ) as resp: + resp = resp.read().decode() + resp = ast.literal_eval(resp) + addr = resp["address"] + port = 43210 + secret_dict = { + "format_version": 1, + "hostname": addr, + "port": port, + } + self.join_secret = json.dumps(secret_dict) + except: + pass + + def _update_secret(self): + threading.Thread(target=self._generate_join_secret, daemon=True).start() + self._last_secret_update_time = time.time() + + def run(self) -> None: + asyncio.set_event_loop(get_event_loop()) + while not self.should_close: + if time.time() - self._last_update_time > 0.1: + self._do_update_presence() + if time.time() - self._last_secret_update_time > 15: + self._update_secret() + # if time.time() - self._last_connect_time > 120 and is_discord_running(): #!Eric please add module manager(pip) + # self._reconnect() + time.sleep(0.03) + + def _subscribe(self, event: str, **args): + self.rpc.send_data( + 1, + { + "nonce": f"{time.time():.20f}", + "cmd": "SUBSCRIBE", + "evt": event, + "args": args, + }, + ) + data = self.rpc.loop.run_until_complete(self.rpc.read_output()) + self.handle_event(data) + + def _subscribe_events(self): + self._subscribe("ACTIVITY_JOIN") + self._subscribe("ACTIVITY_JOIN_REQUEST") + + # def _update_presence(self) -> None: + # self._last_update_time = time.time() + # try: + # self._do_update_presence() + # except (AttributeError, AssertionError): + # try: + # self._reconnect() + # except Exception: + # print_error("failed to update presence", include_exception= True) + + + def _reconnect(self) -> None: + self.rpc.connect() + self._subscribe_events() + self._do_update_presence() + self._last_connect_time = time.time() + + def _do_update_presence(self) -> None: + if RpcThread.is_discord_running(): + self._last_update_time = time.time() + try: + data = self.rpc.update( + state=self.state or " ", + details=self.details, + start=start_time, + large_image=self.large_image_key, + large_text=self.large_image_text, + small_image=self.small_image_key, + small_text=self.small_image_text, + party_id=self.party_id, + party_size=[self.party_size, self.party_max], + join=self.join_secret, + # buttons = [ #!cant use buttons together with join + # { + # "label": "Discord Server", + # "url": "https://ballistica.net/discord" + # }, + # { + # "label": "Download Bombsquad", + # "url": "https://bombsquad.ga/download"} + # ] + ) + + self.handle_event(data) + except (PipeClosed, DiscordError, AssertionError, AttributeError): + try: + self._reconnect() + except (DiscordNotFound, DiscordError): + pass + + def handle_event(self, data): + evt = data["evt"] + if evt is None: + return + + data = data.get("data", {}) + + if evt == "ACTIVITY_JOIN": + secret = data.get("secret") + try: + server = json.loads(secret) + format_version = server["format_version"] + except Exception: + babase.print_exception("discordrp: unknown activity join format") + else: + try: + if format_version == 1: + hostname = server["hostname"] + port = server["port"] + self._connect_to_party(hostname, port) + except Exception: + babase.print_exception( + f"discordrp: incorrect activity join data, {format_version=}" + ) + + elif evt == "ACTIVITY_JOIN_REQUEST": + user = data.get("user", {}) + uid = user.get("id") + username = user.get("username") + discriminator = user.get("discriminator", None) + avatar = user.get("avatar") + self.on_join_request(username, uid, discriminator, avatar) + + def _connect_to_party(self, hostname, port) -> None: + babase.pushcall( + babase.Call(bs.connect_to_party, hostname, port), from_other_thread=True + ) + + def on_join_request(self, username, uid, discriminator, avatar) -> None: + del uid # unused + del avatar # unused + babase.pushcall( + babase.Call( + bui.screenmessage, + "Discord: {} wants to join!".format(username), + color=(0.0, 1.0, 0.0), + ), + from_other_thread=True, + ) + babase.pushcall(lambda: bui.getsound('bellMed').play(), from_other_thread=True) + + +class Discordlogin(PopupWindow): + + def __init__(self): + # pylint: disable=too-many-locals + _uiscale = bui.app.ui_v1.uiscale + self._transitioning_out = False + s = 1.25 if _uiscale is babase.UIScale.SMALL else 1.27 if _uiscale is babase.UIScale.MEDIUM else 1.3 + self._width = 380 * s + self._height = 150 + 150 * s + self.path = Path(f"{getcwd()}/token.txt") + bg_color = (0.5, 0.4, 0.6) + log_btn_colour = (0.10, 0.95, 0.10) if not self.path.exists() else (1.00, 0.15, 0.15) + log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" + + + + + # creates our _root_widget + PopupWindow.__init__(self, + position=(0.0, 0.0), + size=(self._width, self._height), + scale=(2.1 if _uiscale is babase.UIScale.SMALL else 1.5 + if _uiscale is babase.UIScale.MEDIUM else 1.0), + bg_color=bg_color) + + + self._cancel_button = bui.buttonwidget( + parent=self.root_widget, + position=(25, self._height - 40), + size=(50, 50), + scale=0.58, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=bui.gettexture('crossOut'), + iconscale=1.2) + + + + bui.imagewidget(parent=self.root_widget, + position=(180, self._height - 55), + size=(32 * s, 32 * s), + texture=bui.gettexture("discordLogo"), + color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) + + + + self.email_widget = bui.textwidget(parent=self.root_widget, + text="Email/Phone Number", + size=(400, 70), + position=(50, 180), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220) + + + self.password_widget = bui.textwidget(parent=self.root_widget, + text="Password", + size=(400, 70), + position=(50, 120), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220) + + + bui.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + bui.textwidget( + parent=self.root_widget, + position=(265, self._height - 37), + size=(0, 0), + h_align='center', + v_align='center', + scale=1.0, + text="Discord", + maxwidth=200, + color=(0.80, 0.80, 0.80)) + + bui.textwidget( + parent=self.root_widget, + position=(265, self._height - 78), + size=(0, 0), + h_align='center', + v_align='center', + scale=1.0, + text="💀Use at your own risk💀\n ⚠️discord account might get terminated⚠️", + maxwidth=200, + color=(1.00, 0.15, 0.15)) + + + self._login_button = bui.buttonwidget( + parent=self.root_widget, + position=(120, 65), + size=(400, 80), + scale=0.58, + label=log_txt, + color=log_btn_colour, + on_activate_call=self.login, + autoselect=True) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + bui.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_bascenev1libup_cancel(self) -> None: + bui.getsound('swish').play() + self._transition_out() + + def login(self): + if not self.path.exists(): + json_data = { + 'login': bui.textwidget(query=self.email_widget), + 'password': bui.textwidget(query=self.password_widget), + 'undelete': False, + 'captcha_key': None, + 'login_source': None, + 'gift_code_sku_id': None, + } + headers = { + 'user-agent': "Mozilla/5.0", + 'content-type': "application/json", + } + + conn = http.client.HTTPSConnection("discord.com") + + payload = json.dumps(json_data) + # conn.request("POST", "/api/v9/auth/login", payload, headers) + # res = conn.getresponse().read() + + try: + conn.request("POST", "/api/v9/auth/login", payload, headers) + res = conn.getresponse().read() + token = json.loads(res)['token'].encode().hex().encode() + with open(self.path, 'wb') as f: + f.write(token) + bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) + bui.getsound('shieldUp').play() + self.on_bascenev1libup_cancel() + except: + bui.screenmessage("Incorrect credentials", (1.00, 0.15, 0.15)) + bui.getsound('error').play() + + conn.close() + else: + remove(self.path) + bui.getsound('shieldDown').play() + bui.screenmessage("Account successfully removed!!", (0.10, 0.10, 1.00)) + self.on_bascenev1libup_cancel() + ws.close() + + +run_once = False +def get_once_asset(): + global run_once + if run_once: + return + response = Request( + "https://discordapp.com/api/oauth2/applications/963434684669382696/assets", + headers={"User-Agent": "Mozilla/5.0"}, + ) + try: + with urlopen(response) as assets: + assets = json.loads(assets.read()) + asset = [] + asset_id = [] + for x in assets: + dem = x["name"] + don = x["id"] + asset_id.append(don) + asset.append(dem) + asset_id_dict = dict(zip(asset, asset_id)) + + with open(DIRPATH, "w") as imagesets: + jsonfile = json.dumps(asset_id_dict) + json.dump(asset_id_dict, imagesets, indent=4) + except: + pass + run_once = True + +def get_class(): + if ANDROID: + return PresenceUpdate() + elif not ANDROID: + return RpcThread() + + +# ba_meta export babase.Plugin +class DiscordRP(babase.Plugin): + def __init__(self) -> None: + self.update_timer: bs.Timer | None = None + self.rpc_thread = get_class() + self._last_server_info: str | None = None + + if not ANDROID: + _run_overrides() + get_once_asset() + + def on_app_running(self) -> None: + if not ANDROID: + self.rpc_thread.start() + self.update_timer = bs.AppTimer( + 1, bs.WeakCall(self.update_status), repeat=True + ) + if ANDROID: + self.update_timer = bs.AppTimer( + 4, bs.WeakCall(self.update_status), repeat=True + ) + + def has_settings_ui(self): + return True + + def show_settings_ui(self, button): + if not ANDROID: + bui.screenmessage("Nothing here achievement!!!", (0.26, 0.65, 0.94)) + bui.getsound('achievement').play() + if ANDROID: + Discordlogin() + + def on_app_shutdown(self) -> None: + if not ANDROID and self.rpc_thread.is_discord_running(): + self.rpc_thread.rpc.close() + self.rpc_thread.should_close = True + else: + raise NotImplementedError("This function does not work on android") + # stupid code + # ws.close() + + # def on_app_pause(self) -> None: + # ws.close() + + # def on_app_resume(self) -> None: + # if Path(f"{getcwd()}/token.txt").exists(): + # threading.Thread(target=ws.run_forever, daemon=True, name="websocket").start() + + + + def _get_current_activity_name(self) -> str | None: + act = bs.get_foreground_host_activity() + if isinstance(act, bs.GameActivity): + return act.name + + this = "Lobby" + name: str | None = ( + act.__class__.__name__.replace("Activity", "") + .replace("ScoreScreen", "Ranking") + .replace("Coop", "") + .replace("MultiTeam", "") + .replace("Victory", "") + .replace("EndSession", "") + .replace("Transition", "") + .replace("Draw", "") + .replace("FreeForAll", "") + .replace("Join", this) + .replace("Team", "") + .replace("Series", "") + .replace("CustomSession", "Custom Session(mod)") + ) + + if name == "MainMenu": + name = "Main Menu" + if name == this: + self.rpc_thread.large_image_key = "lobby" + self.rpc_thread.large_image_text = "Bombing up" + #self.rpc_thread.small_image_key = "lobbysmall" + if name == "Ranking": + self.rpc_thread.large_image_key = "ranking" + self.rpc_thread.large_image_text = "Viewing Results" + return name + + def _get_current_map_name(self) -> Tuple[str | None, str | None]: + act = bs.get_foreground_host_activity() + if isinstance(act, bs.GameActivity): + texname = act.map.get_preview_texture_name() + if texname: + return act.map.name, texname.lower().removesuffix("preview") + return None, None + + def update_status(self) -> None: + roster = bs.get_game_roster() + connection_info = bs.get_connection_to_host_info() + + self.rpc_thread.large_image_key = "bombsquadicon" + self.rpc_thread.large_image_text = "BombSquad" + self.rpc_thread.small_image_key = _babase.app.classic.platform + self.rpc_thread.small_image_text = ( + f"{_babase.app.classic.platform.capitalize()}({_babase.app.version})" + ) + connection_info = bs.get_connection_to_host_info() + if not ANDROID: + svinfo = str(connection_info) + if self._last_server_info != svinfo: + self._last_server_info = svinfo + self.rpc_thread.party_id = str(uuid.uuid4()) + self.rpc_thread._update_secret() + if connection_info != {}: + servername = connection_info["name"] + self.rpc_thread.details = "Online" + self.rpc_thread.party_size = max( + 1, sum(len(client["players"]) for client in roster) + ) + self.rpc_thread.party_max = max(8, self.rpc_thread.party_size) + if len(servername) == 19 and "Private Party" in servername: + self.rpc_thread.state = "Private Party" + elif servername == "": # A local game joinable from the internet + try: + offlinename = json.loads(bs.get_game_roster()[0]["spec_string"])[ + "n" + ] + if len(offlinename) > 19: # Thanks Rikko + self.rpc_thread.state = offlinename[slice(19)] + "..." + else: + self.rpc_thread.state = offlinename + except IndexError: + pass + else: + if len(servername) > 19: + self.rpc_thread.state = servername[slice(19)] + ".." + else: + self.rpc_thread.state = servername[slice(19)] + + if connection_info == {}: + self.rpc_thread.details = "Local" #! replace with something like ballistica github cause + self.rpc_thread.state = self._get_current_activity_name() + self.rpc_thread.party_size = max(1, len(roster)) + self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) + + if ( + bs.get_foreground_host_session() is not None + and self.rpc_thread.details == "Local" + ): + session = ( + bs.get_foreground_host_session() + .__class__.__name__.replace("MainMenuSession", "") + .replace("EndSession", "") + .replace("FreeForAllSession", ": FFA") #! for session use small image key + .replace("DualTeamSession", ": Teams") + .replace("CoopSession", ": Coop") + ) + #! self.rpc_thread.small_image_key = session.lower() + self.rpc_thread.details = f"{self.rpc_thread.details} {session}" + if ( + self.rpc_thread.state == "NoneType" + ): # sometimes the game just breaks which means its not really watching replay FIXME + self.rpc_thread.state = "Watching Replay" + self.rpc_thread.large_image_key = "replay" + self.rpc_thread.large_image_text = "Viewing Awesomeness" + #!self.rpc_thread.small_image_key = "replaysmall" + + act = bs.get_foreground_host_activity() + session = bs.get_foreground_host_session() + if act: + from bascenev1lib.game.elimination import EliminationGame + from bascenev1lib.game.thelaststand import TheLastStandGame + from bascenev1lib.game.meteorshower import MeteorShowerGame + + # noinspection PyUnresolvedReferences,PyProtectedMember + try: + self.rpc_thread.start_timestamp = act._discordrp_start_time # type: ignore + except AttributeError: + # This can be the case if plugin launched AFTER activity + # has been created; in that case let's assume it was + # created just now. + self.rpc_thread.start_timestamp = act._discordrp_start_time = time.mktime( # type: ignore + time.localtime() + ) + if isinstance(act, EliminationGame): + alive_count = len([p for p in act.players if p.lives > 0]) + self.rpc_thread.details += f" ({alive_count} players left)" + elif isinstance(act, TheLastStandGame): + # noinspection PyProtectedMember + points = act._score + self.rpc_thread.details += f" ({points} points)" + elif isinstance(act, MeteorShowerGame): + with bs.ContextRef(act): + sec = bs.time() - act._timer.getstarttime() + secfmt = "" + if sec < 60: + secfmt = f"{sec:.2f}" + else: + secfmt = f"{int(sec) // 60:02}:{sec:.2f}" + self.rpc_thread.details += f" ({secfmt})" + + # if isinstance(session, ba.DualTeamSession): + # scores = ':'.join([ + # str(t.customdata['score']) + # for t in session.sessionteams + # ]) + # self.rpc_thread.details += f' ({scores})' + + mapname, short_map_name = self._get_current_map_name() + if mapname: + with open(DIRPATH, 'r') as asset_dict: + asset_keys = json.load(asset_dict).keys() + if short_map_name in asset_keys: + self.rpc_thread.large_image_text = mapname + self.rpc_thread.large_image_key = short_map_name + self.rpc_thread.small_image_key = 'bombsquadlogo2' + self.rpc_thread.small_image_text = 'BombSquad' + + if _babase.get_idle_time() / (1000 * 60) % 60 >= 0.4: + self.rpc_thread.details = f"AFK in {self.rpc_thread.details}" + if not ANDROID: + self.rpc_thread.large_image_key = ( + "https://media.tenor.com/uAqNn6fv7x4AAAAM/bombsquad-spaz.gif" + ) + if ANDROID and Path(f"{getcwd()}/token.txt").exists(): + self.rpc_thread.presence() + \ No newline at end of file From 250bc1f84f89394552f38c1168661533aac1be65 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:48:51 +0300 Subject: [PATCH 05/27] Update discord_richpresence.py --- plugins/utilities/discord_richpresence.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index 7ea0615c..21002557 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -730,10 +730,10 @@ def on_app_shutdown(self) -> None: if not ANDROID and self.rpc_thread.is_discord_running(): self.rpc_thread.rpc.close() self.rpc_thread.should_close = True - else: - raise NotImplementedError("This function does not work on android") - # stupid code - # ws.close() + # else: + # raise NotImplementedError("This function does not work on android") + # # stupid code + # # ws.close() # def on_app_pause(self) -> None: # ws.close() @@ -915,4 +915,4 @@ def update_status(self) -> None: ) if ANDROID and Path(f"{getcwd()}/token.txt").exists(): self.rpc_thread.presence() - \ No newline at end of file + From 0af7b4a66e2910a3ece24e02ffae34be1633924d Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:51:05 +0300 Subject: [PATCH 06/27] Update utilities.json --- plugins/utilities.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 08ee3bdb..fe629cda 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,7 +767,7 @@ } ], "versions": { - "1.0.0": { + "1.1.0": { "api_version": 8, "commit_sha": "230d12d", "released_on": "18-07-2023", @@ -776,4 +776,4 @@ } } } -} \ No newline at end of file +} From e17dccdaa98e925c8d0d492912325377b14c6f57 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:56:12 +0300 Subject: [PATCH 07/27] Update utilities.json --- plugins/utilities.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index fe629cda..5d788034 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,13 +767,16 @@ } ], "versions": { - "1.1.0": { + "1.0.0": { "api_version": 8, "commit_sha": "230d12d", "released_on": "18-07-2023", "md5sum": "5fa8706f36d618f8302551dd2a0403a0" - } + }, + "versions": { + "1.1.0": null } } } } + From 97885d7952ba7f233ca27c3bc1973ac532f485c1 Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Fri, 21 Jul 2023 09:03:10 +0000 Subject: [PATCH 08/27] [ci] auto-format --- plugins/utilities/discord_richpresence.py | 127 ++++++++++------------ 1 file changed, 57 insertions(+), 70 deletions(-) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index 21002557..9b91f39b 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -24,7 +24,7 @@ import shutil import babase import _babase -import bascenev1 as bs +import bascenev1 as bs import bascenev1lib import bauiv1 as bui @@ -226,7 +226,7 @@ def get_module(): remove(path) except: pass - + # Make modifications for it to work on windows if babase.app.classic.platform == "windows": with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: @@ -250,24 +250,23 @@ def get_event_loop(force_fresh=False): return running else: return loop""" - + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: for number, line in enumerate(data): - if number not in range(46,56): + if number not in range(46, 56): file.write(line) get_module() - from pypresence import PipeClosed, DiscordError, DiscordNotFound from pypresence.utils import get_event_loop - import pypresence + import pypresence import socket - + DEBUG = True - + _last_server_addr = 'localhost' _last_server_port = 43210 - + def print_error(err: str, include_exception: bool = False) -> None: if DEBUG: if include_exception: @@ -298,8 +297,8 @@ def new_connect(*args, **kwargs) -> None: # type: ignore old_connect(*args, **kwargs) c = kwargs.get("address") or args[0] _last_server_port = kwargs.get("port") or args[1] - - bs.connect_to_party = new_connect + + bs.connect_to_party = new_connect start_time = time.time() @@ -322,10 +321,10 @@ def __init__(self): self._last_secret_update_time: float = 0 self._last_connect_time: float = 0 self.should_close = False - - @staticmethod + + @staticmethod def is_discord_running(): - for i in range(6463,6473): + for i in range(6463, 6473): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.01) try: @@ -333,11 +332,11 @@ def is_discord_running(): s.close() if (conn == 0): s.close() - return(True) + return (True) except: s.close() - return(False) - + return (False) + def _generate_join_secret(self): # resp = requests.get('https://legacy.ballistica.net/bsAccessCheck').text connection_info = bs.get_connection_to_host_info() @@ -394,7 +393,7 @@ def _subscribe_events(self): self._subscribe("ACTIVITY_JOIN") self._subscribe("ACTIVITY_JOIN_REQUEST") - # def _update_presence(self) -> None: + # def _update_presence(self) -> None: # self._last_update_time = time.time() # try: # self._do_update_presence() @@ -403,7 +402,6 @@ def _subscribe_events(self): # self._reconnect() # except Exception: # print_error("failed to update presence", include_exception= True) - def _reconnect(self) -> None: self.rpc.connect() @@ -480,7 +478,7 @@ def handle_event(self, data): def _connect_to_party(self, hostname, port) -> None: babase.pushcall( babase.Call(bs.connect_to_party, hostname, port), from_other_thread=True - ) + ) def on_join_request(self, username, uid, discriminator, avatar) -> None: del uid # unused @@ -508,10 +506,7 @@ def __init__(self): self.path = Path(f"{getcwd()}/token.txt") bg_color = (0.5, 0.4, 0.6) log_btn_colour = (0.10, 0.95, 0.10) if not self.path.exists() else (1.00, 0.15, 0.15) - log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" - - - + log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" # creates our _root_widget PopupWindow.__init__(self, @@ -520,7 +515,6 @@ def __init__(self): scale=(2.1 if _uiscale is babase.UIScale.SMALL else 1.5 if _uiscale is babase.UIScale.MEDIUM else 1.0), bg_color=bg_color) - self._cancel_button = bui.buttonwidget( parent=self.root_widget, @@ -533,44 +527,38 @@ def __init__(self): autoselect=True, icon=bui.gettexture('crossOut'), iconscale=1.2) - - - + bui.imagewidget(parent=self.root_widget, - position=(180, self._height - 55), - size=(32 * s, 32 * s), - texture=bui.gettexture("discordLogo"), - color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) - + position=(180, self._height - 55), + size=(32 * s, 32 * s), + texture=bui.gettexture("discordLogo"), + color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) - self.email_widget = bui.textwidget(parent=self.root_widget, - text="Email/Phone Number", - size=(400, 70), - position=(50, 180), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) - - + text="Email/Phone Number", + size=(400, 70), + position=(50, 180), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220) + self.password_widget = bui.textwidget(parent=self.root_widget, - text="Password", - size=(400, 70), - position=(50, 120), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) - - + text="Password", + size=(400, 70), + position=(50, 120), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220) + bui.containerwidget(edit=self.root_widget, - cancel_button=self._cancel_button) - + cancel_button=self._cancel_button) + bui.textwidget( parent=self.root_widget, position=(265, self._height - 37), @@ -581,7 +569,7 @@ def __init__(self): text="Discord", maxwidth=200, color=(0.80, 0.80, 0.80)) - + bui.textwidget( parent=self.root_widget, position=(265, self._height - 78), @@ -592,8 +580,7 @@ def __init__(self): text="💀Use at your own risk💀\n ⚠️discord account might get terminated⚠️", maxwidth=200, color=(1.00, 0.15, 0.15)) - - + self._login_button = bui.buttonwidget( parent=self.root_widget, position=(120, 65), @@ -658,8 +645,10 @@ def login(self): self.on_bascenev1libup_cancel() ws.close() - + run_once = False + + def get_once_asset(): global run_once if run_once: @@ -687,6 +676,7 @@ def get_once_asset(): pass run_once = True + def get_class(): if ANDROID: return PresenceUpdate() @@ -707,7 +697,7 @@ def __init__(self) -> None: def on_app_running(self) -> None: if not ANDROID: - self.rpc_thread.start() + self.rpc_thread.start() self.update_timer = bs.AppTimer( 1, bs.WeakCall(self.update_status), repeat=True ) @@ -742,13 +732,11 @@ def on_app_shutdown(self) -> None: # if Path(f"{getcwd()}/token.txt").exists(): # threading.Thread(target=ws.run_forever, daemon=True, name="websocket").start() - - def _get_current_activity_name(self) -> str | None: act = bs.get_foreground_host_activity() if isinstance(act, bs.GameActivity): return act.name - + this = "Lobby" name: str | None = ( act.__class__.__name__.replace("Activity", "") @@ -816,7 +804,7 @@ def update_status(self) -> None: offlinename = json.loads(bs.get_game_roster()[0]["spec_string"])[ "n" ] - if len(offlinename) > 19: # Thanks Rikko + if len(offlinename) > 19: # Thanks Rikko self.rpc_thread.state = offlinename[slice(19)] + "..." else: self.rpc_thread.state = offlinename @@ -829,7 +817,7 @@ def update_status(self) -> None: self.rpc_thread.state = servername[slice(19)] if connection_info == {}: - self.rpc_thread.details = "Local" #! replace with something like ballistica github cause + self.rpc_thread.details = "Local" # ! replace with something like ballistica github cause self.rpc_thread.state = self._get_current_activity_name() self.rpc_thread.party_size = max(1, len(roster)) self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) @@ -842,7 +830,7 @@ def update_status(self) -> None: bs.get_foreground_host_session() .__class__.__name__.replace("MainMenuSession", "") .replace("EndSession", "") - .replace("FreeForAllSession", ": FFA") #! for session use small image key + .replace("FreeForAllSession", ": FFA") # ! for session use small image key .replace("DualTeamSession", ": Teams") .replace("CoopSession", ": Coop") ) @@ -915,4 +903,3 @@ def update_status(self) -> None: ) if ANDROID and Path(f"{getcwd()}/token.txt").exists(): self.rpc_thread.presence() - From 4e926ae16859e792e88719c33de4fd5fbe9dc7e4 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 12:05:57 +0300 Subject: [PATCH 09/27] Update utilities.json --- plugins/utilities.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 5d788034..5a02b39e 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -776,7 +776,7 @@ "versions": { "1.1.0": null } - } + }, } } From 05f20a04a3f44b926f8b495a2a57151ad3c518f6 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 12:09:56 +0300 Subject: [PATCH 10/27] Update utilities.json --- plugins/utilities.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 5a02b39e..33a143b6 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,16 +767,16 @@ } ], "versions": { + "versions": { + "1.1.0": null, "1.0.0": { "api_version": 8, "commit_sha": "230d12d", "released_on": "18-07-2023", "md5sum": "5fa8706f36d618f8302551dd2a0403a0" - }, - "versions": { - "1.1.0": null + } } - }, + } } } From e20efd8541ad152d42f385b0b3f6729a6bf14792 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 12:16:14 +0300 Subject: [PATCH 11/27] work --- plugins/utilities.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 33a143b6..6aca9fe1 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,7 +767,6 @@ } ], "versions": { - "versions": { "1.1.0": null, "1.0.0": { "api_version": 8, From 81abddff28e21a548fb60a40f6a4d9c4dd78324e Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Fri, 21 Jul 2023 09:16:48 +0000 Subject: [PATCH 12/27] [ci] apply-version-metadata --- plugins/utilities.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 6aca9fe1..4d436984 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,7 +767,12 @@ } ], "versions": { - "1.1.0": null, + "1.1.0": { + "api_version": 8, + "commit_sha": "e20efd8", + "released_on": "21-07-2023", + "md5sum": "f0dda27fcd396b4d3e8b35ca0ec5d251" + }, "1.0.0": { "api_version": 8, "commit_sha": "230d12d", @@ -777,5 +782,4 @@ } } } -} - +} \ No newline at end of file From 3c83efcc0559edf26ee984a0837daa75cc6ddf28 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 23:56:30 +0300 Subject: [PATCH 13/27] Update discord_richpresence.py --- plugins/utilities/discord_richpresence.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index 9b91f39b..1e1d8850 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -227,11 +227,11 @@ def get_module(): except: pass - # Make modifications for it to work on windows - if babase.app.classic.platform == "windows": - with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: - data = file.readlines() - data[45] = """ + # Make modifications for it to work on windows + if babase.app.classic.platform == "windows": + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: + data = file.readlines() + data[45] = """ def get_event_loop(force_fresh=False): loop = asyncio.ProactorEventLoop() if sys.platform == 'win32' else asyncio.new_event_loop() if force_fresh: @@ -250,11 +250,11 @@ def get_event_loop(force_fresh=False): return running else: return loop""" - - with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: - for number, line in enumerate(data): - if number not in range(46, 56): - file.write(line) + + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: + for number, line in enumerate(data): + if number not in range(46, 56): + file.write(line) get_module() from pypresence import PipeClosed, DiscordError, DiscordNotFound From 16b9b546d9b82cb85cf54528acae555ddccda174 Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Fri, 21 Jul 2023 20:57:04 +0000 Subject: [PATCH 14/27] [ci] auto-format --- plugins/utilities/discord_richpresence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index 1e1d8850..9b0ea84a 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -250,7 +250,7 @@ def get_event_loop(force_fresh=False): return running else: return loop""" - + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: for number, line in enumerate(data): if number not in range(46, 56): From 8f02db1e34bd38d0afa8423777243694340f8366 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Fri, 21 Jul 2023 23:57:20 +0300 Subject: [PATCH 15/27] Update utilities.json --- plugins/utilities.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 4d436984..95557ec7 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,12 +767,7 @@ } ], "versions": { - "1.1.0": { - "api_version": 8, - "commit_sha": "e20efd8", - "released_on": "21-07-2023", - "md5sum": "f0dda27fcd396b4d3e8b35ca0ec5d251" - }, + "1.1.0": null, "1.0.0": { "api_version": 8, "commit_sha": "230d12d", @@ -782,4 +777,4 @@ } } } -} \ No newline at end of file +} From c4916e82d38bb4aec4d785854e42e4a0f05b86c5 Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Fri, 21 Jul 2023 20:57:46 +0000 Subject: [PATCH 16/27] [ci] apply-version-metadata --- plugins/utilities.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 95557ec7..5142fd43 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,7 +767,12 @@ } ], "versions": { - "1.1.0": null, + "1.1.0": { + "api_version": 8, + "commit_sha": "8f02db1", + "released_on": "21-07-2023", + "md5sum": "402a406cab00e6fc6687738d237aeda6" + }, "1.0.0": { "api_version": 8, "commit_sha": "230d12d", @@ -777,4 +782,4 @@ } } } -} +} \ No newline at end of file From 5bfc25cc35fed46727ec22ef75fb6c936e47dc56 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Sat, 22 Jul 2023 03:54:00 +0300 Subject: [PATCH 17/27] Delete discord_richpresence.py --- plugins/utilities/discord_richpresence.py | 905 ---------------------- 1 file changed, 905 deletions(-) delete mode 100644 plugins/utilities/discord_richpresence.py diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py deleted file mode 100644 index 9b0ea84a..00000000 --- a/plugins/utilities/discord_richpresence.py +++ /dev/null @@ -1,905 +0,0 @@ -# Released under the MIT and Apache License. See LICENSE for details. -# -"""placeholder :clown:""" - -# ba_meta require api 8 -#!"Made to you by @brostos & @Dliwk" - - -from __future__ import annotations -from urllib.request import Request, urlopen, urlretrieve -from pathlib import Path -from os import getcwd, remove -from zipfile import ZipFile -from bauiv1lib.popup import PopupWindow -from babase._mgen.enums import TimeType - -import asyncio -import http.client -import ast -import uuid -import json -import time -import threading -import shutil -import babase -import _babase -import bascenev1 as bs -import bascenev1lib -import bauiv1 as bui - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Tuple - - -ANDROID = babase.app.classic.platform == "android" -DIRPATH = Path(f"{_babase.app.python_directory_user}/image_id.json") - -if ANDROID: # !can add ios in future - - # Installing websocket - def get_module(): - install_path = Path(f"{getcwd()}/ba_data/python") # For the guys like me on windows - path = Path(f"{install_path}/websocket.zip") - file_path = Path(f"{install_path}/websocket") - source_dir = Path(f"{install_path}/websocket-client-1.6.1/websocket") - if not file_path.exists(): - url = "https://github.com/websocket-client/websocket-client/archive/refs/tags/v1.6.1.zip" - try: - filename, headers = urlretrieve(url, filename=path) - with ZipFile(filename) as f: - f.extractall(install_path) - shutil.copytree(source_dir, file_path) - shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) - remove(path) - except Exception as e: - if type(e) == shutil.Error: - shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) - else: - pass - get_module() - - import websocket - - heartbeat_interval = int(41250) - resume_gateway_url: str | None = None - session_id: str | None = None - - start_time = time.time() - - class PresenceUpdate: - def __init__(self): - self.state: str | None = "In Game" - self.details: str | None = "Main Menu" - self.start_timestamp = time.time() - self.large_image_key: str | None = "bombsquadicon" - self.large_image_text: str | None = "BombSquad Icon" - self.small_image_key: str | None = None - self.small_image_text: str | None = ( - f"{_babase.app.classic.platform.capitalize()}({_babase.app.version})") - self.media_proxy = "mp:/app-assets/963434684669382696/{}.png" - self.identify: bool = False - self.party_id: str = str(uuid.uuid4()) - self.party_size = 1 - self.party_max = 8 - - def presence(self): - with open(DIRPATH, "r") as maptxt: - largetxt = json.load(maptxt)[self.large_image_key] - with open(DIRPATH, "r") as maptxt: - smalltxt = json.load(maptxt)[self.small_image_key] - - presencepayload = { - "op": 3, - "d": { - "since": None, # used to show how long the user went idle will add afk to work with this and then set the status to idle - "status": "online", - "afk": "false", - "activities": [ - { - "name": "BombSquad", - "type": 0, - "application_id": "963434684669382696", - "state": self.state, - "details": self.details, - "timestamps": { - "start": start_time - }, - "party": { - "id": self.party_id, - "size": [self.party_size, self.party_max] - }, - "assets": { - "large_image": self.media_proxy.format(largetxt), - "large_text": self.large_image_text, - "small_image": self.media_proxy.format(smalltxt), - "small_text": self.small_image_text, - }, - "client_info": { - "version": 0, - "os": "android", - "client": "mobile", - }, - "buttons": ["Discord Server", "Download BombSquad"], - "metadata": { - "button_urls": [ - "https://discord.gg/bombsquad-ballistica-official-1001896771347304639", - "https://bombsquad-community.web.app/download", - ] - }, - } - ], - }, - } - ws.send(json.dumps(presencepayload)) - - def on_message(ws, message): - global heartbeat_interval, resume_gateway_url, session_id - message = json.loads(message) - try: - heartbeat_interval = message["d"]["heartbeat_interval"] - except: - pass - try: - resume_gateway_url = message["d"]["resume_gateway_url"] - session_id = message["d"]["session_id"] - except: - pass - - def on_error(ws, error): - babase.print_exception(error) - - def on_close(ws, close_status_code, close_msg): - # print("### closed ###") - pass - - def on_open(ws): - print("Connected to Discord Websocket") - - def heartbeats(): - """Sending heartbeats to keep the connection alive""" - global heartbeat_interval - if babase.do_once(): - heartbeat_payload = { - "op": 1, - "d": 251, - } # step two keeping connection alive by sending heart beats and receiving opcode 11 - ws.send(json.dumps(heartbeat_payload)) - - def identify(): - """Identifying to the gateway and enable by using user token and the intents we will be using e.g 256->For Presence""" - with open(f"{getcwd()}/token.txt", 'r') as f: - token = bytes.fromhex(f.read()).decode('utf-8') - identify_payload = { - "op": 2, - "d": { - "token": token, - "properties": { - "os": "linux", - "browser": "Discord Android", - "device": "android", - }, - "intents": 256, - }, - } # step 3 send an identify - ws.send(json.dumps(identify_payload)) - identify() - while True: - heartbeat_payload = {"op": 1, "d": heartbeat_interval} - try: - ws.send(json.dumps(heartbeat_payload)) - time.sleep(heartbeat_interval / 1000) - except: - pass - - threading.Thread(target=heartbeats, daemon=True, name="heartbeat").start() - - # websocket.enableTrace(True) - ws = websocket.WebSocketApp( - "wss://gateway.discord.gg/?encoding=json&v=10", - on_open=on_open, - on_message=on_message, - on_error=on_error, - on_close=on_close, - ) - if Path(f"{getcwd()}/token.txt").exists(): - threading.Thread(target=ws.run_forever, daemon=True, name="websocket").start() - - -if not ANDROID: - # installing pypresence - def get_module(): - install_path = Path(f"{getcwd()}/ba_data/python") - path = Path(f"{install_path}/pypresence.zip") - file_path = Path(f"{install_path}/pypresence") - source_dir = Path(f"{install_path}/pypresence-4.3.0/pypresence") - if not file_path.exists(): - url = "https://github.com/qwertyquerty/pypresence/archive/refs/tags/v4.3.0.zip" - try: - filename, headers = urlretrieve(url, filename=path) - with ZipFile(filename) as f: - f.extractall(install_path) - shutil.copytree(source_dir, file_path) - shutil.rmtree(Path(f"{install_path}/pypresence-4.3.0")) - remove(path) - except: - pass - - # Make modifications for it to work on windows - if babase.app.classic.platform == "windows": - with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: - data = file.readlines() - data[45] = """ -def get_event_loop(force_fresh=False): - loop = asyncio.ProactorEventLoop() if sys.platform == 'win32' else asyncio.new_event_loop() - if force_fresh: - return loop - try: - running = asyncio.get_running_loop() - except RuntimeError: - return loop - if running.is_closed(): - return loop - else: - if sys.platform in ('linux', 'darwin'): - return running - if sys.platform == 'win32': - if isinstance(running, asyncio.ProactorEventLoop): - return running - else: - return loop""" - - with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: - for number, line in enumerate(data): - if number not in range(46, 56): - file.write(line) - get_module() - - from pypresence import PipeClosed, DiscordError, DiscordNotFound - from pypresence.utils import get_event_loop - import pypresence - import socket - - DEBUG = True - - _last_server_addr = 'localhost' - _last_server_port = 43210 - - def print_error(err: str, include_exception: bool = False) -> None: - if DEBUG: - if include_exception: - babase.print_exception(err) - else: - babase.print_error(err) - else: - print(f"ERROR in discordrp.py: {err}") - - def log(msg: str) -> None: - if DEBUG: - print(f"LOG in discordrp.py: {msg}") - - def _run_overrides() -> None: - old_init = bs.Activity.__init__ - - def new_init(self, *args: Any, **kwargs: Any) -> None: # type: ignore - old_init(self, *args, **kwargs) - self._discordrp_start_time = time.mktime(time.localtime()) - - bs.Activity.__init__ = new_init # type: ignore - - old_connect = bs.connect_to_party - - def new_connect(*args, **kwargs) -> None: # type: ignore - global _last_server_addr - global _last_server_port - old_connect(*args, **kwargs) - c = kwargs.get("address") or args[0] - _last_server_port = kwargs.get("port") or args[1] - - bs.connect_to_party = new_connect - - start_time = time.time() - - class RpcThread(threading.Thread): - def __init__(self): - super().__init__(name="RpcThread") - self.rpc = pypresence.Presence(963434684669382696) - self.state: str | None = "In Game" - self.details: str | None = "Main Menu" - self.start_timestamp = time.mktime(time.localtime()) - self.large_image_key: str | None = "bombsquadicon" - self.large_image_text: str | None = "BombSquad Icon" - self.small_image_key: str | None = None - self.small_image_text: str | None = None - self.party_id: str = str(uuid.uuid4()) - self.party_size = 1 - self.party_max = 8 - self.join_secret: str | None = None - self._last_update_time: float = 0 - self._last_secret_update_time: float = 0 - self._last_connect_time: float = 0 - self.should_close = False - - @staticmethod - def is_discord_running(): - for i in range(6463, 6473): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(0.01) - try: - conn = s.connect_ex(('localhost', i)) - s.close() - if (conn == 0): - s.close() - return (True) - except: - s.close() - return (False) - - def _generate_join_secret(self): - # resp = requests.get('https://legacy.ballistica.net/bsAccessCheck').text - connection_info = bs.get_connection_to_host_info() - if connection_info: - addr = _last_server_addr - port = _last_server_port - else: - try: - with urlopen( - "https://legacy.ballistica.net/bsAccessCheck" - ) as resp: - resp = resp.read().decode() - resp = ast.literal_eval(resp) - addr = resp["address"] - port = 43210 - secret_dict = { - "format_version": 1, - "hostname": addr, - "port": port, - } - self.join_secret = json.dumps(secret_dict) - except: - pass - - def _update_secret(self): - threading.Thread(target=self._generate_join_secret, daemon=True).start() - self._last_secret_update_time = time.time() - - def run(self) -> None: - asyncio.set_event_loop(get_event_loop()) - while not self.should_close: - if time.time() - self._last_update_time > 0.1: - self._do_update_presence() - if time.time() - self._last_secret_update_time > 15: - self._update_secret() - # if time.time() - self._last_connect_time > 120 and is_discord_running(): #!Eric please add module manager(pip) - # self._reconnect() - time.sleep(0.03) - - def _subscribe(self, event: str, **args): - self.rpc.send_data( - 1, - { - "nonce": f"{time.time():.20f}", - "cmd": "SUBSCRIBE", - "evt": event, - "args": args, - }, - ) - data = self.rpc.loop.run_until_complete(self.rpc.read_output()) - self.handle_event(data) - - def _subscribe_events(self): - self._subscribe("ACTIVITY_JOIN") - self._subscribe("ACTIVITY_JOIN_REQUEST") - - # def _update_presence(self) -> None: - # self._last_update_time = time.time() - # try: - # self._do_update_presence() - # except (AttributeError, AssertionError): - # try: - # self._reconnect() - # except Exception: - # print_error("failed to update presence", include_exception= True) - - def _reconnect(self) -> None: - self.rpc.connect() - self._subscribe_events() - self._do_update_presence() - self._last_connect_time = time.time() - - def _do_update_presence(self) -> None: - if RpcThread.is_discord_running(): - self._last_update_time = time.time() - try: - data = self.rpc.update( - state=self.state or " ", - details=self.details, - start=start_time, - large_image=self.large_image_key, - large_text=self.large_image_text, - small_image=self.small_image_key, - small_text=self.small_image_text, - party_id=self.party_id, - party_size=[self.party_size, self.party_max], - join=self.join_secret, - # buttons = [ #!cant use buttons together with join - # { - # "label": "Discord Server", - # "url": "https://ballistica.net/discord" - # }, - # { - # "label": "Download Bombsquad", - # "url": "https://bombsquad.ga/download"} - # ] - ) - - self.handle_event(data) - except (PipeClosed, DiscordError, AssertionError, AttributeError): - try: - self._reconnect() - except (DiscordNotFound, DiscordError): - pass - - def handle_event(self, data): - evt = data["evt"] - if evt is None: - return - - data = data.get("data", {}) - - if evt == "ACTIVITY_JOIN": - secret = data.get("secret") - try: - server = json.loads(secret) - format_version = server["format_version"] - except Exception: - babase.print_exception("discordrp: unknown activity join format") - else: - try: - if format_version == 1: - hostname = server["hostname"] - port = server["port"] - self._connect_to_party(hostname, port) - except Exception: - babase.print_exception( - f"discordrp: incorrect activity join data, {format_version=}" - ) - - elif evt == "ACTIVITY_JOIN_REQUEST": - user = data.get("user", {}) - uid = user.get("id") - username = user.get("username") - discriminator = user.get("discriminator", None) - avatar = user.get("avatar") - self.on_join_request(username, uid, discriminator, avatar) - - def _connect_to_party(self, hostname, port) -> None: - babase.pushcall( - babase.Call(bs.connect_to_party, hostname, port), from_other_thread=True - ) - - def on_join_request(self, username, uid, discriminator, avatar) -> None: - del uid # unused - del avatar # unused - babase.pushcall( - babase.Call( - bui.screenmessage, - "Discord: {} wants to join!".format(username), - color=(0.0, 1.0, 0.0), - ), - from_other_thread=True, - ) - babase.pushcall(lambda: bui.getsound('bellMed').play(), from_other_thread=True) - - -class Discordlogin(PopupWindow): - - def __init__(self): - # pylint: disable=too-many-locals - _uiscale = bui.app.ui_v1.uiscale - self._transitioning_out = False - s = 1.25 if _uiscale is babase.UIScale.SMALL else 1.27 if _uiscale is babase.UIScale.MEDIUM else 1.3 - self._width = 380 * s - self._height = 150 + 150 * s - self.path = Path(f"{getcwd()}/token.txt") - bg_color = (0.5, 0.4, 0.6) - log_btn_colour = (0.10, 0.95, 0.10) if not self.path.exists() else (1.00, 0.15, 0.15) - log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" - - # creates our _root_widget - PopupWindow.__init__(self, - position=(0.0, 0.0), - size=(self._width, self._height), - scale=(2.1 if _uiscale is babase.UIScale.SMALL else 1.5 - if _uiscale is babase.UIScale.MEDIUM else 1.0), - bg_color=bg_color) - - self._cancel_button = bui.buttonwidget( - parent=self.root_widget, - position=(25, self._height - 40), - size=(50, 50), - scale=0.58, - label='', - color=bg_color, - on_activate_call=self._on_cancel_press, - autoselect=True, - icon=bui.gettexture('crossOut'), - iconscale=1.2) - - bui.imagewidget(parent=self.root_widget, - position=(180, self._height - 55), - size=(32 * s, 32 * s), - texture=bui.gettexture("discordLogo"), - color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) - - self.email_widget = bui.textwidget(parent=self.root_widget, - text="Email/Phone Number", - size=(400, 70), - position=(50, 180), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) - - self.password_widget = bui.textwidget(parent=self.root_widget, - text="Password", - size=(400, 70), - position=(50, 120), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) - - bui.containerwidget(edit=self.root_widget, - cancel_button=self._cancel_button) - - bui.textwidget( - parent=self.root_widget, - position=(265, self._height - 37), - size=(0, 0), - h_align='center', - v_align='center', - scale=1.0, - text="Discord", - maxwidth=200, - color=(0.80, 0.80, 0.80)) - - bui.textwidget( - parent=self.root_widget, - position=(265, self._height - 78), - size=(0, 0), - h_align='center', - v_align='center', - scale=1.0, - text="💀Use at your own risk💀\n ⚠️discord account might get terminated⚠️", - maxwidth=200, - color=(1.00, 0.15, 0.15)) - - self._login_button = bui.buttonwidget( - parent=self.root_widget, - position=(120, 65), - size=(400, 80), - scale=0.58, - label=log_txt, - color=log_btn_colour, - on_activate_call=self.login, - autoselect=True) - - def _on_cancel_press(self) -> None: - self._transition_out() - - def _transition_out(self) -> None: - if not self._transitioning_out: - self._transitioning_out = True - bui.containerwidget(edit=self.root_widget, transition='out_scale') - - def on_bascenev1libup_cancel(self) -> None: - bui.getsound('swish').play() - self._transition_out() - - def login(self): - if not self.path.exists(): - json_data = { - 'login': bui.textwidget(query=self.email_widget), - 'password': bui.textwidget(query=self.password_widget), - 'undelete': False, - 'captcha_key': None, - 'login_source': None, - 'gift_code_sku_id': None, - } - headers = { - 'user-agent': "Mozilla/5.0", - 'content-type': "application/json", - } - - conn = http.client.HTTPSConnection("discord.com") - - payload = json.dumps(json_data) - # conn.request("POST", "/api/v9/auth/login", payload, headers) - # res = conn.getresponse().read() - - try: - conn.request("POST", "/api/v9/auth/login", payload, headers) - res = conn.getresponse().read() - token = json.loads(res)['token'].encode().hex().encode() - with open(self.path, 'wb') as f: - f.write(token) - bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) - bui.getsound('shieldUp').play() - self.on_bascenev1libup_cancel() - except: - bui.screenmessage("Incorrect credentials", (1.00, 0.15, 0.15)) - bui.getsound('error').play() - - conn.close() - else: - remove(self.path) - bui.getsound('shieldDown').play() - bui.screenmessage("Account successfully removed!!", (0.10, 0.10, 1.00)) - self.on_bascenev1libup_cancel() - ws.close() - - -run_once = False - - -def get_once_asset(): - global run_once - if run_once: - return - response = Request( - "https://discordapp.com/api/oauth2/applications/963434684669382696/assets", - headers={"User-Agent": "Mozilla/5.0"}, - ) - try: - with urlopen(response) as assets: - assets = json.loads(assets.read()) - asset = [] - asset_id = [] - for x in assets: - dem = x["name"] - don = x["id"] - asset_id.append(don) - asset.append(dem) - asset_id_dict = dict(zip(asset, asset_id)) - - with open(DIRPATH, "w") as imagesets: - jsonfile = json.dumps(asset_id_dict) - json.dump(asset_id_dict, imagesets, indent=4) - except: - pass - run_once = True - - -def get_class(): - if ANDROID: - return PresenceUpdate() - elif not ANDROID: - return RpcThread() - - -# ba_meta export babase.Plugin -class DiscordRP(babase.Plugin): - def __init__(self) -> None: - self.update_timer: bs.Timer | None = None - self.rpc_thread = get_class() - self._last_server_info: str | None = None - - if not ANDROID: - _run_overrides() - get_once_asset() - - def on_app_running(self) -> None: - if not ANDROID: - self.rpc_thread.start() - self.update_timer = bs.AppTimer( - 1, bs.WeakCall(self.update_status), repeat=True - ) - if ANDROID: - self.update_timer = bs.AppTimer( - 4, bs.WeakCall(self.update_status), repeat=True - ) - - def has_settings_ui(self): - return True - - def show_settings_ui(self, button): - if not ANDROID: - bui.screenmessage("Nothing here achievement!!!", (0.26, 0.65, 0.94)) - bui.getsound('achievement').play() - if ANDROID: - Discordlogin() - - def on_app_shutdown(self) -> None: - if not ANDROID and self.rpc_thread.is_discord_running(): - self.rpc_thread.rpc.close() - self.rpc_thread.should_close = True - # else: - # raise NotImplementedError("This function does not work on android") - # # stupid code - # # ws.close() - - # def on_app_pause(self) -> None: - # ws.close() - - # def on_app_resume(self) -> None: - # if Path(f"{getcwd()}/token.txt").exists(): - # threading.Thread(target=ws.run_forever, daemon=True, name="websocket").start() - - def _get_current_activity_name(self) -> str | None: - act = bs.get_foreground_host_activity() - if isinstance(act, bs.GameActivity): - return act.name - - this = "Lobby" - name: str | None = ( - act.__class__.__name__.replace("Activity", "") - .replace("ScoreScreen", "Ranking") - .replace("Coop", "") - .replace("MultiTeam", "") - .replace("Victory", "") - .replace("EndSession", "") - .replace("Transition", "") - .replace("Draw", "") - .replace("FreeForAll", "") - .replace("Join", this) - .replace("Team", "") - .replace("Series", "") - .replace("CustomSession", "Custom Session(mod)") - ) - - if name == "MainMenu": - name = "Main Menu" - if name == this: - self.rpc_thread.large_image_key = "lobby" - self.rpc_thread.large_image_text = "Bombing up" - #self.rpc_thread.small_image_key = "lobbysmall" - if name == "Ranking": - self.rpc_thread.large_image_key = "ranking" - self.rpc_thread.large_image_text = "Viewing Results" - return name - - def _get_current_map_name(self) -> Tuple[str | None, str | None]: - act = bs.get_foreground_host_activity() - if isinstance(act, bs.GameActivity): - texname = act.map.get_preview_texture_name() - if texname: - return act.map.name, texname.lower().removesuffix("preview") - return None, None - - def update_status(self) -> None: - roster = bs.get_game_roster() - connection_info = bs.get_connection_to_host_info() - - self.rpc_thread.large_image_key = "bombsquadicon" - self.rpc_thread.large_image_text = "BombSquad" - self.rpc_thread.small_image_key = _babase.app.classic.platform - self.rpc_thread.small_image_text = ( - f"{_babase.app.classic.platform.capitalize()}({_babase.app.version})" - ) - connection_info = bs.get_connection_to_host_info() - if not ANDROID: - svinfo = str(connection_info) - if self._last_server_info != svinfo: - self._last_server_info = svinfo - self.rpc_thread.party_id = str(uuid.uuid4()) - self.rpc_thread._update_secret() - if connection_info != {}: - servername = connection_info["name"] - self.rpc_thread.details = "Online" - self.rpc_thread.party_size = max( - 1, sum(len(client["players"]) for client in roster) - ) - self.rpc_thread.party_max = max(8, self.rpc_thread.party_size) - if len(servername) == 19 and "Private Party" in servername: - self.rpc_thread.state = "Private Party" - elif servername == "": # A local game joinable from the internet - try: - offlinename = json.loads(bs.get_game_roster()[0]["spec_string"])[ - "n" - ] - if len(offlinename) > 19: # Thanks Rikko - self.rpc_thread.state = offlinename[slice(19)] + "..." - else: - self.rpc_thread.state = offlinename - except IndexError: - pass - else: - if len(servername) > 19: - self.rpc_thread.state = servername[slice(19)] + ".." - else: - self.rpc_thread.state = servername[slice(19)] - - if connection_info == {}: - self.rpc_thread.details = "Local" # ! replace with something like ballistica github cause - self.rpc_thread.state = self._get_current_activity_name() - self.rpc_thread.party_size = max(1, len(roster)) - self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) - - if ( - bs.get_foreground_host_session() is not None - and self.rpc_thread.details == "Local" - ): - session = ( - bs.get_foreground_host_session() - .__class__.__name__.replace("MainMenuSession", "") - .replace("EndSession", "") - .replace("FreeForAllSession", ": FFA") # ! for session use small image key - .replace("DualTeamSession", ": Teams") - .replace("CoopSession", ": Coop") - ) - #! self.rpc_thread.small_image_key = session.lower() - self.rpc_thread.details = f"{self.rpc_thread.details} {session}" - if ( - self.rpc_thread.state == "NoneType" - ): # sometimes the game just breaks which means its not really watching replay FIXME - self.rpc_thread.state = "Watching Replay" - self.rpc_thread.large_image_key = "replay" - self.rpc_thread.large_image_text = "Viewing Awesomeness" - #!self.rpc_thread.small_image_key = "replaysmall" - - act = bs.get_foreground_host_activity() - session = bs.get_foreground_host_session() - if act: - from bascenev1lib.game.elimination import EliminationGame - from bascenev1lib.game.thelaststand import TheLastStandGame - from bascenev1lib.game.meteorshower import MeteorShowerGame - - # noinspection PyUnresolvedReferences,PyProtectedMember - try: - self.rpc_thread.start_timestamp = act._discordrp_start_time # type: ignore - except AttributeError: - # This can be the case if plugin launched AFTER activity - # has been created; in that case let's assume it was - # created just now. - self.rpc_thread.start_timestamp = act._discordrp_start_time = time.mktime( # type: ignore - time.localtime() - ) - if isinstance(act, EliminationGame): - alive_count = len([p for p in act.players if p.lives > 0]) - self.rpc_thread.details += f" ({alive_count} players left)" - elif isinstance(act, TheLastStandGame): - # noinspection PyProtectedMember - points = act._score - self.rpc_thread.details += f" ({points} points)" - elif isinstance(act, MeteorShowerGame): - with bs.ContextRef(act): - sec = bs.time() - act._timer.getstarttime() - secfmt = "" - if sec < 60: - secfmt = f"{sec:.2f}" - else: - secfmt = f"{int(sec) // 60:02}:{sec:.2f}" - self.rpc_thread.details += f" ({secfmt})" - - # if isinstance(session, ba.DualTeamSession): - # scores = ':'.join([ - # str(t.customdata['score']) - # for t in session.sessionteams - # ]) - # self.rpc_thread.details += f' ({scores})' - - mapname, short_map_name = self._get_current_map_name() - if mapname: - with open(DIRPATH, 'r') as asset_dict: - asset_keys = json.load(asset_dict).keys() - if short_map_name in asset_keys: - self.rpc_thread.large_image_text = mapname - self.rpc_thread.large_image_key = short_map_name - self.rpc_thread.small_image_key = 'bombsquadlogo2' - self.rpc_thread.small_image_text = 'BombSquad' - - if _babase.get_idle_time() / (1000 * 60) % 60 >= 0.4: - self.rpc_thread.details = f"AFK in {self.rpc_thread.details}" - if not ANDROID: - self.rpc_thread.large_image_key = ( - "https://media.tenor.com/uAqNn6fv7x4AAAAM/bombsquad-spaz.gif" - ) - if ANDROID and Path(f"{getcwd()}/token.txt").exists(): - self.rpc_thread.presence() From d9b25ea772a1396a67255678b010a0db55b30a97 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Sat, 22 Jul 2023 03:54:26 +0300 Subject: [PATCH 18/27] Add files via upload --- plugins/utilities/discord_richpresence.py | 927 ++++++++++++++++++++++ 1 file changed, 927 insertions(+) create mode 100644 plugins/utilities/discord_richpresence.py diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py new file mode 100644 index 00000000..2dee9f09 --- /dev/null +++ b/plugins/utilities/discord_richpresence.py @@ -0,0 +1,927 @@ +# Released under the MIT and Apache License. See LICENSE for details. +# +"""placeholder :clown:""" + +# ba_meta require api 8 +#!"Made to you by @brostos & @Dliwk" + + +from __future__ import annotations +from urllib.request import Request, urlopen, urlretrieve +from pathlib import Path +from os import getcwd, remove +from zipfile import ZipFile +from bauiv1lib.popup import PopupWindow +from babase._mgen.enums import TimeType + +import asyncio +import http.client +import ast +import uuid +import json +import time +import threading +import shutil +import babase +import _babase +import bascenev1 as bs +import bascenev1lib +import bauiv1 as bui + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Tuple + + +ANDROID = babase.app.classic.platform == "android" +DIRPATH = Path(f"{_babase.app.python_directory_user}/image_id.json") + +if ANDROID: # !can add ios in future + + # Installing websocket + def get_module(): + install_path = Path(f"{getcwd()}/ba_data/python") # For the guys like me on windows + path = Path(f"{install_path}/websocket.zip") + file_path = Path(f"{install_path}/websocket") + source_dir = Path(f"{install_path}/websocket-client-1.6.1/websocket") + if not file_path.exists(): + url = "https://github.com/websocket-client/websocket-client/archive/refs/tags/v1.6.1.zip" + try: + filename, headers = urlretrieve(url, filename=path) + with ZipFile(filename) as f: + f.extractall(install_path) + shutil.copytree(source_dir, file_path) + shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) + remove(path) + except Exception as e: + if type(e) == shutil.Error: + shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) + else: + pass + get_module() + + from websocket import WebSocketConnectionClosedException + import websocket + + + start_time = time.time() + + class PresenceUpdate: + def __init__(self): + self.ws = websocket.WebSocketApp("wss://gateway.discord.gg/?encoding=json&v=10", + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) + self.heartbeat_interval = int(41250) + self.resume_gateway_url: str | None = None + self.session_id: str | None = None + self.stop_heartbeat_thread = threading.Event() + self.do_once = True + self.state: str | None = "In Game" + self.details: str | None = "Main Menu" + self.start_timestamp = time.time() + self.large_image_key: str | None = "bombsquadicon" + self.large_image_text: str | None = "BombSquad Icon" + self.small_image_key: str | None = None + self.small_image_text: str | None = ( + f"{_babase.app.classic.platform.capitalize()}({_babase.app.version})") + self.media_proxy = "mp:/app-assets/963434684669382696/{}.png" + self.identify: bool = False + self.party_id: str = str(uuid.uuid4()) + self.party_size = 1 + self.party_max = 8 + + def presence(self): + with open(DIRPATH, "r") as maptxt: + largetxt = json.load(maptxt)[self.large_image_key] + with open(DIRPATH, "r") as maptxt: + smalltxt = json.load(maptxt)[self.small_image_key] + + presencepayload = { + "op": 3, + "d": { + "since": None, # used to show how long the user went idle will add afk to work with this and then set the status to idle + "status": "online", + "afk": "false", + "activities": [ + { + "name": "BombSquad", + "type": 0, + "application_id": "963434684669382696", + "state": self.state, + "details": self.details, + "timestamps": { + "start": start_time + }, + "party": { + "id": self.party_id, + "size": [self.party_size, self.party_max] + }, + "assets": { + "large_image": self.media_proxy.format(largetxt), + "large_text": self.large_image_text, + "small_image": self.media_proxy.format(smalltxt), + "small_text": self.small_image_text, + }, + "client_info": { + "version": 0, + "os": "android", + "client": "mobile", + }, + "buttons": ["Discord Server", "Download BombSquad"], + "metadata": { + "button_urls": [ + "https://discord.gg/bombsquad-ballistica-official-1001896771347304639", + "https://bombsquad-community.web.app/download", + ] + }, + } + ], + }, + } + try: + self.ws.send(json.dumps(presencepayload)) + except WebSocketConnectionClosedException: + pass + + def on_message(self, ws, message): + message = json.loads(message) + try: + self.heartbeat_interval = message["d"]["heartbeat_interval"] + except: + pass + try: + self.resume_gateway_url = message["d"]["resume_gateway_url"] + self.session_id = message["d"]["session_id"] + except: + pass + + def on_error(self, ws, error): + babase.print_exception(error) + + def on_close(self, ws, close_status_code, close_msg): + print("Closed Discord Connection Successfully") + + def on_open(self, ws): + print("Connected to Discord Websocket") + + def heartbeats(): + """Sending heartbeats to keep the connection alive""" + if self.do_once: + heartbeat_payload = { + "op": 1, + "d": 251, + } # step two keeping connection alive by sending heart beats and receiving opcode 11 + self.ws.send(json.dumps(heartbeat_payload)) + self.do_once = False + + def identify(): + """Identifying to the gateway and enable by using user token and the intents we will be using e.g 256->For Presence""" + with open(f"{getcwd()}/token.txt", 'r') as f: + token = bytes.fromhex(f.read()).decode('utf-8') + identify_payload = { + "op": 2, + "d": { + "token": token, + "properties": { + "os": "linux", + "browser": "Discord Android", + "device": "android", + }, + "intents": 256, + }, + } # step 3 send an identify + self.ws.send(json.dumps(identify_payload)) + identify() + while True: + heartbeat_payload = {"op": 1, "d": self.heartbeat_interval} + + try: + self.ws.send(json.dumps(heartbeat_payload)) + time.sleep(self.heartbeat_interval / 1000) + except: + pass + + if self.stop_heartbeat_thread.is_set(): + self.stop_heartbeat_thread.clear() + break + + threading.Thread(target=heartbeats, daemon=True, name="heartbeat").start() + + def start(self): + if Path(f"{getcwd()}/token.txt").exists(): + threading.Thread(target=self.ws.run_forever, daemon=True, name="websocket").start() + + def close(self): + self.stop_heartbeat_thread.set() + self.do_once = True + self.ws.close() + + + + + +if not ANDROID: + # installing pypresence + def get_module(): + install_path = Path(f"{getcwd()}/ba_data/python") + path = Path(f"{install_path}/pypresence.zip") + file_path = Path(f"{install_path}/pypresence") + source_dir = Path(f"{install_path}/pypresence-4.3.0/pypresence") + if not file_path.exists(): + url = "https://github.com/qwertyquerty/pypresence/archive/refs/tags/v4.3.0.zip" + try: + filename, headers = urlretrieve(url, filename=path) + with ZipFile(filename) as f: + f.extractall(install_path) + shutil.copytree(source_dir, file_path) + shutil.rmtree(Path(f"{install_path}/pypresence-4.3.0")) + remove(path) + except: + pass + + # Make modifications for it to work on windows + if babase.app.classic.platform == "windows": + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: + data = file.readlines() + data[45] = """ +def get_event_loop(force_fresh=False): + loop = asyncio.ProactorEventLoop() if sys.platform == 'win32' else asyncio.new_event_loop() + if force_fresh: + return loop + try: + running = asyncio.get_running_loop() + except RuntimeError: + return loop + if running.is_closed(): + return loop + else: + if sys.platform in ('linux', 'darwin'): + return running + if sys.platform == 'win32': + if isinstance(running, asyncio.ProactorEventLoop): + return running + else: + return loop""" + + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: + for number, line in enumerate(data): + if number not in range(46,56): + file.write(line) + get_module() + + + from pypresence import PipeClosed, DiscordError, DiscordNotFound + from pypresence.utils import get_event_loop + import pypresence + import socket + + DEBUG = True + + _last_server_addr = 'localhost' + _last_server_port = 43210 + + def print_error(err: str, include_exception: bool = False) -> None: + if DEBUG: + if include_exception: + babase.print_exception(err) + else: + babase.print_error(err) + else: + print(f"ERROR in discordrp.py: {err}") + + def log(msg: str) -> None: + if DEBUG: + print(f"LOG in discordrp.py: {msg}") + + def _run_overrides() -> None: + old_init = bs.Activity.__init__ + + def new_init(self, *args: Any, **kwargs: Any) -> None: # type: ignore + old_init(self, *args, **kwargs) + self._discordrp_start_time = time.mktime(time.localtime()) + + bs.Activity.__init__ = new_init # type: ignore + + old_connect = bs.connect_to_party + + def new_connect(*args, **kwargs) -> None: # type: ignore + global _last_server_addr + global _last_server_port + old_connect(*args, **kwargs) + c = kwargs.get("address") or args[0] + _last_server_port = kwargs.get("port") or args[1] + + bs.connect_to_party = new_connect + + start_time = time.time() + + class RpcThread(threading.Thread): + def __init__(self): + super().__init__(name="RpcThread") + self.rpc = pypresence.Presence(963434684669382696) + self.state: str | None = "In Game" + self.details: str | None = "Main Menu" + self.start_timestamp = time.mktime(time.localtime()) + self.large_image_key: str | None = "bombsquadicon" + self.large_image_text: str | None = "BombSquad Icon" + self.small_image_key: str | None = None + self.small_image_text: str | None = None + self.party_id: str = str(uuid.uuid4()) + self.party_size = 1 + self.party_max = 8 + self.join_secret: str | None = None + self._last_update_time: float = 0 + self._last_secret_update_time: float = 0 + self._last_connect_time: float = 0 + self.should_close = False + + @staticmethod + def is_discord_running(): + for i in range(6463,6473): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.01) + try: + conn = s.connect_ex(('localhost', i)) + s.close() + if (conn == 0): + s.close() + return(True) + except: + s.close() + return(False) + + def _generate_join_secret(self): + # resp = requests.get('https://legacy.ballistica.net/bsAccessCheck').text + connection_info = bs.get_connection_to_host_info() + if connection_info: + addr = _last_server_addr + port = _last_server_port + else: + try: + with urlopen( + "https://legacy.ballistica.net/bsAccessCheck" + ) as resp: + resp = resp.read().decode() + resp = ast.literal_eval(resp) + addr = resp["address"] + port = 43210 + secret_dict = { + "format_version": 1, + "hostname": addr, + "port": port, + } + self.join_secret = json.dumps(secret_dict) + except: + pass + + def _update_secret(self): + threading.Thread(target=self._generate_join_secret, daemon=True).start() + self._last_secret_update_time = time.time() + + def run(self) -> None: + asyncio.set_event_loop(get_event_loop()) + while not self.should_close: + if time.time() - self._last_update_time > 0.1: + self._do_update_presence() + if time.time() - self._last_secret_update_time > 15: + self._update_secret() + # if time.time() - self._last_connect_time > 120 and is_discord_running(): #!Eric please add module manager(pip) + # self._reconnect() + time.sleep(0.03) + + def _subscribe(self, event: str, **args): + self.rpc.send_data( + 1, + { + "nonce": f"{time.time():.20f}", + "cmd": "SUBSCRIBE", + "evt": event, + "args": args, + }, + ) + data = self.rpc.loop.run_until_complete(self.rpc.read_output()) + self.handle_event(data) + + def _subscribe_events(self): + self._subscribe("ACTIVITY_JOIN") + self._subscribe("ACTIVITY_JOIN_REQUEST") + + # def _update_presence(self) -> None: + # self._last_update_time = time.time() + # try: + # self._do_update_presence() + # except (AttributeError, AssertionError): + # try: + # self._reconnect() + # except Exception: + # print_error("failed to update presence", include_exception= True) + + + def _reconnect(self) -> None: + self.rpc.connect() + self._subscribe_events() + self._do_update_presence() + self._last_connect_time = time.time() + + def _do_update_presence(self) -> None: + if RpcThread.is_discord_running(): + self._last_update_time = time.time() + try: + data = self.rpc.update( + state=self.state or " ", + details=self.details, + start=start_time, + large_image=self.large_image_key, + large_text=self.large_image_text, + small_image=self.small_image_key, + small_text=self.small_image_text, + party_id=self.party_id, + party_size=[self.party_size, self.party_max], + join=self.join_secret, + # buttons = [ #!cant use buttons together with join + # { + # "label": "Discord Server", + # "url": "https://ballistica.net/discord" + # }, + # { + # "label": "Download Bombsquad", + # "url": "https://bombsquad.ga/download"} + # ] + ) + + self.handle_event(data) + except (PipeClosed, DiscordError, AssertionError, AttributeError): + try: + self._reconnect() + except (DiscordNotFound, DiscordError): + pass + + def handle_event(self, data): + evt = data["evt"] + if evt is None: + return + + data = data.get("data", {}) + + if evt == "ACTIVITY_JOIN": + secret = data.get("secret") + try: + server = json.loads(secret) + format_version = server["format_version"] + except Exception: + babase.print_exception("discordrp: unknown activity join format") + else: + try: + if format_version == 1: + hostname = server["hostname"] + port = server["port"] + self._connect_to_party(hostname, port) + except Exception: + babase.print_exception( + f"discordrp: incorrect activity join data, {format_version=}" + ) + + elif evt == "ACTIVITY_JOIN_REQUEST": + user = data.get("user", {}) + uid = user.get("id") + username = user.get("username") + discriminator = user.get("discriminator", None) + avatar = user.get("avatar") + self.on_join_request(username, uid, discriminator, avatar) + + def _connect_to_party(self, hostname, port) -> None: + babase.pushcall( + babase.Call(bs.connect_to_party, hostname, port), from_other_thread=True + ) + + def on_join_request(self, username, uid, discriminator, avatar) -> None: + del uid # unused + del avatar # unused + babase.pushcall( + babase.Call( + bui.screenmessage, + "Discord: {} wants to join!".format(username), + color=(0.0, 1.0, 0.0), + ), + from_other_thread=True, + ) + babase.pushcall(lambda: bui.getsound('bellMed').play(), from_other_thread=True) + + +class Discordlogin(PopupWindow): + + def __init__(self): + # pylint: disable=too-many-locals + _uiscale = bui.app.ui_v1.uiscale + self._transitioning_out = False + s = 1.25 if _uiscale is babase.UIScale.SMALL else 1.27 if _uiscale is babase.UIScale.MEDIUM else 1.3 + self._width = 380 * s + self._height = 150 + 150 * s + self.path = Path(f"{getcwd()}/token.txt") + bg_color = (0.5, 0.4, 0.6) + log_btn_colour = (0.10, 0.95, 0.10) if not self.path.exists() else (1.00, 0.15, 0.15) + log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" + + + + + # creates our _root_widget + PopupWindow.__init__(self, + position=(0.0, 0.0), + size=(self._width, self._height), + scale=(2.1 if _uiscale is babase.UIScale.SMALL else 1.5 + if _uiscale is babase.UIScale.MEDIUM else 1.0), + bg_color=bg_color) + + + self._cancel_button = bui.buttonwidget( + parent=self.root_widget, + position=(25, self._height - 40), + size=(50, 50), + scale=0.58, + label='', + color=bg_color, + on_activate_call=self._on_cancel_press, + autoselect=True, + icon=bui.gettexture('crossOut'), + iconscale=1.2) + + + + bui.imagewidget(parent=self.root_widget, + position=(180, self._height - 55), + size=(32 * s, 32 * s), + texture=bui.gettexture("discordLogo"), + color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) + + + + self.email_widget = bui.textwidget(parent=self.root_widget, + text="Email/Phone Number", + size=(400, 70), + position=(50, 180), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220) + + + self.password_widget = bui.textwidget(parent=self.root_widget, + text="Password", + size=(400, 70), + position=(50, 120), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220) + + + bui.containerwidget(edit=self.root_widget, + cancel_button=self._cancel_button) + + bui.textwidget( + parent=self.root_widget, + position=(265, self._height - 37), + size=(0, 0), + h_align='center', + v_align='center', + scale=1.0, + text="Discord", + maxwidth=200, + color=(0.80, 0.80, 0.80)) + + bui.textwidget( + parent=self.root_widget, + position=(265, self._height - 78), + size=(0, 0), + h_align='center', + v_align='center', + scale=1.0, + text="💀Use at your own risk💀\n ⚠️discord account might get terminated⚠️", + maxwidth=200, + color=(1.00, 0.15, 0.15)) + + + self._login_button = bui.buttonwidget( + parent=self.root_widget, + position=(120, 65), + size=(400, 80), + scale=0.58, + label=log_txt, + color=log_btn_colour, + on_activate_call=self.login, + autoselect=True) + + def _on_cancel_press(self) -> None: + self._transition_out() + + def _transition_out(self) -> None: + if not self._transitioning_out: + self._transitioning_out = True + bui.containerwidget(edit=self.root_widget, transition='out_scale') + + def on_bascenev1libup_cancel(self) -> None: + bui.getsound('swish').play() + self._transition_out() + + def login(self): + if not self.path.exists(): + json_data = { + 'login': bui.textwidget(query=self.email_widget), + 'password': bui.textwidget(query=self.password_widget), + 'undelete': False, + 'captcha_key': None, + 'login_source': None, + 'gift_code_sku_id': None, + } + headers = { + 'user-agent': "Mozilla/5.0", + 'content-type': "application/json", + } + + conn = http.client.HTTPSConnection("discord.com") + + payload = json.dumps(json_data) + # conn.request("POST", "/api/v9/auth/login", payload, headers) + # res = conn.getresponse().read() + + try: + conn.request("POST", "/api/v9/auth/login", payload, headers) + res = conn.getresponse().read() + token = json.loads(res)['token'].encode().hex().encode() + with open(self.path, 'wb') as f: + f.write(token) + bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) + bui.getsound('shieldUp').play() + self.on_bascenev1libup_cancel() + except: + bui.screenmessage("Incorrect credentials", (1.00, 0.15, 0.15)) + bui.getsound('error').play() + + conn.close() + else: + remove(self.path) + bui.getsound('shieldDown').play() + bui.screenmessage("Account successfully removed!!", (0.10, 0.10, 1.00)) + self.on_bascenev1libup_cancel() + PresenceUpdate().ws.close() + + +run_once = False +def get_once_asset(): + global run_once + if run_once: + return + response = Request( + "https://discordapp.com/api/oauth2/applications/963434684669382696/assets", + headers={"User-Agent": "Mozilla/5.0"}, + ) + try: + with urlopen(response) as assets: + assets = json.loads(assets.read()) + asset = [] + asset_id = [] + for x in assets: + dem = x["name"] + don = x["id"] + asset_id.append(don) + asset.append(dem) + asset_id_dict = dict(zip(asset, asset_id)) + + with open(DIRPATH, "w") as imagesets: + jsonfile = json.dumps(asset_id_dict) + json.dump(asset_id_dict, imagesets, indent=4) + except: + pass + run_once = True + +def get_class(): + if ANDROID: + return PresenceUpdate() + elif not ANDROID: + return RpcThread() + + +# ba_meta export babase.Plugin +class DiscordRP(babase.Plugin): + def __init__(self) -> None: + self.update_timer: bs.Timer | None = None + self.rpc_thread = get_class() + self._last_server_info: str | None = None + + if not ANDROID: + _run_overrides() + get_once_asset() + + def on_app_running(self) -> None: + if not ANDROID: + self.rpc_thread.start() + self.update_timer = bs.AppTimer( + 1, bs.WeakCall(self.update_status), repeat=True + ) + if ANDROID: + self.rpc_thread.start() + self.update_timer = bs.AppTimer( + 4, bs.WeakCall(self.update_status), repeat=True + ) + + def has_settings_ui(self): + return True + + def show_settings_ui(self, button): + if not ANDROID: + bui.screenmessage("Nothing here achievement!!!", (0.26, 0.65, 0.94)) + bui.getsound('achievement').play() + if ANDROID: + Discordlogin() + + def on_app_shutdown(self) -> None: + if not ANDROID and self.rpc_thread.is_discord_running(): + self.rpc_thread.rpc.close() + self.rpc_thread.should_close = True + + def on_app_pause(self) -> None: + self.rpc_thread.close() + + def on_app_resume(self) -> None: + self.rpc_thread.start() + + def _get_current_activity_name(self) -> str | None: + act = bs.get_foreground_host_activity() + if isinstance(act, bs.GameActivity): + return act.name + + this = "Lobby" + name: str | None = ( + act.__class__.__name__.replace("Activity", "") + .replace("ScoreScreen", "Ranking") + .replace("Coop", "") + .replace("MultiTeam", "") + .replace("Victory", "") + .replace("EndSession", "") + .replace("Transition", "") + .replace("Draw", "") + .replace("FreeForAll", "") + .replace("Join", this) + .replace("Team", "") + .replace("Series", "") + .replace("CustomSession", "Custom Session(mod)") + ) + + if name == "MainMenu": + name = "Main Menu" + if name == this: + self.rpc_thread.large_image_key = "lobby" + self.rpc_thread.large_image_text = "Bombing up" + #self.rpc_thread.small_image_key = "lobbysmall" + if name == "Ranking": + self.rpc_thread.large_image_key = "ranking" + self.rpc_thread.large_image_text = "Viewing Results" + return name + + def _get_current_map_name(self) -> Tuple[str | None, str | None]: + act = bs.get_foreground_host_activity() + if isinstance(act, bs.GameActivity): + texname = act.map.get_preview_texture_name() + if texname: + return act.map.name, texname.lower().removesuffix("preview") + return None, None + + def update_status(self) -> None: + roster = bs.get_game_roster() + connection_info = bs.get_connection_to_host_info() + + self.rpc_thread.large_image_key = "bombsquadicon" + self.rpc_thread.large_image_text = "BombSquad" + self.rpc_thread.small_image_key = _babase.app.classic.platform + self.rpc_thread.small_image_text = ( + f"{_babase.app.classic.platform.capitalize()}({_babase.app.version})" + ) + connection_info = bs.get_connection_to_host_info() + if not ANDROID: + svinfo = str(connection_info) + if self._last_server_info != svinfo: + self._last_server_info = svinfo + self.rpc_thread.party_id = str(uuid.uuid4()) + self.rpc_thread._update_secret() + if connection_info != {}: + servername = connection_info["name"] + self.rpc_thread.details = "Online" + self.rpc_thread.party_size = max( + 1, sum(len(client["players"]) for client in roster) + ) + self.rpc_thread.party_max = max(8, self.rpc_thread.party_size) + if len(servername) == 19 and "Private Party" in servername: + self.rpc_thread.state = "Private Party" + elif servername == "": # A local game joinable from the internet + try: + offlinename = json.loads(bs.get_game_roster()[0]["spec_string"])[ + "n" + ] + if len(offlinename) > 19: # Thanks Rikko + self.rpc_thread.state = offlinename[slice(19)] + "..." + else: + self.rpc_thread.state = offlinename + except IndexError: + pass + else: + if len(servername) > 19: + self.rpc_thread.state = servername[slice(19)] + ".." + else: + self.rpc_thread.state = servername[slice(19)] + + if connection_info == {}: + self.rpc_thread.details = "Local" #! replace with something like ballistica github cause + self.rpc_thread.state = self._get_current_activity_name() + self.rpc_thread.party_size = max(1, len(roster)) + self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) + + if ( + bs.get_foreground_host_session() is not None + and self.rpc_thread.details == "Local" + ): + session = ( + bs.get_foreground_host_session() + .__class__.__name__.replace("MainMenuSession", "") + .replace("EndSession", "") + .replace("FreeForAllSession", ": FFA") #! for session use small image key + .replace("DualTeamSession", ": Teams") + .replace("CoopSession", ": Coop") + ) + #! self.rpc_thread.small_image_key = session.lower() + self.rpc_thread.details = f"{self.rpc_thread.details} {session}" + if ( + self.rpc_thread.state == "NoneType" + ): # sometimes the game just breaks which means its not really watching replay FIXME + self.rpc_thread.state = "Watching Replay" + self.rpc_thread.large_image_key = "replay" + self.rpc_thread.large_image_text = "Viewing Awesomeness" + #!self.rpc_thread.small_image_key = "replaysmall" + + act = bs.get_foreground_host_activity() + session = bs.get_foreground_host_session() + if act: + from bascenev1lib.game.elimination import EliminationGame + from bascenev1lib.game.thelaststand import TheLastStandGame + from bascenev1lib.game.meteorshower import MeteorShowerGame + + # noinspection PyUnresolvedReferences,PyProtectedMember + try: + self.rpc_thread.start_timestamp = act._discordrp_start_time # type: ignore + except AttributeError: + # This can be the case if plugin launched AFTER activity + # has been created; in that case let's assume it was + # created just now. + self.rpc_thread.start_timestamp = act._discordrp_start_time = time.mktime( # type: ignore + time.localtime() + ) + if isinstance(act, EliminationGame): + alive_count = len([p for p in act.players if p.lives > 0]) + self.rpc_thread.details += f" ({alive_count} players left)" + elif isinstance(act, TheLastStandGame): + # noinspection PyProtectedMember + points = act._score + self.rpc_thread.details += f" ({points} points)" + elif isinstance(act, MeteorShowerGame): + with bs.ContextRef(act): + sec = bs.time() - act._timer.getstarttime() + secfmt = "" + if sec < 60: + secfmt = f"{sec:.2f}" + else: + secfmt = f"{int(sec) // 60:02}:{sec:.2f}" + self.rpc_thread.details += f" ({secfmt})" + + # if isinstance(session, ba.DualTeamSession): + # scores = ':'.join([ + # str(t.customdata['score']) + # for t in session.sessionteams + # ]) + # self.rpc_thread.details += f' ({scores})' + + mapname, short_map_name = self._get_current_map_name() + if mapname: + with open(DIRPATH, 'r') as asset_dict: + asset_keys = json.load(asset_dict).keys() + if short_map_name in asset_keys: + self.rpc_thread.large_image_text = mapname + self.rpc_thread.large_image_key = short_map_name + self.rpc_thread.small_image_key = 'bombsquadlogo2' + self.rpc_thread.small_image_text = 'BombSquad' + + if _babase.get_idle_time() / (1000 * 60) % 60 >= 0.4: + self.rpc_thread.details = f"AFK in {self.rpc_thread.details}" + if not ANDROID: + self.rpc_thread.large_image_key = ( + "https://media.tenor.com/uAqNn6fv7x4AAAAM/bombsquad-spaz.gif" + ) + if ANDROID and Path(f"{getcwd()}/token.txt").exists(): + self.rpc_thread.presence() + \ No newline at end of file From 5f2e53dc38d5c61a15c8047448617e2d57aa0f22 Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Sat, 22 Jul 2023 00:54:53 +0000 Subject: [PATCH 19/27] [ci] auto-format --- plugins/utilities/discord_richpresence.py | 153 ++++++++++------------ 1 file changed, 69 insertions(+), 84 deletions(-) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index 2dee9f09..e0042ed7 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -24,7 +24,7 @@ import shutil import babase import _babase -import bascenev1 as bs +import bascenev1 as bs import bascenev1lib import bauiv1 as bui @@ -64,16 +64,15 @@ def get_module(): from websocket import WebSocketConnectionClosedException import websocket - start_time = time.time() - + class PresenceUpdate: def __init__(self): self.ws = websocket.WebSocketApp("wss://gateway.discord.gg/?encoding=json&v=10", - on_open=self.on_open, - on_message=self.on_message, - on_error=self.on_error, - on_close=self.on_close) + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) self.heartbeat_interval = int(41250) self.resume_gateway_url: str | None = None self.session_id: str | None = None @@ -197,30 +196,27 @@ def identify(): identify() while True: heartbeat_payload = {"op": 1, "d": self.heartbeat_interval} - + try: self.ws.send(json.dumps(heartbeat_payload)) time.sleep(self.heartbeat_interval / 1000) except: pass - + if self.stop_heartbeat_thread.is_set(): self.stop_heartbeat_thread.clear() break - + threading.Thread(target=heartbeats, daemon=True, name="heartbeat").start() - + def start(self): if Path(f"{getcwd()}/token.txt").exists(): threading.Thread(target=self.ws.run_forever, daemon=True, name="websocket").start() - + def close(self): self.stop_heartbeat_thread.set() self.do_once = True self.ws.close() - - - if not ANDROID: @@ -241,7 +237,7 @@ def get_module(): remove(path) except: pass - + # Make modifications for it to work on windows if babase.app.classic.platform == "windows": with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: @@ -265,24 +261,23 @@ def get_event_loop(force_fresh=False): return running else: return loop""" - + with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: for number, line in enumerate(data): - if number not in range(46,56): + if number not in range(46, 56): file.write(line) get_module() - from pypresence import PipeClosed, DiscordError, DiscordNotFound from pypresence.utils import get_event_loop - import pypresence + import pypresence import socket - + DEBUG = True - + _last_server_addr = 'localhost' _last_server_port = 43210 - + def print_error(err: str, include_exception: bool = False) -> None: if DEBUG: if include_exception: @@ -313,8 +308,8 @@ def new_connect(*args, **kwargs) -> None: # type: ignore old_connect(*args, **kwargs) c = kwargs.get("address") or args[0] _last_server_port = kwargs.get("port") or args[1] - - bs.connect_to_party = new_connect + + bs.connect_to_party = new_connect start_time = time.time() @@ -337,10 +332,10 @@ def __init__(self): self._last_secret_update_time: float = 0 self._last_connect_time: float = 0 self.should_close = False - - @staticmethod + + @staticmethod def is_discord_running(): - for i in range(6463,6473): + for i in range(6463, 6473): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.01) try: @@ -348,11 +343,11 @@ def is_discord_running(): s.close() if (conn == 0): s.close() - return(True) + return (True) except: s.close() - return(False) - + return (False) + def _generate_join_secret(self): # resp = requests.get('https://legacy.ballistica.net/bsAccessCheck').text connection_info = bs.get_connection_to_host_info() @@ -409,7 +404,7 @@ def _subscribe_events(self): self._subscribe("ACTIVITY_JOIN") self._subscribe("ACTIVITY_JOIN_REQUEST") - # def _update_presence(self) -> None: + # def _update_presence(self) -> None: # self._last_update_time = time.time() # try: # self._do_update_presence() @@ -418,7 +413,6 @@ def _subscribe_events(self): # self._reconnect() # except Exception: # print_error("failed to update presence", include_exception= True) - def _reconnect(self) -> None: self.rpc.connect() @@ -495,7 +489,7 @@ def handle_event(self, data): def _connect_to_party(self, hostname, port) -> None: babase.pushcall( babase.Call(bs.connect_to_party, hostname, port), from_other_thread=True - ) + ) def on_join_request(self, username, uid, discriminator, avatar) -> None: del uid # unused @@ -523,10 +517,7 @@ def __init__(self): self.path = Path(f"{getcwd()}/token.txt") bg_color = (0.5, 0.4, 0.6) log_btn_colour = (0.10, 0.95, 0.10) if not self.path.exists() else (1.00, 0.15, 0.15) - log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" - - - + log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" # creates our _root_widget PopupWindow.__init__(self, @@ -535,7 +526,6 @@ def __init__(self): scale=(2.1 if _uiscale is babase.UIScale.SMALL else 1.5 if _uiscale is babase.UIScale.MEDIUM else 1.0), bg_color=bg_color) - self._cancel_button = bui.buttonwidget( parent=self.root_widget, @@ -548,44 +538,38 @@ def __init__(self): autoselect=True, icon=bui.gettexture('crossOut'), iconscale=1.2) - - - + bui.imagewidget(parent=self.root_widget, - position=(180, self._height - 55), - size=(32 * s, 32 * s), - texture=bui.gettexture("discordLogo"), - color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) - + position=(180, self._height - 55), + size=(32 * s, 32 * s), + texture=bui.gettexture("discordLogo"), + color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) - self.email_widget = bui.textwidget(parent=self.root_widget, - text="Email/Phone Number", - size=(400, 70), - position=(50, 180), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) - - + text="Email/Phone Number", + size=(400, 70), + position=(50, 180), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220) + self.password_widget = bui.textwidget(parent=self.root_widget, - text="Password", - size=(400, 70), - position=(50, 120), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) - - + text="Password", + size=(400, 70), + position=(50, 120), + h_align='left', + v_align='center', + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220) + bui.containerwidget(edit=self.root_widget, - cancel_button=self._cancel_button) - + cancel_button=self._cancel_button) + bui.textwidget( parent=self.root_widget, position=(265, self._height - 37), @@ -596,7 +580,7 @@ def __init__(self): text="Discord", maxwidth=200, color=(0.80, 0.80, 0.80)) - + bui.textwidget( parent=self.root_widget, position=(265, self._height - 78), @@ -607,8 +591,7 @@ def __init__(self): text="💀Use at your own risk💀\n ⚠️discord account might get terminated⚠️", maxwidth=200, color=(1.00, 0.15, 0.15)) - - + self._login_button = bui.buttonwidget( parent=self.root_widget, position=(120, 65), @@ -673,8 +656,10 @@ def login(self): self.on_bascenev1libup_cancel() PresenceUpdate().ws.close() - + run_once = False + + def get_once_asset(): global run_once if run_once: @@ -702,6 +687,7 @@ def get_once_asset(): pass run_once = True + def get_class(): if ANDROID: return PresenceUpdate() @@ -722,7 +708,7 @@ def __init__(self) -> None: def on_app_running(self) -> None: if not ANDROID: - self.rpc_thread.start() + self.rpc_thread.start() self.update_timer = bs.AppTimer( 1, bs.WeakCall(self.update_status), repeat=True ) @@ -749,15 +735,15 @@ def on_app_shutdown(self) -> None: def on_app_pause(self) -> None: self.rpc_thread.close() - + def on_app_resume(self) -> None: self.rpc_thread.start() - + def _get_current_activity_name(self) -> str | None: act = bs.get_foreground_host_activity() if isinstance(act, bs.GameActivity): return act.name - + this = "Lobby" name: str | None = ( act.__class__.__name__.replace("Activity", "") @@ -825,7 +811,7 @@ def update_status(self) -> None: offlinename = json.loads(bs.get_game_roster()[0]["spec_string"])[ "n" ] - if len(offlinename) > 19: # Thanks Rikko + if len(offlinename) > 19: # Thanks Rikko self.rpc_thread.state = offlinename[slice(19)] + "..." else: self.rpc_thread.state = offlinename @@ -838,7 +824,7 @@ def update_status(self) -> None: self.rpc_thread.state = servername[slice(19)] if connection_info == {}: - self.rpc_thread.details = "Local" #! replace with something like ballistica github cause + self.rpc_thread.details = "Local" # ! replace with something like ballistica github cause self.rpc_thread.state = self._get_current_activity_name() self.rpc_thread.party_size = max(1, len(roster)) self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) @@ -851,7 +837,7 @@ def update_status(self) -> None: bs.get_foreground_host_session() .__class__.__name__.replace("MainMenuSession", "") .replace("EndSession", "") - .replace("FreeForAllSession", ": FFA") #! for session use small image key + .replace("FreeForAllSession", ": FFA") # ! for session use small image key .replace("DualTeamSession", ": Teams") .replace("CoopSession", ": Coop") ) @@ -924,4 +910,3 @@ def update_status(self) -> None: ) if ANDROID and Path(f"{getcwd()}/token.txt").exists(): self.rpc_thread.presence() - \ No newline at end of file From 7c0740904979a0580454c49513e3d5249ce0b11e Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Sat, 22 Jul 2023 03:57:07 +0300 Subject: [PATCH 20/27] hopefully final --- plugins/utilities.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 5142fd43..95557ec7 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,12 +767,7 @@ } ], "versions": { - "1.1.0": { - "api_version": 8, - "commit_sha": "8f02db1", - "released_on": "21-07-2023", - "md5sum": "402a406cab00e6fc6687738d237aeda6" - }, + "1.1.0": null, "1.0.0": { "api_version": 8, "commit_sha": "230d12d", @@ -782,4 +777,4 @@ } } } -} \ No newline at end of file +} From cb862471bf3bd0367781d8479820147eb6257a5c Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Sat, 22 Jul 2023 00:57:33 +0000 Subject: [PATCH 21/27] [ci] apply-version-metadata --- plugins/utilities.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 95557ec7..3c2240ca 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,7 +767,12 @@ } ], "versions": { - "1.1.0": null, + "1.1.0": { + "api_version": 8, + "commit_sha": "7c07409", + "released_on": "22-07-2023", + "md5sum": "2313a4a4939508ea4a907c8f6d23d96c" + }, "1.0.0": { "api_version": 8, "commit_sha": "230d12d", @@ -777,4 +782,4 @@ } } } -} +} \ No newline at end of file From c74b47009e8fc6e624d54b9b9692fb182fee68e1 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:44:09 +0300 Subject: [PATCH 22/27] Fixed time on mobile rich presence --- plugins/utilities/discord_richpresence.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index e0042ed7..a1893293 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -737,6 +737,8 @@ def on_app_pause(self) -> None: self.rpc_thread.close() def on_app_resume(self) -> None: + global start_time + start_time = time.time() self.rpc_thread.start() def _get_current_activity_name(self) -> str | None: From 88bb90aa92dbdaf7bc96ec84a17e54e124cadd64 Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Sat, 22 Jul 2023 13:44:40 +0000 Subject: [PATCH 23/27] [ci] auto-format --- plugins/utilities/discord_richpresence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index a1893293..5b01fdb9 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -737,7 +737,7 @@ def on_app_pause(self) -> None: self.rpc_thread.close() def on_app_resume(self) -> None: - global start_time + global start_time start_time = time.time() self.rpc_thread.start() From 01f48c58a6b6f2442d3970289d1ffb3e627827cc Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Mon, 24 Jul 2023 01:20:12 +0300 Subject: [PATCH 24/27] md5 --- plugins/utilities/discord_richpresence.py | 30 +++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index 5b01fdb9..558af75c 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -10,7 +10,6 @@ from urllib.request import Request, urlopen, urlretrieve from pathlib import Path from os import getcwd, remove -from zipfile import ZipFile from bauiv1lib.popup import PopupWindow from babase._mgen.enums import TimeType @@ -22,6 +21,7 @@ import time import threading import shutil +import hashlib import babase import _babase import bascenev1 as bs @@ -42,17 +42,19 @@ # Installing websocket def get_module(): install_path = Path(f"{getcwd()}/ba_data/python") # For the guys like me on windows - path = Path(f"{install_path}/websocket.zip") + path = Path(f"{install_path}/websocket.tar.gz") file_path = Path(f"{install_path}/websocket") source_dir = Path(f"{install_path}/websocket-client-1.6.1/websocket") if not file_path.exists(): - url = "https://github.com/websocket-client/websocket-client/archive/refs/tags/v1.6.1.zip" + url = "https://files.pythonhosted.org/packages/b1/34/3a5cae1e07d9566ad073fa6d169bf22c03a3ba7b31b3c3422ec88d039108/websocket-client-1.6.1.tar.gz" try: filename, headers = urlretrieve(url, filename=path) - with ZipFile(filename) as f: - f.extractall(install_path) - shutil.copytree(source_dir, file_path) - shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) + with open(filename, "rb") as f: + content = f.read() + assert hashlib.md5(content).hexdigest() == "86bc69b61947943627afc1b351c0b5db" + shutil.unpack_archive( filename, install_path) + shutil.copytree(source_dir, file_path) + shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) remove(path) except Exception as e: if type(e) == shutil.Error: @@ -223,17 +225,19 @@ def close(self): # installing pypresence def get_module(): install_path = Path(f"{getcwd()}/ba_data/python") - path = Path(f"{install_path}/pypresence.zip") + path = Path(f"{install_path}/pypresence.tar.gz") file_path = Path(f"{install_path}/pypresence") source_dir = Path(f"{install_path}/pypresence-4.3.0/pypresence") if not file_path.exists(): - url = "https://github.com/qwertyquerty/pypresence/archive/refs/tags/v4.3.0.zip" + url = "https://files.pythonhosted.org/packages/f4/2e/d110f862720b5e3ba1b0b719657385fc4151929befa2c6981f48360aa480/pypresence-4.3.0.tar.gz" try: filename, headers = urlretrieve(url, filename=path) - with ZipFile(filename) as f: - f.extractall(install_path) - shutil.copytree(source_dir, file_path) - shutil.rmtree(Path(f"{install_path}/pypresence-4.3.0")) + with open(filename, "rb") as f: + content = f.read() + assert hashlib.md5(content).hexdigest() == "f7c163cdd001af2456c09e241b90bad7" + shutil.unpack_archive( filename, install_path) + shutil.copytree(source_dir, file_path) + shutil.rmtree(Path(f"{install_path}/pypresence-4.3.0")) remove(path) except: pass From e32326d3ab80a0221c5a54632e0a5ef3fb503778 Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Sun, 23 Jul 2023 22:20:46 +0000 Subject: [PATCH 25/27] [ci] auto-format --- plugins/utilities/discord_richpresence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index 558af75c..383c5a08 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -52,7 +52,7 @@ def get_module(): with open(filename, "rb") as f: content = f.read() assert hashlib.md5(content).hexdigest() == "86bc69b61947943627afc1b351c0b5db" - shutil.unpack_archive( filename, install_path) + shutil.unpack_archive(filename, install_path) shutil.copytree(source_dir, file_path) shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) remove(path) @@ -235,7 +235,7 @@ def get_module(): with open(filename, "rb") as f: content = f.read() assert hashlib.md5(content).hexdigest() == "f7c163cdd001af2456c09e241b90bad7" - shutil.unpack_archive( filename, install_path) + shutil.unpack_archive(filename, install_path) shutil.copytree(source_dir, file_path) shutil.rmtree(Path(f"{install_path}/pypresence-4.3.0")) remove(path) From 90fff9b8a029eac1ee6e938955655e8c3e6008f1 Mon Sep 17 00:00:00 2001 From: brostos <67740566+brostosjoined@users.noreply.github.com> Date: Mon, 24 Jul 2023 01:20:55 +0300 Subject: [PATCH 26/27] Update utilities.json --- plugins/utilities.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 3c2240ca..95557ec7 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,12 +767,7 @@ } ], "versions": { - "1.1.0": { - "api_version": 8, - "commit_sha": "7c07409", - "released_on": "22-07-2023", - "md5sum": "2313a4a4939508ea4a907c8f6d23d96c" - }, + "1.1.0": null, "1.0.0": { "api_version": 8, "commit_sha": "230d12d", @@ -782,4 +777,4 @@ } } } -} \ No newline at end of file +} From da69d3d9e73e49fffcfc763a018b048cdf4a3be0 Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Sun, 23 Jul 2023 22:21:26 +0000 Subject: [PATCH 27/27] [ci] apply-version-metadata --- plugins/utilities.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/utilities.json b/plugins/utilities.json index 95557ec7..ceb32a85 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -767,7 +767,12 @@ } ], "versions": { - "1.1.0": null, + "1.1.0": { + "api_version": 8, + "commit_sha": "90fff9b", + "released_on": "23-07-2023", + "md5sum": "69723f76a0114fe99d6c85715ad4eb49" + }, "1.0.0": { "api_version": 8, "commit_sha": "230d12d", @@ -777,4 +782,4 @@ } } } -} +} \ No newline at end of file