From 47305d612a568a6800419daa18dafe6763dab063 Mon Sep 17 00:00:00 2001 From: Nyo Date: Tue, 19 Apr 2016 19:40:59 +0200 Subject: [PATCH] Moved pep.py to another repo --- __pycache__/actions.cpython-35.pyc | Bin 0 -> 492 bytes __pycache__/banchoConfig.cpython-35.pyc | Bin 0 -> 2059 bytes __pycache__/bcolors.cpython-35.pyc | Bin 0 -> 325 bytes __pycache__/cantSpectateEvent.cpython-35.pyc | Bin 0 -> 740 bytes __pycache__/changeActionEvent.cpython-35.pyc | Bin 0 -> 802 bytes .../changeMatchModsEvent.cpython-35.pyc | Bin 0 -> 910 bytes .../changeMatchPasswordEvent.cpython-35.pyc | Bin 0 -> 490 bytes .../changeMatchSettingsEvent.cpython-35.pyc | Bin 0 -> 2478 bytes __pycache__/changeSlotEvent.cpython-35.pyc | Bin 0 -> 531 bytes __pycache__/channel.cpython-35.pyc | Bin 0 -> 2133 bytes __pycache__/channelJoinEvent.cpython-35.pyc | Bin 0 -> 1531 bytes __pycache__/channelList.cpython-35.pyc | Bin 0 -> 1518 bytes __pycache__/channelPartEvent.cpython-35.pyc | Bin 0 -> 1017 bytes __pycache__/clientPackets.cpython-35.pyc | Bin 0 -> 4278 bytes __pycache__/config.cpython-35.pyc | Bin 0 -> 2619 bytes __pycache__/consoleHelper.cpython-35.pyc | Bin 0 -> 2482 bytes __pycache__/countryHelper.cpython-35.pyc | Bin 0 -> 4870 bytes __pycache__/createMatchEvent.cpython-35.pyc | Bin 0 -> 1131 bytes __pycache__/crypt.cpython-35.pyc | Bin 0 -> 7726 bytes __pycache__/dataTypes.cpython-35.pyc | Bin 0 -> 374 bytes __pycache__/databaseHelper.cpython-35.pyc | Bin 0 -> 3458 bytes __pycache__/exceptions.cpython-35.pyc | Bin 0 -> 3159 bytes __pycache__/fokabot.cpython-35.pyc | Bin 0 -> 1625 bytes __pycache__/fokabotCommands.cpython-35.pyc | Bin 0 -> 8430 bytes __pycache__/friendAddEvent.cpython-35.pyc | Bin 0 -> 505 bytes __pycache__/friendRemoveEvent.cpython-35.pyc | Bin 0 -> 515 bytes __pycache__/gameModes.cpython-35.pyc | Bin 0 -> 645 bytes __pycache__/generalFunctions.cpython-35.pyc | Bin 0 -> 825 bytes __pycache__/glob.cpython-35.pyc | Bin 0 -> 420 bytes __pycache__/joinLobbyEvent.cpython-35.pyc | Bin 0 -> 640 bytes __pycache__/joinMatchEvent.cpython-35.pyc | Bin 0 -> 1790 bytes __pycache__/locationHelper.cpython-35.pyc | Bin 0 -> 1198 bytes __pycache__/loginEvent.cpython-35.pyc | Bin 0 -> 3742 bytes __pycache__/logoutEvent.cpython-35.pyc | Bin 0 -> 945 bytes __pycache__/match.cpython-35.pyc | Bin 0 -> 16466 bytes __pycache__/matchBeatmapEvent.cpython-35.pyc | Bin 0 -> 454 bytes .../matchChangeTeamEvent.cpython-35.pyc | Bin 0 -> 436 bytes __pycache__/matchCompleteEvent.cpython-35.pyc | Bin 0 -> 448 bytes __pycache__/matchFailedEvent.cpython-35.pyc | Bin 0 -> 434 bytes __pycache__/matchFramesEvent.cpython-35.pyc | Bin 0 -> 783 bytes .../matchHasBeatmapEvent.cpython-35.pyc | Bin 0 -> 319 bytes __pycache__/matchInviteEvent.cpython-35.pyc | Bin 0 -> 513 bytes __pycache__/matchList.cpython-35.pyc | Bin 0 -> 2341 bytes __pycache__/matchLockEvent.cpython-35.pyc | Bin 0 -> 551 bytes __pycache__/matchModModes.cpython-35.pyc | Bin 0 -> 169 bytes .../matchNoBeatmapEvent.cpython-35.pyc | Bin 0 -> 318 bytes .../matchPlayerLoadEvent.cpython-35.pyc | Bin 0 -> 447 bytes __pycache__/matchReadyEvent.cpython-35.pyc | Bin 0 -> 460 bytes __pycache__/matchScoringTypes.cpython-35.pyc | Bin 0 -> 193 bytes __pycache__/matchSkipEvent.cpython-35.pyc | Bin 0 -> 439 bytes __pycache__/matchStartEvent.cpython-35.pyc | Bin 0 -> 1021 bytes __pycache__/matchTeamTypes.cpython-35.pyc | Bin 0 -> 219 bytes __pycache__/matchTeams.cpython-35.pyc | Bin 0 -> 181 bytes .../matchTransferHostEvent.cpython-35.pyc | Bin 0 -> 488 bytes __pycache__/mods.cpython-35.pyc | Bin 0 -> 750 bytes __pycache__/osuToken.cpython-35.pyc | Bin 0 -> 6477 bytes __pycache__/packetHelper.cpython-35.pyc | Bin 0 -> 4809 bytes __pycache__/packetIDs.cpython-35.pyc | Bin 0 -> 4034 bytes __pycache__/partLobbyEvent.cpython-35.pyc | Bin 0 -> 589 bytes __pycache__/partMatchEvent.cpython-35.pyc | Bin 0 -> 547 bytes __pycache__/passwordHelper.cpython-35.pyc | Bin 0 -> 1158 bytes __pycache__/responseHelper.cpython-35.pyc | Bin 0 -> 2265 bytes .../sendPrivateMessageEvent.cpython-35.pyc | Bin 0 -> 1402 bytes .../sendPublicMessageEvent.cpython-35.pyc | Bin 0 -> 2444 bytes __pycache__/serverPackets.cpython-35.pyc | Bin 0 -> 10227 bytes .../setAwayMessageEvent.cpython-35.pyc | Bin 0 -> 687 bytes __pycache__/slotStatuses.cpython-35.pyc | Bin 0 -> 299 bytes .../spectateFramesEvent.cpython-35.pyc | Bin 0 -> 882 bytes .../startSpectatingEvent.cpython-35.pyc | Bin 0 -> 1333 bytes .../stopSpectatingEvent.cpython-35.pyc | Bin 0 -> 995 bytes __pycache__/systemHelper.cpython-35.pyc | Bin 0 -> 2741 bytes __pycache__/tokenList.cpython-35.pyc | Bin 0 -> 4092 bytes __pycache__/userHelper.cpython-35.pyc | Bin 0 -> 7295 bytes __pycache__/userRanks.cpython-35.pyc | Bin 0 -> 312 bytes actions.py | 17 + banchoConfig.py | 42 ++ bcolors.py | 9 + cantSpectateEvent.py | 21 + changeActionEvent.py | 26 + changeMatchModsEvent.py | 43 ++ changeMatchPasswordEvent.py | 17 + changeMatchSettingsEvent.py | 109 +++ changeSlotEvent.py | 18 + channel.py | 78 +++ channelJoinEvent.py | 56 ++ channelList.py | 40 ++ channelPartEvent.py | 36 + clientPackets.py | 143 ++++ config.ini | 24 + config.py | 107 +++ consoleHelper.py | 71 ++ countryHelper.py | 282 ++++++++ createMatchEvent.py | 44 ++ crypt.py | 302 ++++++++ dataTypes.py | 12 + databaseHelper.py | 137 ++++ exceptions.py | 58 ++ fokabot.py | 55 ++ fokabotCommands.py | 355 ++++++++++ friendAddEvent.py | 10 + friendRemoveEvent.py | 10 + gameModes.py | 23 + generalFunctions.py | 22 + glob.py | 16 + joinLobbyEvent.py | 19 + joinMatchEvent.py | 60 ++ locationHelper.py | 48 ++ loginEvent.py | 172 +++++ logoutEvent.py | 39 ++ match.py | 656 ++++++++++++++++++ matchBeatmapEvent.py | 22 + matchChangeTeamEvent.py | 22 + matchCompleteEvent.py | 22 + matchFailedEvent.py | 22 + matchFramesEvent.py | 35 + matchHasBeatmapEvent.py | 3 + matchInviteEvent.py | 24 + matchList.py | 80 +++ matchLockEvent.py | 23 + matchModModes.py | 2 + matchNoBeatmapEvent.py | 3 + matchPlayerLoadEvent.py | 22 + matchReadyEvent.py | 16 + matchScoringTypes.py | 3 + matchSkipEvent.py | 22 + matchStartEvent.py | 47 ++ matchTeamTypes.py | 4 + matchTeams.py | 3 + matchTransferHostEvent.py | 23 + mods.py | 30 + osuToken.py | 227 ++++++ packetHelper.py | 249 +++++++ packetIDs.py | 111 +++ packets.txt | 110 +++ partLobbyEvent.py | 18 + partMatchEvent.py | 27 + passwordHelper.py | 36 + pep.py | 352 ++++++++++ responseHelper.py | 47 ++ routes.py | 52 ++ runserver.bat | 4 + sendPrivateMessageEvent.py | 47 ++ sendPublicMessageEvent.py | 108 +++ serverPackets.py | 273 ++++++++ setAwayMessageEvent.py | 20 + slotStatuses.py | 8 + spectateFramesEvent.py | 33 + startSpectatingEvent.py | 51 ++ stopSpectatingEvent.py | 31 + systemHelper.py | 91 +++ tokenList.py | 165 +++++ userHelper.py | 268 +++++++ userRanks.py | 9 + 153 files changed, 5942 insertions(+) create mode 100644 __pycache__/actions.cpython-35.pyc create mode 100644 __pycache__/banchoConfig.cpython-35.pyc create mode 100644 __pycache__/bcolors.cpython-35.pyc create mode 100644 __pycache__/cantSpectateEvent.cpython-35.pyc create mode 100644 __pycache__/changeActionEvent.cpython-35.pyc create mode 100644 __pycache__/changeMatchModsEvent.cpython-35.pyc create mode 100644 __pycache__/changeMatchPasswordEvent.cpython-35.pyc create mode 100644 __pycache__/changeMatchSettingsEvent.cpython-35.pyc create mode 100644 __pycache__/changeSlotEvent.cpython-35.pyc create mode 100644 __pycache__/channel.cpython-35.pyc create mode 100644 __pycache__/channelJoinEvent.cpython-35.pyc create mode 100644 __pycache__/channelList.cpython-35.pyc create mode 100644 __pycache__/channelPartEvent.cpython-35.pyc create mode 100644 __pycache__/clientPackets.cpython-35.pyc create mode 100644 __pycache__/config.cpython-35.pyc create mode 100644 __pycache__/consoleHelper.cpython-35.pyc create mode 100644 __pycache__/countryHelper.cpython-35.pyc create mode 100644 __pycache__/createMatchEvent.cpython-35.pyc create mode 100644 __pycache__/crypt.cpython-35.pyc create mode 100644 __pycache__/dataTypes.cpython-35.pyc create mode 100644 __pycache__/databaseHelper.cpython-35.pyc create mode 100644 __pycache__/exceptions.cpython-35.pyc create mode 100644 __pycache__/fokabot.cpython-35.pyc create mode 100644 __pycache__/fokabotCommands.cpython-35.pyc create mode 100644 __pycache__/friendAddEvent.cpython-35.pyc create mode 100644 __pycache__/friendRemoveEvent.cpython-35.pyc create mode 100644 __pycache__/gameModes.cpython-35.pyc create mode 100644 __pycache__/generalFunctions.cpython-35.pyc create mode 100644 __pycache__/glob.cpython-35.pyc create mode 100644 __pycache__/joinLobbyEvent.cpython-35.pyc create mode 100644 __pycache__/joinMatchEvent.cpython-35.pyc create mode 100644 __pycache__/locationHelper.cpython-35.pyc create mode 100644 __pycache__/loginEvent.cpython-35.pyc create mode 100644 __pycache__/logoutEvent.cpython-35.pyc create mode 100644 __pycache__/match.cpython-35.pyc create mode 100644 __pycache__/matchBeatmapEvent.cpython-35.pyc create mode 100644 __pycache__/matchChangeTeamEvent.cpython-35.pyc create mode 100644 __pycache__/matchCompleteEvent.cpython-35.pyc create mode 100644 __pycache__/matchFailedEvent.cpython-35.pyc create mode 100644 __pycache__/matchFramesEvent.cpython-35.pyc create mode 100644 __pycache__/matchHasBeatmapEvent.cpython-35.pyc create mode 100644 __pycache__/matchInviteEvent.cpython-35.pyc create mode 100644 __pycache__/matchList.cpython-35.pyc create mode 100644 __pycache__/matchLockEvent.cpython-35.pyc create mode 100644 __pycache__/matchModModes.cpython-35.pyc create mode 100644 __pycache__/matchNoBeatmapEvent.cpython-35.pyc create mode 100644 __pycache__/matchPlayerLoadEvent.cpython-35.pyc create mode 100644 __pycache__/matchReadyEvent.cpython-35.pyc create mode 100644 __pycache__/matchScoringTypes.cpython-35.pyc create mode 100644 __pycache__/matchSkipEvent.cpython-35.pyc create mode 100644 __pycache__/matchStartEvent.cpython-35.pyc create mode 100644 __pycache__/matchTeamTypes.cpython-35.pyc create mode 100644 __pycache__/matchTeams.cpython-35.pyc create mode 100644 __pycache__/matchTransferHostEvent.cpython-35.pyc create mode 100644 __pycache__/mods.cpython-35.pyc create mode 100644 __pycache__/osuToken.cpython-35.pyc create mode 100644 __pycache__/packetHelper.cpython-35.pyc create mode 100644 __pycache__/packetIDs.cpython-35.pyc create mode 100644 __pycache__/partLobbyEvent.cpython-35.pyc create mode 100644 __pycache__/partMatchEvent.cpython-35.pyc create mode 100644 __pycache__/passwordHelper.cpython-35.pyc create mode 100644 __pycache__/responseHelper.cpython-35.pyc create mode 100644 __pycache__/sendPrivateMessageEvent.cpython-35.pyc create mode 100644 __pycache__/sendPublicMessageEvent.cpython-35.pyc create mode 100644 __pycache__/serverPackets.cpython-35.pyc create mode 100644 __pycache__/setAwayMessageEvent.cpython-35.pyc create mode 100644 __pycache__/slotStatuses.cpython-35.pyc create mode 100644 __pycache__/spectateFramesEvent.cpython-35.pyc create mode 100644 __pycache__/startSpectatingEvent.cpython-35.pyc create mode 100644 __pycache__/stopSpectatingEvent.cpython-35.pyc create mode 100644 __pycache__/systemHelper.cpython-35.pyc create mode 100644 __pycache__/tokenList.cpython-35.pyc create mode 100644 __pycache__/userHelper.cpython-35.pyc create mode 100644 __pycache__/userRanks.cpython-35.pyc create mode 100644 actions.py create mode 100644 banchoConfig.py create mode 100644 bcolors.py create mode 100644 cantSpectateEvent.py create mode 100644 changeActionEvent.py create mode 100644 changeMatchModsEvent.py create mode 100644 changeMatchPasswordEvent.py create mode 100644 changeMatchSettingsEvent.py create mode 100644 changeSlotEvent.py create mode 100644 channel.py create mode 100644 channelJoinEvent.py create mode 100644 channelList.py create mode 100644 channelPartEvent.py create mode 100644 clientPackets.py create mode 100644 config.ini create mode 100644 config.py create mode 100644 consoleHelper.py create mode 100644 countryHelper.py create mode 100644 createMatchEvent.py create mode 100644 crypt.py create mode 100644 dataTypes.py create mode 100644 databaseHelper.py create mode 100644 exceptions.py create mode 100644 fokabot.py create mode 100644 fokabotCommands.py create mode 100644 friendAddEvent.py create mode 100644 friendRemoveEvent.py create mode 100644 gameModes.py create mode 100644 generalFunctions.py create mode 100644 glob.py create mode 100644 joinLobbyEvent.py create mode 100644 joinMatchEvent.py create mode 100644 locationHelper.py create mode 100644 loginEvent.py create mode 100644 logoutEvent.py create mode 100644 match.py create mode 100644 matchBeatmapEvent.py create mode 100644 matchChangeTeamEvent.py create mode 100644 matchCompleteEvent.py create mode 100644 matchFailedEvent.py create mode 100644 matchFramesEvent.py create mode 100644 matchHasBeatmapEvent.py create mode 100644 matchInviteEvent.py create mode 100644 matchList.py create mode 100644 matchLockEvent.py create mode 100644 matchModModes.py create mode 100644 matchNoBeatmapEvent.py create mode 100644 matchPlayerLoadEvent.py create mode 100644 matchReadyEvent.py create mode 100644 matchScoringTypes.py create mode 100644 matchSkipEvent.py create mode 100644 matchStartEvent.py create mode 100644 matchTeamTypes.py create mode 100644 matchTeams.py create mode 100644 matchTransferHostEvent.py create mode 100644 mods.py create mode 100644 osuToken.py create mode 100644 packetHelper.py create mode 100644 packetIDs.py create mode 100644 packets.txt create mode 100644 partLobbyEvent.py create mode 100644 partMatchEvent.py create mode 100644 passwordHelper.py create mode 100644 pep.py create mode 100644 responseHelper.py create mode 100644 routes.py create mode 100644 runserver.bat create mode 100644 sendPrivateMessageEvent.py create mode 100644 sendPublicMessageEvent.py create mode 100644 serverPackets.py create mode 100644 setAwayMessageEvent.py create mode 100644 slotStatuses.py create mode 100644 spectateFramesEvent.py create mode 100644 startSpectatingEvent.py create mode 100644 stopSpectatingEvent.py create mode 100644 systemHelper.py create mode 100644 tokenList.py create mode 100644 userHelper.py create mode 100644 userRanks.py diff --git a/__pycache__/actions.cpython-35.pyc b/__pycache__/actions.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..afe0fc4dd8556008f74242c28843ea8bc5d2df7a GIT binary patch literal 492 zcmZ|L&1xGl5C`zT?e%`yYrBW|7z{nd5JL~Wgir$YxtBl>gTbtK71&=@@@E9}(XP^u`0ZqVD&=fqwYCvg|n zx&Hyvu&j2w6OG?5vMS5eeJz+oW{eS)etxz-wHsz@-CY{FlJcul|LHU=SJkW;9XR#hJZ8!4ZlW zSbJhz0gJ?{z-qwiz#71sMkZAl+r<_z7hdDNXs`mzGuMvMvJGst(DBF`UcR1tlTbv* za!-mlna;m(TYG7kWmF%Bnx-;j8O^xXiI`?IR`Qfu^WbWcO{fkhDR*1>ExY$LOoZkF zVflXU#)|V^qIjf7dGnNu*+C>l4*9KAP7^VZI*F4g)QJ?4gU*^^zAc=4II~R@uL!8S zaIp%n08Rid0mz|P0Ybl~Gn6MP1}7YbIm zcEgv#K{oMSgn1DJ#{GTtx%Y@}ZP6sABQ@in+N0-TI^&Cwut}F5#B@JQGu&oUbZqFT zHR{qHkHcB2?-u|s5q#j2+%xdQMQnX1uVy@r#~VR8ALTdv0m{W#{Jq_~f7IjWLp_V* zBbA(;rF`rBv(JyB?%CNzH#>%GNuyft2def?gR#zzXV!r_!3y5ZU zvvqCLg&pMd(D&wj4COt=eG|#IClga=vW;wAB!eb`A_0->G*l|QK!o|@VgH-{-pE>D ztTf$!_U+RffQMiApY^E-PdR-+J2q~?>Az&}j9W#*4T3U3oA160uql{;6&2G_RRi;I z2`1ADtMUIl(NtTIptqf|XJ2N|s+vK4@{XuQ-3FpmxE<9%z=2=S*QZ=?6{h<$5t)+C za?f5ll8>ZJ^XfE}liXpGycTmE9p`nM3!f-c@CCuGFkDrp4Ze>vGYZr;$tL-T%r}-O zTdty|4q`%A&~^(I>>##xAYpejfs=xI$Xi`Cdr00XN(TtC7hCfDH{``=rUaE@TgLH% z3$&DNdMX+3(nFPKP6hwTvWIF(O-Gc{^pK!TkEI@>E%sXgv5~~KkrXzGrg%SMSw=m1 z{$20ksDFc-!+x2*59n9)KiN9)Yw9+JMy(mKjv{aHpLsOXJg*>yeW%FD_=9U5sT6~2 z`N~(Z&Pv_?E@EaT4bW(l`G-qsTeckdP1B}KZ3RK@V%#K~rAGIH;Kx~*E_ND0z+@Bz z_Te5P7aUP{jAlApNfZn+KPBb`MceZ#b?0Met?jwWtos$~dfS{c3#G#amO;pyJN7MK MnpRkN;}OsK2MZA*1poj5 literal 0 HcmV?d00001 diff --git a/__pycache__/bcolors.cpython-35.pyc b/__pycache__/bcolors.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a972d294788860621b34765087173709f8355c55 GIT binary patch literal 325 zcmY+A&q~8U5XL9X=1&V1PhNTvJhfo3+8!(-Vt0{Z*h)$jw}F6}6oteMi7hm5Yo`YS`HE3I;(fdE3&0AT>OK(qi&5GEi2 zK>#ff7N8Bn26R9;fG!9ZunmiTr`ns!*HWg*VJuTwl$F(6osYBHMva{|cGlQ1t4MEk zQnSgUjQ88hi6WG76e)5y__JN^9N-Hj4Hx7o$C4Ar=q>&lm-5b zvv(*QeM*h*0iA?2*)HBT>vdSX+1jq{4Fsj9 z@Mri-zH&nR15S)LNmbkPd0w9RY`2@u;PJ!19}ETf4Yvl`@B&4@Mo4fG$N)w+I)M?{ z`6-O(PWNEs?Q|bTJ|r0O;3u9fxgx`k{|76=Aa)(xD52C*PEqtXgcU#va7|%(ROke* zJwSq~2dNJ$57y{G#fP*6rVMEXR@6Chk&pZ-_@)YJ4dwwT1%wb2Kw5XrO#{*bL# zdeyVRHfhGE)}LnL+?I=EZ4ai*ymRNyl(>Aq$*wSdm9w1LCfW<8-bC@78EyBt7z#eU zAz%acJ!aC(1lP7*6dVY1=6d&c0o#l*Dl+zxWs)h|kZQ)wsmO$4iLIQ+PSe(V+wa+y zHbQQzBhyaF)*SzWo(*Kdq8FJ+xxc|1L<-5<`erY(eu27+v478c$KxJb3{5_njMYra zj2$kX9F60yl*_KZ7~|8KZ70@WFmAeXY0DQ8Pcl|%!vH@jQgiD;Xg z%FFN^yoFCloOlHeh#8xpVDF5-&oll!o;`@;*3<95-`#S6AFwez#IMm5xePxB1;Fqh zW-w&yeGi7-8gm$O$dGdQg84oj9CZDkr~(%$snYP#0<<$U^$pz;AO~1^7BErL* zmJA+|G6Ja7#=MWdC7vS#6C>+vS;z<*y#Z;8~GD&ZQR%VX^ zr}q9ct0bx05`Qd6>bGo=qpll&QdFNzpsO2ED$^3>?Oe>oX;GLMH5NjyH6zlzOiEE0 z3kN#En;Ny8mf8g4O3o5(xYDxgyM!x}G_Bg!)K7FGn{;c4?kmUxTzGW^W^FE? qW2D=VTe!%G+zvX^bS+gjY^&IC&nP22o>ker5GM?oqH?#Ho!%cN(7sRr literal 0 HcmV?d00001 diff --git a/__pycache__/changeMatchModsEvent.cpython-35.pyc b/__pycache__/changeMatchModsEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84e47a97b1084b341426c846b7f471f22538679a GIT binary patch literal 910 zcmZ8f&5GMF5FS~MlX&BGHf=X-FTE9f=t2)Au#~dU6xu^WAzKP27q6rIIfSZ=gVIg;0IJ52iMm)6`3&(1{pwn zTr-H-A?`rz9M&9S4r6pVd_}xViNnbKi7YTkiIpA?Cn~~Qoazk&C;=7#I|np_g##sn zdj>mZ;~aJzN)FnEMUBR?4zfpr!(#}yFLo}J2=PGIVbOr412SSIl3}oE-QPrv1zl(# z7AJ6xbuL|y-z}sC4^b3m6PR4Q9{#_5+ctu z=>#|VQml<%*z)^{P9#SyNw>$;Y-Pq%HvJpO#~A(7>tBufVms8E$z&wwMUjaY+n29K z>19#eU8>pWztY}qu|1c?o$;_U<4h2>dX5I1CJ%X&b(qWAyyJ#Wm$h*|WnK1&2Rvjg u*5&dXkx@<7@gwO%OP>sN#BSN5D&`4pn8xc>KHg-4?megmnii7b>-+;y7s%}Z literal 0 HcmV?d00001 diff --git a/__pycache__/changeMatchPasswordEvent.cpython-35.pyc b/__pycache__/changeMatchPasswordEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf732062fc55a66a40cd41679ff81a5a5d599c93 GIT binary patch literal 490 zcmYjO%}T>S5T4ylT3eb{DhNG!H3tJ8yr@XQc<^8;L=Z|YG1>klO~UTBQfN>5GCqc{ zu~$#Nf+uIAmM$~H%&Wya4|=NDn@mF3!#({OK){|0v}oYX7Vx`sn088=Ilh7D+kHAt6O?2w5z j#x4_Po1$inNmNbujDtCSaJ?v!WyWvtPeB^C7HQERnVNAI literal 0 HcmV?d00001 diff --git a/__pycache__/changeMatchSettingsEvent.cpython-35.pyc b/__pycache__/changeMatchSettingsEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14b94855ab7c1f684b3596a3c31738e9d93ee716 GIT binary patch literal 2478 zcma)8OLrSZ7QWrBw-)81GCQPUWBwpN$zRW+K<*r4J3}>Hqf`=g$>pJd~QO9q5R8kXN9CLqpHstdkEfWX93rhTJ|MOyK_WpRI!sx5(I^VT+h( z4&+tnysi_8vi*nU4U_sC8=c2FIQjZQfrAUwSCINO=v+5Z59|@1g)%H+uqJq7>$PPF zGoTf6p4JN7*UtZdkFm`22lhc&fs?l{RJ*FHCHc>d3qzlOZ<}!>UEDBT{O>Pa{Awq| z!q{iHN!az9@B+Y+H=%P2=-bdqCS~iRCf|b29lbx6KKbQ$w&UF1!8&>Wz4zaX=N?8~ z_b`&g7sMrW;C16d)eXC($9hb}Q+Gf3;NzYkyMEY;7dC~*{bOJH==yIKMly=opoAB& zQmHWFlb*|e`r$!bst@{%_uP*FzUUqL3WxUz}+A&ZP%XE z_Q<2nCpEIa8((XXO;RIOLJm-AkPSli$+l6^8eiYoe6&%kqrJMZM;`C2t$jgu9+K6^ zk4a;5s}`3VZDfj{mMfyb53znoF8_2J|#NASrtvDN?yPK=AH{Q3Q+7avbU6-6Mk?QPrPtnedrYPDR;q#@?((R` znGmXE7I$4CzKl3kQ-?^q=k|?v{uFFhRiPaTxi1(;J=eliTM^aiA}Unj)NQN!k(v@- zgqzuF4Es#wBy)R-cEp(u5!}Kf@@f`^${<5I5Gtj;Ug_`hS9&qN@hREb-P_sO`f7ZS z?DwfF86`Z5dV1!1q@ui{N?zdOPb5-BBWW=f9j@f_*oIp*RjFSRr*8_VZR9?C*ZAGIMRXLndl{yRtOl8q?J&O9OAXrF~nc@he5RWw5U;$oHm0ZI0D2Q}i=1A9;DtqW@ z3+~d|EnTO`J$dp+OTLA^dW2bGGL-viUPWp94R~=%`8HF z43y<7%li)aKQZo?PpbExR@re~4vvnVa&z6^J-++VQ?JtR4=bYm^lx?-?^-A;{h`X> z&eMSD9WuX6z@BmP&b*a!W>b#!|G)F8cbt-S_O|m@-abRlb!*YOYF%;W?M0M%=a!u@ zmUpc=M_X=LMW^8Kd&r_(;QGf1U(n*F78Nb-YO$onZ7o)`AX?}cGlz5HUuaXg`@M(` R0`{rC7GfSn(Yo-qe+MahXz&04 literal 0 HcmV?d00001 diff --git a/__pycache__/changeSlotEvent.cpython-35.pyc b/__pycache__/changeSlotEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..556e110c175fade185c75bca6849050c1ed61d61 GIT binary patch literal 531 zcmYjO%}T>S5T4yl(zMtr2tpqr2MY>bL_}x=3sQuLhtf-$ZhyDgu$vYN>8U=0@8B!# z)srvap(kg9t=;T=J2T8T`_1N}Ua#@p&)Ww9_=2NF(SJbG7KXS92!P(fC(tANJcShQJgvPgL8ZWr1Ct}Le{3TB2 zJlCd{OyYFRBP1ASEabpc7IB_Txc+mH+_2>Kp)PPu)n;DhoTsLim7T&kj}^mNl%34D zWs^@(4UBfz;cY+Ut0-TNMtwESGQlrbS2z8nm1XOep7j4kJg!h&D_a{6?;8s4Az#-J zfL2MBow9SL&M*pGWuH-15O5}$YAN^=7a3QkGDxJ5N|%MDf+IM!1pjU!hfBfl@yCPq J5l%??(OWjU0srgMNMzh_T3GpSAsDGY^i<&W5cJgY{#>dx2NYU<6~H#W2saX{y(v8Qzw*+w2Yc+bn7^ zucPgbwhoH`*rt6vmqh?{wBKbB0FL&1EW%J%`+XLn+1LJnMFT{%TXn+AAWcQG_{()( zax@Au)Z@BN8cYR0I9T;?YP(J(R4C)Th_kdg*ywhhFvAjIA)<37q|&fe4=;7st0Tpe zSQW05m*XT3Pel+BZk%Pws_(Uo3mt&zoMw@b0Tx~3D)w8rjr`F=UrOo-uX+me!D1g6 z^WoUqL9?YDG`Ff6E3dE2c$MiXMz_Qf3Hs=7jhaag+r(1PKMjn7SXS|4X%~D zW>j?>8&-o&LC|a^l8HC)mufS~^zN_Qjdi^**WK$Kn&8G>e=_Od@c;S#=%aih%@c7jd-~%=c$nw&Lv?wv-1#t{S3ABRr*Yx?_enc03`Ag=-(qhCT!BJT89nr^ z754}Yqqr-^-gcOLNXmkL1>ITflf^8fvBfD8_%e#1F1iYo;yOF#au{{d;Hx9jkA}f5 z=z3mdaf&NYT-F-V=V8kZ0;t+So{KaZ+H!}Qd+WrR<#cY0A)3_=b^nefAETwVU|46@ zT610v*PL5G)SPLOA^8z^3OsFw(kQRsbZfDpAnKlG({gjZ3T4d=6=wfGgJjs99Sg{u_~9>Ku^SBePHD z3o=wd8isC)T!}z^jaJWX+ieTKrS0way1ib{)u1@wtFBsR1SLwid*Fm$%G3qyBHH(=P<(jE*wNKws&KhdWTDT+KpH_7=$*I#}V z_UDYND2el&rP0$d<54MQOmG%uV#3QPj*@ZAIm^Qrm>dieN|r8qy9j3p<@<;fKxV

qV~#O~TU zs7fY(5L3o=SQ!0Ytm3~xHZk;npY^`H?6LWUs;;ju<)kQbb~69;i_7G+C>E#X_;Say zsra;5Xb+vIIcre?R0-JomJIQIQ)2@^_7F^s4=jMiKU5U9X>F>*de@HO*syl%Vy6zs zzYJ}|4Xqr=TzaNH$nQZlAoJi4Jf$rk59%nx9yu_zLH-Ua>?vwnM3Z*&W$ge~4jr~d zDTVdPn~yzEEy&s=r+(N*OT7l!J`F*304q%22-F?OI;3Ge;*Y`IM4E;mI~HEGiT9Gp z>&E0~{`tnl12XCFnE0d+|MLmziC}W9=#C6Ra z$EqGsHD+c+ae%|1Zu20XF>=7^jW#tq43^<>S)ij>3AtDoI#b;$~wKB4^*S9Lv|OLQc3k6SM zdKhfD{TEOxC9SAzQw}Djb1dHM{RRajr3~*NlivoZTZQ;~LE(S4 literal 0 HcmV?d00001 diff --git a/__pycache__/channelList.cpython-35.pyc b/__pycache__/channelList.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96ad7b92cae0c9b89c71830bb608a62038cc7b3b GIT binary patch literal 1518 zcmZ`(OK;Oa5FS7Ba#MMv6Dp^I=+PficH+I-{0*TrK zDnEd~!C&%~6MumdGq#f$1e@%5cV_mRote+Ry1D7y+S)$);sX4D+Hi3F6sNg|Lg3#5 z1>mpN7Wfvt6gCJ4zM|HF3mbggT5#BRf8rrHh)6Dnjfxt?p)7(ePxubRxOQQM z=Qlv$wn5l_6GRhHTlq#b;z=Uaer&SEq1V|hw@l&Q^E#E)u$>(y;wX#LgzNc$oyz$m zRvB5s$MP5=130nZq)70>f^!Q#e}Xd$_THy( zZo?TGZ0Mb#mjkCQ(7i(CKo8S6wL!n4WD;FvFCL<`zaTcTDHv3u%S-JOoWUlf^i{OHj-nOrE7m0oDqO{0!T`mknSg+5XoO zbPob5Ik^Vzn5OD3i-#=JQ@O&7c)~)-sHH5NGdYubo+kKi zpyQA$BUy~BsyU^Jnv>@^73Lln<2W(3U`3DC(F|q44J&U_MVV%}P&|uglF~1j!zNOX zV`Gpv1b5K4^H!C1L5Y4tZ(zOs?IkCvuVsb&-k-W!75r;|n$`vR`%q~qZeQ@L6vPwl zp}2|D(6V|Z)tb*Xr880a2um AJpcdz literal 0 HcmV?d00001 diff --git a/__pycache__/channelPartEvent.cpython-35.pyc b/__pycache__/channelPartEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f959b06d06f6e9b5c0798c905cc4722522fa10ab GIT binary patch literal 1017 zcmZ8f&2G~`5T0E-w)5k(Dp2SNAGlbEN`SZ^R8?wk+HWlj$@nMO;AgE zYM+3&;K+fO*()brffF;10~MR>?6E;=r(;qmcKFTN8$s@#Aca;SZyDBD6B?`sNTLn>NMYTgDE#Y0i|8$HheC zrd3w{y+}vjKzbPZ_tW0D92@)iAVFt&xY}KS)OmJv*8Wqs^abP+&D;{ zWTH(4P$j_Em~7$tGA1EtG;sbc@&|6Pq!i%NssPq$Q-xS+YSDk6!Ylz`U^k9CR1b zH3G}7Tm9pU&7I)OWhE*KUAF^I!gb?OYx8I-j6>*k&)6Tc;)C&OFJ-!EG0sI&tDz*d z#dPBW-$krN@(#aJOPWeLSLB~&VkV4_7jq$3`ua8Dn=$I0p!!9wicGu`St+FHlyaQw zy&@|lUOK0RoJQKXA7VOD#(BSgaPamdY)FTquii?Z^0p_)C`m}!AwzFsX_|y6-F2Mm zHX7hf*0eTIYv0;pcdZWFv?`C#Gwx`V6!B;z@1UQ&N5&^Z&q3ZIL(ik~z7eB}MuCj? WVp=4#OzhAHs*A?q9ajH%r|}0%1M6u3 literal 0 HcmV?d00001 diff --git a/__pycache__/clientPackets.cpython-35.pyc b/__pycache__/clientPackets.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d03235fcc71e2b3d71dc940a563690235003393 GIT binary patch literal 4278 zcmcgvTW=dh6h7CbVZ zKPdH=0sav0^RhAb5h*X;NndW5Khe>Vl_8y-4aM!C3rdQm+U;&c-e_UVU_baMm!1FGD`sfgUz6EfYKb>!# zKb_yQ&TqMSTj)pkqWD2%3+rK$1ntmfOx(!n0@l<_M3=a!plNuz#+c1?4Q>+I$s~-_ zLjO*;)0%TjoIrQNb4UF3ao}Ctd3oXfA;6KiD;EnABki2CBR{Vvv5ubN<`|mg;?S&% zjr>8TX!hn%mPt7*>aX}&&ri25p$#|gD z=iz$15nhuOE~O}Od_A>ikcyB$4j`oYgr5-P|HY-)L#e<9$qt*2-M8 zuq`<7+NiFkLiifYW*`G*(0MY|&gda}xU$EApNIp8+wJq<%yyo(aBBFXM)$I$fA@xuW1~mto=Kr}|95W* za)nQLT%zFk&H+HWaKDR==@dS>gHgwE!h3y;<%zz_&P`SkF49^Sll zOlaO2Y?UXb9(;mo;me0axWm)ubDnFSi$Qm-@-0vU|D-Q83`^GvNF; zkkOocE!pzjI5A(yt<{{Xmn%stW?8{XW4UNB)1g%zvQ9~^4Y&#q_#zIt6c4!a4!F_{ zUgn0ANn=tYNmBo7XHYtRM7x$Y0i9jWtZRS?HOa3Td<5FtTEfGlq2! zldyg_hSF1#Q?Nx3x+rWEB-7a(izLxO{g$y_$L1xwSTC_YT;A`Ry_&&!doeVedy)^p zG-;xtjGr6nozdr1ZrW>P`8|`J_g=xPjMubx+B@ToJsv%kJD2ypnm6g`Ygnh1)}8Er zNnh*{#3Jr5F6m;!n@4iw(ND;cl@`PxGyuoYCWeg)$j89{$p*)`P`2F$BVT~rrk)A5uI%dnO)dF#AX+QjzRE8*Ow<_*06;0xxvJ7H;2GaqISN_gjW@vcAF% zCvW*}B5Gc=vZz$e8d0+<(=3K0Noc!3O2G#|NUpKCT)^LD9JCqIP^2Je!zJt;U5UT`&pN()~dy7samLJ@$#$r>idHDNnU>gJg@o} literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-35.pyc b/__pycache__/config.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d8a639a185892bb66d177ee5ae226d0cce65cb3 GIT binary patch literal 2619 zcmZuyOK;pZ5FSeV(mrCxj_tgo^l@0gcGoD9L(vui5~N5EZc!UZvM?aXB6nA|B5g>n z4KLD@`%il6U*feuf&PV_Iz#T-Yi%TPNPZkXW;EY$v(srlSy_AgXA9tOSa^JFzrdot zLBa83kOB0s+6?*(e&QZ*AO1kphf5Fo*ky3gt^A9VU_T)_e>`lwz@mObA#f>1%eonT zA4COCs&I)&4RFdH*wFFsU<}|NQcvLGjOfl zG*83g_(UYSImg1CoiIz44$o6P3Q2*-I&KDB4B|@cc5!?gD4uLnIMCx&f^aPr@TJg?>0<> zkbOjqS13~%$9iOHsUlR;;Y~$~n49{{ShF$rz$@pam)6l;&s3Di2K`RtHg`Mz1+tB6 z{MUW;-O(#?cAzJN!I4bI<4o+F?L9k6cE{t3T{SwIxo~`88c~FQLPybU@*2&CdcZck zb=LNqY`QYjoNFbFH1MH1z+#;}!QRC&bB;X0=)%vVsJsc;K)nf7mbEYx%M)uQ2qP4K z%?&Mt$~z*|Sk?-bq2@BwT87%oP-g+c6Wz1r7Myw@e_pWj7Vwn?O>z)ItIN>s60`<< z-9r2h9K6ljz!>8jaPU57@Q!a{G;U4bV`#k`iR9FhXOyaWIF4c!>X8Vg_&7-=GU!xX zC?sPbg`UWqqVo{BM$^tPlKFP{I?j~s=Y>W+F8 z9>eMv!h$tessd8~V8Jb#F42QU>m!!r=BM)EicRfcYe<*a9NzAa08I~Th zgI&`~$Z3vFUm>8jQ9#|t$e+E}Er)np4nf&qKAW!1xUsO%6+MW`6)ct>{~$dyrXx%~ zmKzwkzuQdXk`e_jb5y}3g-O?DcFew)ehIBo9$rFSoR@VmXJcHKQOwvF$JeM29E14A z?j3?KKo_*|ObDSahVk`Whpqz0uq~r5a?9u)!027l-TW-N1^n)^?!6L;~&V$LIlmwj5~#z(_V^<1V}m}-(0N+2DH zf`$zo0qL5tER;=by}_b-D4??Dud>bg>n*e$Hb&q(5{gwnNe{L1i_}-oRN4jqnIlRr<-v5fwEs6jD literal 0 HcmV?d00001 diff --git a/__pycache__/consoleHelper.cpython-35.pyc b/__pycache__/consoleHelper.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..765edaf755bf00fe5ac93106e698589a060d423b GIT binary patch literal 2482 zcmb_dUvJws5U2b{wiUTa+tfvebY+@{&D|oe4+Dx|FjmA~Qy^6WJ6o{Ko8U-H>`XBY zicT6OdFsB*UiTUHDfGIheT6;kc$5{VFbWhXCGmbd9?9SF$WyIawVo{A+yARX$UkJJ zLtw~OhLh3*c*hAm_sY7Xdc{&4}xO&LpsLU zGFvJ+--}l-ujx(kZeWT$FMe{P{L-u6G!$yU8$Si$VF7_CRp@@z6R#l#q(*=_rM(-G zA~4duX$^}hlnT8@BVKFhNw9YGj3|_%rxx=X@qI;36+IP-cv-7pM#Ta})rwbB?Hu7a zlvC=UgLAq;#T^bNU=}}~ZvZCTah&+GdEK7(%dfCE&s5*&fH#%yLVXSd>iKUUS1zyPdXw(6&{tPoayn*XXM90< zG+90DkNg3j9?ml`ebl6H;BeBz3n~YUc823IBnHX)r|}m}dU-vNavW_m8pm*UCx;Hq z*?@@znl-eDA2#W(U{ORj_d45OZK^fd+o81FdL=310Zz;#E>8P0DINMepBsy(xbZu>e`=zi(O_ zy%sy`%E{4DPxDznd;V?DcgEujCmQt9T(`x-F%z&4Nevh4;{EKZ>$!!u(^NU`8C0Zb z8#TlFz*Ebu7)!>AA*x{Q&nC+PC@@(r1AtVh^ChIocZOj=UhLKj@s?HD)hR-PIL55N zCz6iXISq#+M*9*&!A1dOhDaMhUPzG&s2Xlnl029s#H|}b{ZQ&gVrm=Q8C}`XkyLPi z%iVAdnvpUlC2<#OA29$Wi}2}84?yK{LuI}zc%i9tyw2?SFUiMmrjB4p;WwgJ|1fE^ zRcS6_aOm`5fT&oN*u1<}k6SOGJ0cG(gxXM{LQ~I)B@jswhrH*ZfFCSbhfn)f#hj<;4TQpO)4^p~Xk+7Av9RIG2?^2WmibhE(m7lS)eaeKCTw zeT!FO{@mW)-r1i^O=o~~SZjveBfyYGN}d;R-}Cf=D1#C&A0A>I5Vf~L`6ASixr>y) dJLSP7WKDHtBXyk2oNd@S`>}1=3$|&S{{!Uo5^?|l literal 0 HcmV?d00001 diff --git a/__pycache__/countryHelper.cpython-35.pyc b/__pycache__/countryHelper.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c25eb06678526d7398c72e2c040d5a14b737def4 GIT binary patch literal 4870 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 Kz^o~=X8#u)f15=B literal 0 HcmV?d00001 diff --git a/__pycache__/createMatchEvent.cpython-35.pyc b/__pycache__/createMatchEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3bf29de74caa43d298001302ea3fe10f803bc25 GIT binary patch literal 1131 zcmYjQOHbQC5T5ngaU2q$p#{N(pte$TC{k4+RfwXhgbE}A2wKsmaB%De;y7Nj-tcO2 zY5He+=HKkKr~ZYWI=gv@jppf@@0(d==S!u+@=W<;-~jxAN5?|{14YqrF3L2@7E1P> z0NhQJ9zX=pw?GnzEa(v!5a<(-6e4?+L9j*qfcVDHp|OX?8JZj#3kELqDfH;L-G;Uc zLV$E2%E8YGtgUMn`!+}yqCDUb7>_6h6i5%LJOb?7@Lo`R?dJ;%(O zgC6b=(54_Bv>%l~PMh(mF3>Pn4{#!9Aew~`NuOv&Ov^Gvb1<-goFuQn9uC?rfOtpn zjOz2I?5QdHmJs-gRL50sUwIv02ABJOhpCcr5B)5=;UZLCllijar@J*ZHF8Z^VHDJB zYbt+%jddQf`^C*l{qX4Ep#G!3UOD2tQwaq2vvB2p`TNRQWm^a?DmRy{m<_+Ql8cHV zT&X@&xo*Y;5|x6u#S(ew2W=)ZH8sSj>k*ZG5%Z>UO%Kadf!ziyl`WoRYStvT451-X z(-Charf>Ti0hKq2m76iS!!xOhnl#%@_IR^-r*e|F8Df<$nEb5WPl9*8499X=a@A#B zydsRRnHbMk3Xq-0Y=^}u6RMbsRwB1}%moWo?wkvpOS#QJXEJ4f-21Yv=+Snq>MBo@ z8y;0i&AK&T`XY~i?9q;R9gf*ck(P4IbiYA7$AW)vYwM>qc6}_n=jW%Qm8LO!ef?(b zG+0g3yVdOS^wGVU{Z~_5@X}Ahn0fl?R8Rq-mPcpo4O%81ouv!3MCYtBDUu~xA+toh z#F}be%%c&yOH66Cpw*IAGg_6j(v@biN9qOURDPqw!*0wr@u@8qu literal 0 HcmV?d00001 diff --git a/__pycache__/crypt.cpython-35.pyc b/__pycache__/crypt.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66990732fef5b7e6bda11148c1acff5c8f488ab6 GIT binary patch literal 7726 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 literal 0 HcmV?d00001 diff --git a/__pycache__/databaseHelper.cpython-35.pyc b/__pycache__/databaseHelper.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8a691b964eac087aeff7ac1e557315e741ebca9 GIT binary patch literal 3458 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 literal 0 HcmV?d00001 diff --git a/__pycache__/fokabot.cpython-35.pyc b/__pycache__/fokabot.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24744a8920b14c711a4d2f1b9a1716ff06177daa GIT binary patch literal 1625 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 F{s3i2dfxy5 literal 0 HcmV?d00001 diff --git a/__pycache__/friendRemoveEvent.cpython-35.pyc b/__pycache__/friendRemoveEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a40570f1864f55aa69daf43478ef894da87201cf GIT binary patch literal 515 zcmYjO%}T>S5T4z%F)gk2q<9N>&0`LV}ha$c~DNax^yaZx^(FPM35nvRU0UyXC zATV@l-xv*pKxgZtt%-7rQaqxP022ZW0xJSb0+PbSfCZ8@O=6A16$K*ey7a`XJUbw* zjS~~8&Lud~&@l$e3dNYGtna=Z_w4tj9r-1<1y8b)GyM)lme?}lV}Vdd7EVL8gBYQF z@`EXth3fc>Jy$9BRfuZN3N9XaoO7XC>tg>NQ+xWZs^?;yN@a$bNPMX*O{adsl~G9H zP}R`HOE%@HYUWiN*O&e$h+SO5?}dBixx5_8Sr~d^oaZq=E6*>zpquCOZV`FwQP(rw zl{jiQpDQ!+Qx@}@-lbkD&<;5;#2zBY5Op-d&`J|kpDoovPFdGU#%3|^;b%jk>$FIV F{sGwWe~thE literal 0 HcmV?d00001 diff --git a/__pycache__/gameModes.cpython-35.pyc b/__pycache__/gameModes.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c9db92aa579917bc0ed1fe1fc36b1a45f9901f8 GIT binary patch literal 645 zcmZ9Jy>8nu5XVoFE!%3+1R1i1V+F)&I}|94AP5>Gnc_u50l|PliL@<5UpP{36Ljpu zG;fnxYbL)!rW^&^F-n1d^6q%#?@p(Ohu+!I$% zYkC3J1ORLE>e?YmjA@2hjrW?S`cCo(Km**{fX9X?G`7{+%&@h#-P&IaWk`%65r)3| z_|F><*Gi&he8$>isx%<8=Gyn(6Wj-c6a*xu0Y z(?T=KS#eC;Qa{Pxqg^ymTKJTSQrB2)^O$MHdvo?*j=e$SP*T&S4&IlIljykV=DbMx z#CNbuJwVq;kHoi{{+8(p&?DFD`FJ!P%T=iB`Fx6LRb_Ix`uc5}1XcAAsO5B0eOA QbtW&(4^@WKwKkvq1|XoDoB#j- literal 0 HcmV?d00001 diff --git a/__pycache__/generalFunctions.cpython-35.pyc b/__pycache__/generalFunctions.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f7868f5ab942cb34785bed1ee53d971e3b82cb0 GIT binary patch literal 825 zcmZ8f%Wl&^6dga}#->HAx~N!;0I3)$P6J{SMG>N*f=yYtswxx|x%Q-XaXe;b9Fowk z;3N12e#u)_h(E}Rdxs`X8F`M+yzV`7?(FXDc+VdHIsIV}@{4RV6Zk9W@)1Bn$Ok}! zBqsR|9Ee2{TXT~n4HEj!q8;)HqvNWGRmKZRRT?Rp@M2%lai(Zi(5RSGu2LpoB4;tA z)D8cb?hv{R0IqbdPbMi_nw#X7TN~U)9{P>NndctzV#066--s4z%_mDD9Rs28zIG3u z&#sSsvv$T@Od?fVTqH0wgM-?LZygj{5OwMH(5G1``n#A3(>i4F5LAV_q(Lv|G1R3W ztT#d>>`R^%z9Agg{$Iq}U7x+biU< literal 0 HcmV?d00001 diff --git a/__pycache__/glob.cpython-35.pyc b/__pycache__/glob.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..110f1091c976bd2e581d148abc7862ed353a7404 GIT binary patch literal 420 zcmYk2K}*9h6o6mTt?jz)Djq!SHauAL;6V`)oQfb5oG64sNt-DN`JVUm)nVueWtC6P(m zsdAsL9u~LvbA@QEdI_(V%8yBvr2MjT$8-?N`z;sBHMLCOe#uHEl;1ERNh7mr^$yKY zPh#*jot=g=)-L3FxeS}Et_yb19vz2CP}iG4q~S`xD5y8eJ1aR|7wlaBJcwNl2aPNp M$U%fgc5S`t%ow$iBBt8yYFx+w=QV->TP=#nJ0V)+zC`geJa_kAQIzL zg+Iez+AAk+oH+EvY^0#BJ-O{U8Vq@aFHwKMdd(+!~ztOA2KnrJF$kF!|3J zOxRxMFyVVHU?L!;sem7}-m%8}1n?h%G5w|GAqMAtv3~1iO`&lzU56jGJj(D$~L8s=)?LK#3K~_i?g7txPYG7;AiI%_c~yQdI@M z#-c`P`nAkTeOeWj#MF4RO41&tGfk|bjPvUJ@@(K5-=5<`wZPJ_FVenTDF}>MVEYr~ zUE()Kqvz2GS7Y7GW|7S5y1=KaqvI$U)^$5n^T>95Zd*KCVX238YrJ_}rUiOrSA7ED zKJ&#L@rXSX@;=Q6uJq|4?I)By3x^hLYPabvxJR1lzF1aiQ{V~xWuUqQeYU&!KRisD A8UO$Q literal 0 HcmV?d00001 diff --git a/__pycache__/joinMatchEvent.cpython-35.pyc b/__pycache__/joinMatchEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b98804b76c82f95d22e7d8cea4b21f62f8101b14 GIT binary patch literal 1790 zcma)7&2Aev5FTnJt)xhiI^M8x+DlC4u|B-H#6jLcBR#7yx(3vyi*7G z8x~p#`LD3+2nqIxK?=~j<{9+Ze7yv{(wz68=gs*t^vZL-0=)`EDCWV>h*O0KwuB%r&2|ol5K@)A~Lfdgd<_Jtq*0AJs~;Vs~1|RXL2Mm+sK{3R$zkPK-JLr ze-E}E9&L$}15@<-M=HtlRD5*u;FF`Uo9C0Rj*qUPwolMPH=o#Q9Ar@{NL`naK&kDi z29BXeGi*NgCUzI&pA2B~F9kIOxbPfJ_O9Z*F(RLS&duVY9re)g3TOy&u>@O%^MKWrD`jlaNY3CMPf)g{#FuV;> z)#U$P;&YSnf zMQV~f4JJZae@BjjucbNv<8i`&yjbIy9sE?Jp&SctB!B9X>b~X+N_;FMahE@f6V3Y~ zFohBvS#6Ru<%hfb4r81QV#DL$L~u=5iPnYCUB27r6It+nf_8#z!jFrAUgMa!ffihe zTq?s&EXMqU;|a%y5ss2Qt{<+iUuAlCEX6v)h_h}U=U?WZZ*08io`3fI z?h6bAEo*L6A|i@{o&gW|IOj^r@fsR6JTW|yLTC347iWn!=bLY;8RN-JazaM#R7v!2 z{+iDpy(vhLaiNq{&QTeLc!*JF#a8LPQ+=Q@3&z5h2dO-s6%L_oL!5;oH;K%&U2)Z9 zPnsQBWYP9~!?xV|{;a~-cir@m9G$Pdly!!_#q+UuoEWj&GG6#`?!0aoT!Mx3@aJs-jPdsydwB9 z0T;>=GI|3EFt1W)b#EE_Qn}6AIJUhe(w4WvR#}HFvD@t40(JVGnxev~-XYQ^@*WXR l5d_F}gzQD=~ntX1u>tCr^`yXEM!OH*u literal 0 HcmV?d00001 diff --git a/__pycache__/locationHelper.cpython-35.pyc b/__pycache__/locationHelper.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3fc500b98ee10d8d5392141194282afcc7cb27a GIT binary patch literal 1198 zcmbVL&rj4q6n<@Y+ikZCqQ-z8tnoq(>;@ArV2p+(YGRCm1aVmorJaQ~rBkM}#9iQo zNB&K`lLY0`$oq1I#*5|<*hFPR2=7t~jMyzAI+ne;+F%%m831Hn(c_n*8V ziUcbP$FbwwTU{mBo62PiCWj&;?`9Ph@`!oM&%KmM$wc8hc2D2i8`sY-JLi|ms*;Zv z_n#biTS9Q*#hG_>Th?!>T2k;#)l(isg))aCP2)pl33fVUg;W+gc+N6q4!Af8q%zQ< zYJvrkG9yOyRV(CK!Bh5*r8yJI%|)EaO`dYWB4r(hG%J*`z18(~rN_CT8~*`f0Y5iu z-SvK#o$bisV9*zFo~LZ}Z0$uqZ0Gr?T^#pQ9tJYz*(7^AA1Mb9P+b7=R=5aYo4TQI zXoj|^i+gDQo!;-bg7o5hq?aUj|BK|rCSH;88MYlrnxsGJLejcrTh0Lzjs=%+du5oV zfgPYe$A=Rgq&m&(^v}PLbM3x2YJ6K--l*C!LjzmX*t*WoM)TK|wR6 dNN}R8*LZotqhZQk(NW7{uIB1E4AXG${|2_P6I%cP literal 0 HcmV?d00001 diff --git a/__pycache__/loginEvent.cpython-35.pyc b/__pycache__/loginEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0301d235fb105a82f947cc7dfd81503d56df859 GIT binary patch literal 3742 zcmZ`)OLH5?5$@T=`$2#YQG_1h%2Z;}31KUVWyf)7OC%*zMM(_FgfdK3YKa|yyIAZ( zy8~K)fJ>a5U6slum)zr9{(-Apm0yt4TyscN`2nfQDc!TX02JE9ZgZwz)BSb#>~gtW zSgkA^Z`A<)0W*IZu0O^%{38+z9|kT!W3pz@U|gNi0>A@p_Wl0 zj~NJnF960Gi~t-44!Y6c3kG8yMk>;d2EiRA)rl!EWF4F|Nl^_SCNfH#rHzH+G!%cY zLCwKnGs;y;S!ID1uX>fPagT zqd}RZWKck#7<#1luM2u}q=$!&voJzDbIM(Xk-JLbt_sdPI18~R6VZM#vCP3J2hI`@ zW|znXoxdDQ#bppH;H;1@JZ1%B8RwNoEwRTEVV+o=B8*Eg!fhHlX$Zb##3=%ucZH;s z=~pVWt23cp1LrzeC8)(3OdFxEDBpi4!F~@$aY-<-)!5p@ATe?S#0_xX0IN(9;=`ug zH5lbDa&N|=zpnfJIL8_DAHWDRDgtKxy6&6b>fzS6a@P~T;w^A~NFiHuAb5WcBdllKh9ANR3rKfKGl5 z8qUf#IeQPU#!tOJMVb{L&8HVMccz+tq`9gzza-7r8%`cx4@^0XS@)-A??+}=mD%53 zH>-WO8H1$h#s2>A0p4~5b4gUBYJuh9KhM33bBsp&P({70tk3GU&V5C3pau?#yj(7Zd97+v4u zb>oTW4rm#^ZCYL13ym}LoExpa>l!9T8-|AP4ROYekO${Hs27d~J#O3x2ch8I*Zldd z79G(%GljD02cAK^3ug{fYkt@t=GRQm^ZOKJW=$O~b6xKBcM%&2a$Y6;*cW!oZkmLY z%tSHBvgf*AxiE@b(nh083&9RM@(4ga(lws+vZNe{LYIeZj+e z6w{kw?5$hUn-(S{^)?r>81Swy_-{f4Oe+s(nZagh*=<>j!ifU7 z5-H4#!PGd*J)EFEm*uFL_RVI8i%^!l*-o1aCovyUuo;3EF|G6hU-(Vm{WaE!TCz%l z!+x*l2iO!39v#U%arRA*yV3GcnCN~E#{%JWcfFQRKvd)XL*Mpx&N-q|ba1&9X~|qH z4rSWLIYVfA{ZrR&;$33N;*2j@XazR+tUWsvvP7fyC>f!m@42?ee}&=Q8HMv+{9tUafoFllx;;EYPRW47Q-2< z0&Ze8?8pdv>@%5;sz4>@;+IiHFow`~dECY&)$?1v>j&7A*;7Q_$4t@>4t91P%i>H& z$D62OWkE@Hg1`@C=F^?My(h=(^Nmu=HN(z9bbZQ2Jhq3Yan&$~3JFv)=0s>fzZ*lsffO zy*H2<>?X_QWi$=HLIPTb<@E(tLb{k*(=W4(meFfkNh`5yT1l@kT&1dP0p+)Fm%(3& zWr(jYqpZp*`ZBJT*&?eUT}CfeRz+!5Uu6|`MI))9;hS1TjqCcNeqF1ut9k+btmtJ{ z)@!V$=UG92i&gauTVhp=iRfZXYkCPi1kW(q^-^#N{{Wu>@PRu$ks=fQ|=XrcPv)=Vaqqf~@pNvWXzu?wm5r0Kf4hTXT3=#nE zKWE^v8@mObb;E7&>>KWY=Rim<8@^L+$%q_vooiBrgTU-yo-$3F<}OY7hhPp611zkZ zFj&|*v0&kVvOzfzJ8-oOE5XJUmBC!Ne+&#-ffn0CTQ20TqM zP#E^Q8`_;p6mR>wV%F53pe`vs##Gx;snK6$?sEn|8y#VP6rea}!F`oS2L)GdJ%KEtD>78kl1^C-n|_uQw_5~(Xe z!Mwb#LtiLeE5u$WiLRMy2Yke*O0R^Npu&7_-Js?qPjDZT427;|g4)>S2^Sb@_l%2) zuXOn|FczuZ4|{{byOVBNSB>COf_)O^4he_As|%zKiEj(*$?SWsoMn I&WhLi2Tm#L7XSbN literal 0 HcmV?d00001 diff --git a/__pycache__/match.cpython-35.pyc b/__pycache__/match.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bd386faeffe3637e2bee59a63f34b69ed3e901f GIT binary patch literal 16466 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 UY`C4v@id#2@`|o)=r=XlADK^NSpWb4 literal 0 HcmV?d00001 diff --git a/__pycache__/matchChangeTeamEvent.cpython-35.pyc b/__pycache__/matchChangeTeamEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d0f1855fdcaf59cdaf893fb77e2100db798fc4e GIT binary patch literal 436 zcmYjN!AiqG5S`skTU$~sJqUszc+0_p2T?>sXgql7r3Fy}Qj=|Ko3>#$DHNKM{)Qjm zFYVP+e!+{r4N^Mnyu5ik^L96f&1U1|p#AvCh`wnjIQSh>9|IyRMlsPd2&YKU2RO50 z-*XCdP%pF-HY!NVyGZ>6*bq&La$XWfxuA`(HZxA@)Oz<(r<|t3u33%LRjF&tNKq^WOmquCye{g(WsYbgTSDza^SG+wQ}Xh>9cW_1?A^?lQ5MTv`%Z0t HtIPiYwQ*fU literal 0 HcmV?d00001 diff --git a/__pycache__/matchCompleteEvent.cpython-35.pyc b/__pycache__/matchCompleteEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53bacd5aa74114a338c2c54bedc1660221dd2558 GIT binary patch literal 448 zcmYjN!Ab)$5S=93)|S?m9t1%UyzRk)hk}TR(DmS@mkOfnW!+7!-EB8y(?X#=>CgBf z{zR^x`U_t4Wl`xc^YUhrd6VRz)oL8KyZ0ZA=#zGggI^MXc)T=XBC9w14QuvSQAZ&HoPQ^HiFi|$)ue0iPIh-H=L%zd0HcVURM80*go8N zwDxEUGp$HdGX+(gcx-vSYp8gwfvy^kwQ!87YMDrw*0un_BK)f#MqRBkbMRbJTN6qKDilyY`v>j++!|q+q Q@???8EBEHYeQ}fj066kuv;Y7A literal 0 HcmV?d00001 diff --git a/__pycache__/matchFailedEvent.cpython-35.pyc b/__pycache__/matchFailedEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..803218d011e1fce68c1bab109e13b9df1a0432a1 GIT binary patch literal 434 zcmYjN%Sr<=6g^3%t(B>kE(Adk+|9xP5kW*munKOvsUXULj5Dcyr0tMQ3x&=~KSqBk zTUY%BH+nOQ^pbmW?(5{S(`an&Z@2E>8SsfUSYMPJA5v#2fS-F(*TMS75BN*>OSPNF2tvPEe6QQ7?~dOequ zJj@oh+K(rxl7a+GlK{Lf>cVH9XpuLC-lAN)zd^}BON?y2E8uzI_ F{{aDdT&e&7 literal 0 HcmV?d00001 diff --git a/__pycache__/matchFramesEvent.cpython-35.pyc b/__pycache__/matchFramesEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fae840ee5bf31424c70c732c1def62331d3de52 GIT binary patch literal 783 zcmYjPO>5gg5S?9p+L4;zL%{u@(3=l6gtkyhD22L#UP8ew0plK2dm~$tC1rPIVvtYi zssE&ZX|KK1*B*OmXSQkZvhy@M^XAR0q~kd5K0fSS{Bi((z~18Mzd=*a5$3oV6abSu zpTUIvk8_xCnB#WfD{>qnj|c7!E8%=bO0+^W5A7|Q`iam2WB^-k9R^zmS}gS;=OCYu z_88BaL*~%V1CTFm^e%+y{?>)og$$9y4T^Yh{SCBtJI)P<(dP|peM>_0p+9%g0?1vE z>3-OQz78M@ZU59Z?m*Vvr(h-Z2%rsUlH9QpSabk7f-HvCgO&kyMH;(G;eu{42+wy6 z?<)y*M+nAml#uU7##cJgjoSUeC4I~c?n##j!79UsJ`4Dg)(s}^e3zx7nR1W zCzCQ2##4AyCZfmKDk3$gi)6DbQ{(F@7o{>ito=ytnXFbMf&zgkuNu(^6XSU;W;>y7ajNjWbB{WOYHfOnjZ6EWB6p&dDg v!7)2wk66UyL(C3bN&lFP33>z+wf*gBK^)0N7fq~&M5W@izkmUfx#WFx51&A0Kau^t*fD9&v6lR7NAe#{+7_77F$MQUP?|XP?EE>IJGDwKRY$=7FR)Ha&~HoOJYglN`@jfpt--y zU94hUQpWHa^HWN5QtcRFI(ZlY{+mQy literal 0 HcmV?d00001 diff --git a/__pycache__/matchInviteEvent.cpython-35.pyc b/__pycache__/matchInviteEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e247b608f6dcb99836070ad8b2227c7a9c1e5d42 GIT binary patch literal 513 zcmYjNK}*9h82ys8>ozwxb`W;*Y7Y*Eco7l7itI9k3Bu^5wwdeNwI%5agZ4E4jDIOt zPx}jA^h=eokGx0TOY*+(oprnI^WNa~lmdLgQK5!=guFlsa1n?B=7&9jIe~yez~BRY z+z^=A#vX6O)Yq?dnFs^%fRHIt39tgFDC7iI6iNa+0u`x!1{H&n>BxwJ80)q()VTpG zQ?JMZ@l?A93y+7D0VQ5(Kv@Ukr-nNUM8CF;>7HO))!0ZbCeHo`%eDm5Ue}L9p5)J- zAMsqOleL%o%Slp%Iak)7aFp>Lr*Hp~OVtSLbK6uco%oVQJW=hej&Qu(tCNML~VFH*4?TmWckMwZ{Sf3PXPf z;^Sx0h^VvVE$Uc{b(=aiz4mSL9r^njNW=%2Z3GTa$zE^z=F@j65B;nsqD5X*ufuNeW)4zn?arpETv=aXjt!PgD>^ zBe{3^Z2zR!h@$I;9-J)iX++masoM=;M7LXJvkM?(yOwLsYAdmYNRRdN<7UG^eT@m= z4T)|_%;dlU=(jeI#}*BN8S9`n%^jNOXr8Bef#yZJHS$BOm*@q?aozT&aAVnZfAD>H zWFoIpuZTEI8L7nxo-aeB+cg%py|EN~dzst>v!_DGDwv$DWR5(ouR`Uo=naf*n*7hzvq4MNdVuwD8h9zYEz zE{-S}Pt|1Q41>gETisEd0!iM8svP*btBQDa9;NQ)ykGNt=gB9_3uoUwv2TIvCFVNqWUb!GEmR8dgLrOSZla169hwOtbENGL=4_s^0`o@|)GJqLe zV@xt;JKzm2Me#Wo|b+M#8nUvxUjz$S^c4GA-kM# zJtN6b_a>&CZGNDmP)kU)QN|1`aiTCv6DfCwh$n?OJeL!l)Xrr53$s5XPMj;PW>S=s zi>aK-q>A~aR7YO#T*mkeWqM65)pKcoc(|U3#sX-tf+TM)_$W&132b?Zp&x)C6Rn)H zgsWcF znDFgu7I7y~+$0p&I9Hiw*@{)*Kf6+>Y*(t4s@VnH4VK(y@&ywa6JejLKUI(vhJqakF>iaX+hY%+ zQm&r-3!a<=Ee)AT=FNL=LJpeEox}Ft^AQF3f~|!$Tw$nVOet;x0${kg6BrUmDWnWO z5XTFFLFj!W8}1XUw#&z07}pr;4O0m)1E?ry0y7FFfp-EGaX5pDLCHXSFss=;4?v#T z;e!o??QZ2ki4-47>eMxBgX6m;N+`&glUPdd-@zfo`h@h}_)6sZHVW&;UnwqcBhyU8 zlrQx|oXojarY`biK0uNQ7O_qy$nY|ezx4P#AC99c*tachqO!wxRG0_ WjSW-3SmfzS@JoV~vfN#=OMd}@!g;X( literal 0 HcmV?d00001 diff --git a/__pycache__/matchModModes.cpython-35.pyc b/__pycache__/matchModModes.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3043031cd1c6b4069b9acdfc28d716909967f70c GIT binary patch literal 169 zcmWgR<>gBK^({=2fq~&M5W@i@kmUfx#R5Pgg@GXoNHQ`+F*2kuF$8NezXXahyaXxo z(`35EmX}|Yo0xNpJ*_A;)i*z7B|{M_Pzp@^GH|hqaY-!;E-6h*iz&)1D9B0GEi*KY tN!BYUsMIUYh{;VXNzMT30;1Gny@JYH95%W6DWy57b|CYMnScZvBLKyuDOvyk literal 0 HcmV?d00001 diff --git a/__pycache__/matchNoBeatmapEvent.cpython-35.pyc b/__pycache__/matchNoBeatmapEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..720fafb96bcfa045f2d22ecbcce40a753107dcb3 GIT binary patch literal 318 zcmWgR<>gBK^)0NNfq~&M5W@izkmUfx#WFx51&A0Kau^t*fD9&v6lR7NAe#{+7_77F$MQUP?|XP?EE>IJGDwKRY$=7FR)Ha&~HoOJYglN`@jfpt--y zT&!YTQpsFdhUcLIqKBS(B|XNt|*b zIQE?tcEBjIC#H_I&L@*ly-Asiqw4r1jD}J! zhB^tG?)&wOn@Y^epejV0#KK^%p(pPVDy>V#+px68<&PR^ERM|8)oZb QTEz2QT-qmV`@a^ZL|&y=L8ri}o+AsVd10@fBS)?|7IwV3MwAD;WGHXuU7 TZJp1GY@y@@{x>l6o=yG>aqeQv literal 0 HcmV?d00001 diff --git a/__pycache__/matchScoringTypes.cpython-35.pyc b/__pycache__/matchScoringTypes.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6dcdcaef639bd65b6f7d8bde12fac93f74613396 GIT binary patch literal 193 zcmWgR<>gBK^({<=fq~&M5W@i@kmUfx#bQ7rg@GXoNHQ`+F*2kuF+?#jq%boCYqGoq z$}zkIDSgQVB>Xg)Z?P69=NF~k;z&$RE-gw-u8d+$&d*KCU&&C!22=qiewnyf#ki!F z1(%ejrNtCw78K;9>XsRr#w6<%6jbUJXT;OJE(8$~!MJeKO$1T0iOJNqN!wv2Ed-jC{)`(x zC|g(k1vh#oi1ae|%(<^KkHc=aeR9x${A5I5v|}9n25yc42^OP_=oy4lBIq5Q*<|23 zTXfJUwiDJGaLaGu<_WMNnh}+}CX7l!8)0QqPWsep2Z(b{GhtWSB7IS3|5GR*l@4tj zngKbzF=BSi6QeEeP1_%s*QlB+BlW#swdPSCPe7d4Z29sRN>jB}78Ozl=~SWHwcQWP zR4!16;#B5-ltdql!WA9nh(5e0^gePP*G+6SuTR>6E(+D&)qI((GI?#E JYV6N;_%EYbUhDt> literal 0 HcmV?d00001 diff --git a/__pycache__/matchStartEvent.cpython-35.pyc b/__pycache__/matchStartEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..486fcd59a3f8d8c88d2598a3f000fb0f74fa3b26 GIT binary patch literal 1021 zcmZuu&ubGw6#iy^Cfkkup%x;jpdtjcqS%WRDHsdAltNkxB~sSxG(UE;yPcVYMv_xK z6g>J@_%F@XQ_kMJ=$ob1Slpd?^X8lPzVFSO^;#`>xV(1s!vVfy;j_r!rYT>OiD@vT z!11-uaLoST7LF~%G;DmP7~2H58qSUm%%44&Zkfw zVi&Q83k%H;31)7Y=40xj9{Z@P3I{l z&U|)cm{&}*bw~-u@9|D@z@s7g{cK<4UBRW) zu86WO*RD)+C3R?)R#^e%yMjmYMEgY=P5N0^dn!+OCiPmEs{`ZihDubOY#O+Id^(dlVh^b6|9^YP2gc4wm~CL6NX{;TzV%rmu7OtjaFvN+`~<;!Pe z;8g99IrbXO70a`q(pt6G*#mZmt*~u VD_g@n9;N)Pc~0q(35h4PegpS@>vI4A literal 0 HcmV?d00001 diff --git a/__pycache__/matchTeamTypes.cpython-35.pyc b/__pycache__/matchTeamTypes.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b029bf12ef33824f6819a4e21c256714601b87d GIT binary patch literal 219 zcmWgR<>gBK^)1YTfq~&M5W@i@kmUfx#WFx5g@GXoNHQ`+F*2kuF+?#jq%bo?F*BsF zFa&F|z67dZcnQ+*k_kjG0|`G(mRnpIsfj5e`5r(rioGN;-8nzM;1*j+YGQ6!@hwiE zKnR$*lA(wLr~^#=GIX(uaY-!;E-6h*iz&)1D9B0GEi*KYN!BYUsMIUYh{;VXNzMRi g45=(gE!Hcjyv1RYo1apelWGTYK`|SUU}FS903>`i5dZ)H literal 0 HcmV?d00001 diff --git a/__pycache__/matchTeams.cpython-35.pyc b/__pycache__/matchTeams.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2eb2cc1544f38d1e867e30c13dbbb41b7c55882b GIT binary patch literal 181 zcmWgR<>gBK^({=Efq~&M5W@i@kmUfx#bQ7rg@GXoNHQ`+F*2kuF+?#jq%boCYqGoq z$}zkIDSgQVB>Xg)quBEDLsAoSZ?Pohl&0QdE=o;V$xy@wlmrvMbX}}sTvE$|OG?wy zVu~^g3UX3)%M49plJyD-D)ovpVsaBpk~2Ui6zdgK-r}&y%}*)KNwovnQOp7)*cjOu E0pwpQRsaA1 literal 0 HcmV?d00001 diff --git a/__pycache__/matchTransferHostEvent.cpython-35.pyc b/__pycache__/matchTransferHostEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf9d90c555e9c3726ed82522aea5faaa35420dd5 GIT binary patch literal 488 zcmYjNJ!``-5ItGSCw3Du6f$*OuB|kQ9otAsAdpP$pXp!H z+9_M-F6~K_kVEh3?&&@xoqJyMqSbl%CIDY>Vh;NE2-!nQa1qD=rbnN^ltAJ@Lg52- zbO?-ncED^H#irUALr}y6LdHl6U`b#{;GMu89Sj}VJFv5zQU^rOOdDgYC@h&-X${1{ zsz(WryL$>dOtIbYB3}*C3=Y3&enoq6w&1yXj^dOnsa++aT+X<7 zDx}i2RixtiFwpERD_;LYE`6pOYstl=NO`WCrL7Gj6^WXeP_b2?AUuqIZi3q|;G3~p z&t{=mlx4;*H&@qT+%L;*U(Q3DJvkZsxWNnc%dM`?qddvD`7YT;0;feA)Fq5|5N#@2 jsQFaXaT66oMzCW~=nOSozgrc_I^zTL2hwzOiR=6U(8_G& literal 0 HcmV?d00001 diff --git a/__pycache__/mods.cpython-35.pyc b/__pycache__/mods.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9a7361ad8e92b0aa61797d0a3575f9f8a21d4fd GIT binary patch literal 750 zcmbW!%Wl&^6b9fwspGgw)1f;Pchpe^t=XdAo(+5y)=b?`1|7rY1B1Mh?O!3UrN@FD0Bd;~fIAA^p;C+MBs zTgvIN)Ml2_A(oEfDK+I(X)1RLBxoT)3kh0C&_aS1r%F?ipoIj5yy7UHa97o;rkpBG z1vw?JIEts#I%g+T=!BDy$?b$~-v;Eo_TxDfJK>FG6Owyvnaz~EYk$Vwa4@EFFV&fr z@srV_)C$wjf%#-7h6-I1_}>zUg=n>W*i+x4d$PN?|Kj}u+c z?a!$oHXde!M&_f;iyxR(rTdIO_gh!}mbvXE>2TPe*(eIkgWJYse{dc}^YeJrmyZ*l WNArdELaH<{FZCzJSAv`S-}^5Rd!9i6 literal 0 HcmV?d00001 diff --git a/__pycache__/osuToken.cpython-35.pyc b/__pycache__/osuToken.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79035e743ce663ef787cd7baafcf0c194f8b369d GIT binary patch literal 6477 zcmbtY&2t<_74QAt8LcFLf5dh?w)3HltxXIGA&E&jk&H3eiizwBSxjw>w_DOktC>yD ztc({!QRN&s=EjW+Ck~vs!I1+8s<==!967o854gbZz3!R)&_<|={dn`H`*rtw{a*k2 zb)TA_cP}m;`>cLKh<}Q)PYL`@{Nv{!62F4z3DM5^f@l}A@}g)LGrlC+rHn6&b~)oK zqFu@Os%Td;z9!nWjIWD!UEG%Ft0ca}NG0(cT{|@Pdb9ijiiou?(d?5e`HO-O3hJ>+ zqzu%98fXm#pb~}#k3qrLgsO{eN3>looMYPHs(GOn#P*_SAK}-dMDg21+hal<7nqCo z39$)!k_ApNf13F-OwV#}F9~@~w9g5NaWQC3d42{n!~e@Vz=LLO)SvXCc)JjwhOAx{Z;n)z3Rgbrt!e^p56aE|%cgv8Jn zn15YJ40nLdX@RBLZbQW%wH1{O`6Ov3kk8S%(uwQ_2zO~ zNunJU`WNAqPvU|1fZn|5@wVzFuIq56x4i5bCQft^Zo1AOR{G8=m0Qk_iIR}? zk}W?mEsSXB@2X6gd98YW66`4wwmmF${qB}(x{mh49TMi}QeWxaAdZ75jGNv;&HQcx zA){1xptTAU?%FdI-cqcT<>OZWDF!kX57bW+8g5pb1a+C*J^2oa?cy=5hV3?&jAT6C zBgTnBZx^Mmvx%9w7fGIti4m!SWR&alqAn%}O)%V#X`YgSRM&b@xXIj4T<42{8gLhV zzq_N7*y94%*^Yux$(1cX3{{VH=ml}o@>ZfS@q;k-jDXiQ5+3;iO2vIR8{>qVG^F)M zwBe1FQ(?<{%L_KVFiOU1T6yzG&l=J67JH-aAm=*WXb>j)DUBB)>ay1^CZOd#j8#_h zJ=H@5P?&V1a8K#P+tAUj*HcM?;_+rAGu@Tz^kLM4z(gOWQpJg9<_u2kDny|49e}OzJh-X4{gh&cvyGZ=D5YJ6v zi3`hrQ@l{Yehd66&_QXyc-)B+Gq zt79TLF1Al_6F{M<1rk-bDVZ<@#anAM-W?KlFdQLLHP9{s^m59 z+EiqKv*-5)a3Xzdm}as@?n%(=d5@L1sY0a*svyfA)#O8623dRYAAFkU)2h)gb?Iq> zFrC{}2|`oTs!8iq!Q4?4e+taHuYT8a~y)-(J`=lm!B(|an8 z{Y{nDcKxKgb!SymAd{p{%Y#86(+bhIo29fItKLSsU|f-TuRWi=^927+=qqUd@2jit zKU!6L>&akawq{b5nQTf~~gN?30e zhu??mKa$dW{1lMjEm;_C5{4Kk}X=WXT7gDl9UrmMvve2ip6H z^Cw%_#%+hvVqWZR`Pg$GD^$hUAJ>}2v|^Ne4ZSxDtpBvauBH)wNLx+-DWh|7c*N*D zs+{mauJ#o)F^C8+?9_z5NG<`t4NNK!+7+x%J|>5BI@#WYNuJ!TVvja6gC3a>e3P-X z7GaPkr>9lF-&diu9!VFoIJLslCeG1FD`ry+$wtBgQCTbuj~Xk->JMXP^#C((f2wvP zcwuBI?`Z#rt(rTuw(6_Ue4l-NUnkbSC$sDutDkM(JX+-&n|0XTb)b8ORo+sTsW8&# z*-FzmEEj{vMmY^yXGsl~kv@h&vPE;rC3(X z+t}`u+bB#7m@So3zQorU%x0S8`J>P# z3*gJ(r=7hRqe-%!=c&IQ)BdEfW`$5W7k12c`JuWt3IRTLB>Tr>cpstWgRX4?nObYjMmmoUq<=XkX5`Z#V&eu)Ojtv#G*(kha89F4zb zm6pkff1vxg4kAhy3#SUlOGDSh4^diuwEjLj%0kA z;q1ul(S$&Hfe5XAUOM_!;%LrvlgKq9Eh29cd5g$(BHtkLO(Jg-d56fmM7~YrJtFTD z`GCj`BDaZLnIv?EWrNAecnzd+qFSsL%-6}DC}|Y&Mm6SNtd{uC(s=A8mv1c-_TPgLFvPOmoP@+tM z)B8XeO1N}H;CY@={IrY##0jExTQu`mcm;T;cfTEZ-o`yCSi{HLo z?A!N#?>%U$T(&QbO+NVG6(Rm127Wp8Z{dobqw(=yinb8-eqV~Z6uu#RQ#=EXPEPoF z?wi6d2!Dk80E@!6xStnWm~IQdB%Wb9WE|m_d1yrVqrxBKev!q-g+C$uN%71Ob&KA< zQaN{mt%!RKNjLn=^m+&P=%;96A)X3xjD1OQY=|S?^p~kzKdy_ zfr21u9kJ>sr0dBDH6KtzV8uwR{t8LK>vn_Cub7F6`H6{jlR`TPx8m(T2vvgoi*GI8 ze7GDuS&4hi=0nx$cH2Sq$=q8H8?|otpcZXE?0SvIL3}4@cLP=H9wg;d6|En+M>K^- z6fIdW9BCV4azfhDk&|-NP-Tqt4MoCqv7ulIwhvnghT1a7R{Lvo+;pZm<(XqM?dE6j zJa6)0OEH7FNANCfDIEFY63viefzX_|g)W(UQ{2ZOY&8#G*n0)mF<_m-;fif72W*{G z7qSoxb!r`L7Qb({0sFLDQCxG^;!f8J=f+pAMPudl_C} z{5M1m!f#~w2;;w}ut5053@OnVvO*b}pIOSI4YIZ@U$Fnm5k|qMOaEynVyAlNb zg(5gV7CR-Xo*nqi*vPgHjfFe7wsF0StBvaixb|>GxSF_@n7tJzv9j$&NuhT)jOT77 zg($t#^R+kiGu`R=#&!M7_gF#4q|j`(J6;StR=o!9dOt~?=pTXd1+8~t>pQ7ex3v)U zhg~lnTE@9uga1TdLnBIK(vqKZ83j|hkn8&)5uopfzd%HS6H+W@dJ>)CmWZ_4{i^JK z09hcsCeu;vgde7(aV|AIe3G2m2ym_Bl0-%%Tr&jtD9hI+5+vbZ$QX4f;%SoqoQ@(M6FcMLNFSM2 z73fN3{P2C-y8m*noMj3)Lj1D(?)v}mgA+%e9jmtoG8gBsQZmx^;#RmZ2WGsKIh6_Z zW%!nwM3WRY*~!%mJ(!5WoM#dfR7eUscB^wFO~F&4q!Eg8l$xgIJT(`nc@<5PqkUHL z>LMYR7@ z)D^7J=TQL?tzXX&?+5D(| zsww2QeD_KVi7*?Y)1zTZsP5Vt9s1RNv|YdIeio_KYU^i0aQY5*hQSmJhuYRP5_)YI zmtBVrSbdcmGCUi&Qm7X}+Vl73pEIGf1Ua3C#h(ox)p z;{@@q@FHCvmEaxHGRyLk>6lZ<;ZrD*)HgBS;9?FPy7yxzIVem)L5u6_=P;#?oG{f& z#XNjuTR#juKRw;M%WhM3cHNB)a3Jt@-7B-zIh~m*oB~rg%WwK|Fr8_oTr8AYiTQd$ z1x7^&dFCGT3N;OmO|LPmzJu4N+TUINBW6cjqZCyQ{V%BN?ipRvqH9>;Yt`%3x2iY3 zh|;=>%M?E`u~S{U4JuAyq#{+72B~z`4VP`>r#fpN`7_2dy=T#1ZP1$Zqr#6$_IGea z|Afg~NPeIoCrC9``??9J##ykvuLCIRU{vj*;Au4{}@> zrzPLNxg_JPhd?ls=FVSI3Ivf!+!Qkl6eDtC8td2w;FL&oZV+nvtO&sW=>4qrdo+`z4X7pV-0O=C* z9#64Zhp;g?g-gvS9-N|c{m*zxNl%#_oWeOj`rY6Zo$f#8DGpCL!bhskxYyINl#WNM znQjjh9nf^|I=(zikhbit!r*6pnZn+XC!eWfnP#Z(dPQ8Ff%|9|s3FU9H0tuiXo7x{2}^9bW!Msm#$CH-v=nbvS0p4?!k)%Y$Wf|2U}xX;Y-@6%_eeZ zi0m({g{>eA_Pc8FU$h{tUJ!?uPeTAJxzO%3a2AWTzO=du$rB3jC-fy)adbST8iv2+ zlH5+G6(%`4(w9^(F3q<%6xB-urmv3;SyO!jl8K4G0pPUpx3=djZMu^AR9fa z#itO7-bN$JRHPt*p|B|+iJ_I}P`Hd6qq3a4ATOBTmgi(yzTUrV$X5)tfVmZ0CtNDs zxKLwt`MZIO9?pWC9u&n&o-?p9-Xe$D;=*pn$0z$DWy*+_bEIQ9=bdTiE6)7bMW^JH F{sWOs1Iz#b literal 0 HcmV?d00001 diff --git a/__pycache__/packetIDs.cpython-35.pyc b/__pycache__/packetIDs.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f8406c808f484390446d27a5d75f67e1a3616e1 GIT binary patch literal 4034 zcmb`J*;*9I6374UD5C6}3pnG33-0^cY=U-UO9N_%Vo{L|1=W=`Rn_ou9%J6(ywUmk z_FVA-b1_$wQQLI$ajAU%pdzv&BV)_z@8~$C{{Cxu}LN0J;Fa2)YO!0gZr1L8IVHpiAK2K)->12mKDd47v=y0=fde3c3ou2D%2m4!RB= z1C4=ifNp?qf^LFufo_3sgKmSzLF3>%pgZ8Zpu6CEpnKr^p!?tlpaJ*vgUaAf zpikfh&;qyus(=?ki{K^D5_lQ34E_xI4E_T80{#m63MLSNzk$AiS3oP^@1XDCAD|!L zRnRKGkV*!-8{y^MVV4i-HlssNj;| zvfzr~s^FU7x?oIjLvT}YOK@8-F1RDOE4U}PFL)q$D0n1zEO;V#DtIP%E_fk$DR?D# zEqEh%D<}vi1e1a(!L(pT@J=u*m=nAgd=L}`CBeMlqo6GKBv=qs1dDw*n|BiIyd30y(VV1BIDII+6Q+Fo64 z^dzOTYHwKHx+>U_qrJvpDh!p!<@%cI*d-N)*1BqRq#9SR>v}}P>0_Y&sH-rlL{?M} zmwdJ&MeV81*WNmHB*nT~H^1kms3(22b**OVcRC1kK;5ZOc1`!A_EhFO#>mC=!MMt< znp>D$G6K)4sma)R>fNvZRCglkOr^55I2ck_lcbm&q(2pfzOv049lY0$r#Ls6%ZsWS zQD5t7H4_*o!bX1T#k= zR%CC`*>qb^?IeP1Cw4+#hbk5}I#Z**2~bh5t?lNLO%v}#lAY8Jlb7mlHV0AqPEynWLCWK*72goK(osg zTL&sFnwF3>DH^@aZ%pc%?<&)=gUu0K3@k6Ks$f=!(Luo~X%6#2kw(Vi+*VeGv1b1v z@{WkL^LoN$x@P&UZq4f`34c})@nXrHi9(C3Nq|yKHU#3;nLDiqn6<)e$JWqoIsQU2 z$?~ZYo2eK@-X(RY*^TUO8Uq*`x-jx>ffm{vD z*9C_~$5cYr!HKNEoKK-dvY={uTTwPo{Xj?B*6wmL%<_F|<^!gry!xDNuA6LbCMk<& z`J%FF`-5AShR9}Vpls!Ar-dHo>58;Jp(9U4c@|=$NrgxneUr4zMrelMj8~#nF`iX^#&YCuJ)P zcXYs%4UDGXR*2Ewa)! zu{KxOTv*ADkcFw`)|}CSZC=XG9PT(K8kP97IiDbwZR@&cMokz6jvblhSsiTFYkp{3 zfufc>p|EBeBu`6Xb5Yj=Pwl3r_vJu_LY#-mWJu6RYmSP_+D`7dbiscS{As?^V*Gt; z>iNo)+O9ha zGQNxt&{t2sfQLOFKP!1O;C zFctT@gsHse3Z@DQaus}|{D==;529aG1+H?g$}9S}M`YcEU=L6N9ArZT929s7rGheo zy@aCx;ZLB9IZOqq{!x+Cg0jt>$^`a`vwR22Q#eWxG%~flx{aUfdOqLX+&()`zaG+M z=2DFdpRPCBuZ+%iXwxQR&<*j%p?yCNZMIdm#t?m`SZJ*?pD)pcUXwYY=!eYu(9f4y zRiS3*VvCgzr-tb0uNr!JUAbE0JJbek=o-7K{HWHo#UjLu+EUrjn&+&z5WT&eTnwU+ zaPyn`3symVvi!oVBKQurPv-7y{AxDF?X}-57BjmtM&pa^*~?izG-fw+%Nb8_$&cJj zG&H*q(>#U7n2x&+0mwuos;{0X`TMP+9mw`h~%Lnj?w9tONu8WPvbNXe# KwF$bSx#S;D4vjki literal 0 HcmV?d00001 diff --git a/__pycache__/partMatchEvent.cpython-35.pyc b/__pycache__/partMatchEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3439763a70d60547e646b059a5ed7be1b46a7d0 GIT binary patch literal 547 zcmYjNO-sW-5S`7YO;Z!BMFdZRUd+Kl4}yq@(0UNH2o;1d{HM9bHO;$kX=neEVN0Yp zejJPFc7S>46|_ySA15ObYxXQn+(OdF=)!8+C&&tB-}UBsw<)G=mGydEIY^R7oJ{Md z-Jq5vc`faC*ORZOB388|*F_u@MuIw~y9i*|+~y85c@@3FU0!CgioC~V5ihw#K*Va~ X2J9fGOBdrwm__1}J}RZI4s(oOqUd{M literal 0 HcmV?d00001 diff --git a/__pycache__/passwordHelper.cpython-35.pyc b/__pycache__/passwordHelper.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca6cb16e4069b1d0e3bbf0220b190a6f7c58050d GIT binary patch literal 1158 zcmaKr&2G~`5XWa7=fkEUaDfw&53RsLjnt}C2qA=M4;)ab=m9Cp#j!VyTVq?hn?|XM zgj6Knf;+F|E2q8!C;qdk)09fA*X!BY*_qwnj30Ho{>HHR`CEhNH?1@W@>{(61BL`$ zP)0OZ%7O->);lzCC_!(~ci1#2aVcq98A6Maww0UgJY<) zLx$)Cf(>f~8HY|9bRp={_Q#hFojP#kod5K_y@`xZgLE8}k=C<9CC_z~nIJBdlDaJN zLkilmSrY4&$eC+<9=Dr`+730y}XxMDOJCmR}W(5^D#>fuP@(Z&YWKI zm5(vDG6eo|GHfDtAb>xzfTRKYS4_pq6iLCJ)wG#V!?AX{xV$)a03FDv9K8Nb6AY5!T(|Z z6+lBj3f=QhzW2@^O$Rr??y$Ladz3Wa0!Aw}H#ZUmmgdLM-b4H=&ach&b`-j*idBd8(>XI7ifQ>fE?_79}czAetuUgfX$REdl%_8(C zdao4#e+`{Khrxk=4tWTjWH^UTa+2xfkyAi#fdo)QP6@pQwSpA2>of0QG1|BDa)Ks} z7NKuK=OGLNp;tL1^5|6oT`GHT^5{(tUFFdwLYKe?lX+x-i-afwQA&v!AZAly4v2C} zXh7Ubi3$+al(>xyFx9Tlhrep&Ln21Z#}@8;7C*y`@PPW9;IS(PK%I|VMjSjKmO~iF zeabN9qX*dbT;hu-SkN;9t9=KbhHijuIt;)Bhb4|BEKMuN3XKLHal&GCtt@32c!6iR z{euNHSnlP@8(;qHu?Vj>P)2CwOSR zUSC;W4z+Iy2^t%odqG0&OkzvoNZX{oPi)~*KfEuo1SKX{?47 z&=RoM#?d4o;rxbelRz}KeVaP2e;O5!EjEdYa`)jvs+vmcrKqGf5zQSYfn_DwLo}BL zDw<6~TQ5W`uz@bgTNz=iJ5kMY2 zPf#I4#SE1)G?Ss(49#VzoFOekw=z`8P&GrhGc=!}g$&)v&|-!@%Fx{uwd)^;I7#=N zqrF|+vwX-MJS0Ps+wB+aqwTMZX3RIZn0N#W zNR0KGN+6r_nkm!x=dtUELF?)AqbsAS)J&rp+mvF{fNNrg5Ak*DUc&@+yGD;qDSh6p zPir+XZYrh6!0)OClR^mt6yPr8Hmgl|AL^ULuGM$&ZZ}JT!Xdci$iF9PDEgZ|v^k?a50<;f_u#WA6Gk!In3+CS2Y!Wm38u1D8R!#mMr!$&HoLO>+DB2nssq{SoNt z>U|Rf@HTLngXLSI8%=ptA?MV!RZoPes&YLS6=khZL_FMvY8h2zaY?K0ec<(zxi%dRt>a|eiS#jTUCEo?6{LL9d>WrXF$->R$s!FbspTGMLlq5n$ literal 0 HcmV?d00001 diff --git a/__pycache__/sendPrivateMessageEvent.cpython-35.pyc b/__pycache__/sendPrivateMessageEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4105619ebdf72ebf6b14600f312450f824143412 GIT binary patch literal 1402 zcmZux!EW0|5S=9{N}{C5bz-9d5@b^pCef#FUp|I<%R&cMq`tqF*@Go4s z9L&GNp<@g5|;e|rVYsoB%hT#NFOS}K2!^BZ^5iWaud!0enZ_JBxU*Q)RGLs+=94vjpC(k z+!@ykl?9Ykec~V})~;#BrSg~ABk(SijRR1BGQ5`l8=vl88eS_5|C}-S37s8unkVz1 zzB?0{;Zc~TBId7$BI8<4gv$M52~Btv5z~8rZiEi%)-lpnyK)>pmc~|>+})FvZN8k`K3#0_)5S^U z#bI~j(Vm5qwZq{^U(~^8_&S^yfW|h$O7!=?+<%3y+^7-yF|u%hCYA+g=5+CZ_os%N zp%^U{^GHt9IQz=*zTg8n&En3wt&yWFoCw>B(h+uPzi_4PDs^W6qASlXnqsR1w9-c_ z?Pc;@=v-!6*s92|K~rQwx|vWVOM_U-?X3v0G8I3FG#AP?vFDk2B2%eEZ2f^$lh9aK z8)bcwMKVU(eniXKdiziJeze|kD*M(i`mgP+V_}L9)KzlwqO=i+H<8HA2us=Z0=Xwm zm)!1_3${UREmgVCcXv7!d-=U>aQS5w9zS3kWVggu3etZ-aa{VawX=1!BW4F?Iv5HG*W96h)aD=fslp7E8#xQ+1j@g8BbHOjVBjq KV!|4iKj$O+*<>I9 literal 0 HcmV?d00001 diff --git a/__pycache__/sendPublicMessageEvent.cpython-35.pyc b/__pycache__/sendPublicMessageEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..456928a9313eeb9a73609919636cde75549b1dd3 GIT binary patch literal 2444 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 HXMywIpQoKu literal 0 HcmV?d00001 diff --git a/__pycache__/setAwayMessageEvent.cpython-35.pyc b/__pycache__/setAwayMessageEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09ca7703a99e0f3507ad888c0d2dd758b33f3152 GIT binary patch literal 687 zcmZWmO>Yx15FPJsHeU_>l8}%pvB&O#NF3l$Ap|AmMun70h$Ekl{9-Q zcYX)={u*C7@fSETUfL4C@{GsN^WMzJ4|~1NT{X#qrY&Rt2sBe ziSl!aCFK)wf$A>5t5J%(VH$->rHKREcJgQW45jT9weo-)uH4Vq?wI<$Wqbp*L)g#f zgQL*^7eiBy$0L;%MTSonPoIsFeo-v@dNTUEtCIzF+%J|kq)|nN?$@;w0}s~lzS*8GFRP_kmv(I1YUs3<;$(exQ8k6uQ(>Me dBb6;}ZAT+ap(n?#UKEW|T!**Kz9)fgM7=CT1>6$g|s03mNi5-wo4^#pP4ilOf>n4z<38_qjs2sA??V6@>X1mo2 z?S;Wl;74%d&Oh;$({5bh#Ow5f;rM;>7viEN=^Kkcm&TVCYp`G}9W@zFQt@^u&u zxK^gRDt;itAxA;-p&0W;8X@ZiQ?euFg^icxzGkRH$#7sp`axxtBERzu}OfRAC4bAX-ADLn>5r}dtu}A!uw;M>P9v(?pc_25^jH|x{Cm`#2RFS zmI7v0%^hcCNuDYG&=(P;^EK-y!&=JcKe? GJoGQS{nbDK literal 0 HcmV?d00001 diff --git a/__pycache__/startSpectatingEvent.cpython-35.pyc b/__pycache__/startSpectatingEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55fa84ae984c658e2b89a5f4c63bd3a9dfa57ede GIT binary patch literal 1333 zcmZ8hZEq7t5T3m~pI_n^60l1E2~&`eC5Y5YZKbF}RdG-$C_)$nb<+=*yG`Qr`S!ZI zwwl!W()5oYAte6Ne(fjzLe&qQS^FG|vv)i@JM%m<&z{_AG-|h7TW|gg0RDls$3y=I zTslUBD`t=a3|BsbAuHk@486kVFyw{r!_Y7M5)4a)AHXnx7)v<(jhJNuAN2fB$OQWl zG1G$`K^@m)T>1k8IGwOLKmu^?0XYoLIT#NT4s!+?-)Kyl(GZ9>C)&L7NY%~@VDi>#YT(>;>YEbiS>^xoJCk!ImU*hZ9&L}p%u(O4L5Tdr;i zgM_wrNpAh)R36&EVQRZ|EX*r{f1>2%wHv~gMfUqtOc56MGoe(z{!d7$=W|>HTmZNMijUu77<*CS49iuQf9RkW! z{3Oy`C|l3fC^L^`DwT+Ba3s|vG`2=!EHgH6D4NA-Q$CC+Ra^hX)BR_bYoo0Hh zmcQKV_X1m`N~eLXxzWU}M+R5|N9er}}*_G_g@V<8PQyZWFaVn|^q933E#=R=5@)mEgHtVn& z-{c)u$4G;B__o*fw)q40ExY1fW$GU0_bRH5Kk5dV5*hj8Nb=6HYb!1z8y#E0j*+(P* literal 0 HcmV?d00001 diff --git a/__pycache__/stopSpectatingEvent.cpython-35.pyc b/__pycache__/stopSpectatingEvent.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85d5dbae48ef6d7d6db199d6be03e6eae8a1fc3b GIT binary patch literal 995 zcmY*XO>fgc5S_JM=c7r}2HFcn7;%6cs00!{M5sbEsajD*6{TtkLCAGBACBWS>rF~R za-sYg&itD_Avkg8#EBWFNsE(xJ3H^~n;FX+`F!?Xp?JD&0sMj+kAd+ET=5}76aNeX zfche4P-pYJ0d->@n@~6Bu?2Mtnn;=O74I&Q$#Kd0jgCZ0WOUNXAq}WuWC_%PHQAZ5vhxb`ZRn5)~s|d3-SjrHg`7@ z*{wwOHe_&iioMc!x8V|io#6@JN^M-;aXwESS8^v5P9VZImrfjUPr0faww;TP;6qHQ z4j(xe9A($!c%AM(jOQsK<1!}358mB>?;OuO1iC2^l^rE|Jj5ExX^BDDEUjyM5OaA@ z(P`R4x6gI<+FM)gK%8qkxfbhUo2xhEuqQ?TWOgshKY2V-T@l85Bhl4_+9UJooX`cy z`(nr!&FB;lKMweS>pYs^U`MXk<0{s94{b!i_jwR;sdJI+hH6&?Lh`1zTSE3-rBmk~ zWnyg|9n@ZFGgflZbCP~}cv98o>uN=>(9~CD@jaGoouM(488rJK={uAr0*>fPoK7## z_C)awG@qd--kcVn38fm-ZhRuMd z&?DYN0A?E*mN7G|$Z{;h^5%xM$)2)RwrR?{$d_!H$4#yhED_`g(gegO?c4w|)#>Mb T(HsPPhv6yCBIM9L`5FHJ&$|BG literal 0 HcmV?d00001 diff --git a/__pycache__/systemHelper.cpython-35.pyc b/__pycache__/systemHelper.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20dd8957ed58bad9fe2a5030560b49fe21f14b57 GIT binary patch literal 2741 zcmaJ@-A)@v6h7-+ukFPKlC&f=kuo4vs?}inqf}Lj(xl|4C@Dg*X(NXSExUtRSa#i+ zSqM&$Dv{Ud1N5pFmHGnhJ5-w6UgfUWz3Mr$yI>NjhCMzz=gj$^Z#>g#dDo{d{_xZT z_#4JPHu87yk-s7d@!x_Npnu9+(6`F<3iK-_Z$sZMc?bGV$ycFYEqNFEZpqi6Un}`K z^y?6!DH~p+uLgv;9e26#8r^viqGsKQCLlzqXZR+B_}Vmli{k9ITW>IESPh7lp9(U| z_{hsh6u>TqZ^2###3l0CMW@}iGurhU54mE|Hd8yCNiGh!V3A}ZOOhxVuq+92VSRPw zDN6-g?Rj29aFvOKEi5dsp2#@ueOLPiOVOY>j3j3d{8)0Yp{v7GMlX5~+7(?%rFIg3 zpZ@})iUAt(@8Z2%8~6A@S7qDV8zLGGW4>_k>CKIxGaMduQECkq#6S1EPWh&$dxI|U&x#mWw!#aED%?yE(vzihF92k2QHsf zK-sYC05VpgUIkJIII+fLuds^1;JsEYrV@?dv@ww}|#cGgHd zKqd6mf)fA>+m1?`lP0d;Kuy814T%eCnwV2V6yqzKqAT|`u&JKkwg9|FH`w7}7IWs8 zeU&?z3e&@6QE;hzp|F{fC*hO)trzWc>?%|A{gLEBnuM~=RLTzhNU<%xoeCO}B&5M8 zMy0UChJLWel_YJBBXq=qIO2(-X)&(IFb)b0%#CvIVHC%#katLG2IagML#Bwm#y*$Q zAD949Q*uRmNr+?XD^?2M@}9?zv+8f9naU^p?GiLbbI<0Uvo$`%fnp}4m&Yth7}BB! zbGlYUsq1A4-@JztH2R#jcK^R3=S+1v9Z|=f(b^vXz18*w-NG`*i2IiCRu$ ztaYeeEcw2#bZsl36+AO*E31#RGljR?M~Ii^0KIq6XI}TX7Z1O?)JU4HeHaE2jzJ zX$7)u68ajMa|xZGfoq`fjOP%5j049diV&6{et>}_M5!Xfsj{v`8l&0UjFuE-Mh9v< zy}DrNG{;Lkh6P)Gc;bVF?j=7jYD6gKQ$q&LXZ}Eeti@Rq)O!Ou!Nuws5g&?#-Y2}b_<%iCQhuJ#8A@M3ze*7KZPsI_vW|D_zZ*l5}cM*_D_c`9d z4mr@1=gn(34@fsoo#=W{xER%5esZhx+4ixZmPJ4PW8cveV?N*5xU9W1w>>>E=Cy5` zeP^WI%qPu`c_J?0UU88OI2h8lc8NvuB=g|p$yxmTJR^*7&LY1!h`LKdj{{~G-!W^i%cFUfzu2|F7oHb`p zTbC+cMSO{p_GBI&p_2H52xThfiO}y1F-3%oh!2U-?+t@g=5(8BH+IG%IfVpWU)oRU Za=uMPl^-B!TE$=G^xK=d-gKL_{{UXSt78BF literal 0 HcmV?d00001 diff --git a/__pycache__/tokenList.cpython-35.pyc b/__pycache__/tokenList.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9335cc9d93a7eec07f54ba3b409e03778dd26eea GIT binary patch literal 4092 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 {} changed action: {} [{}][{}]".format(username, str(userToken.actionID), userToken.actionText, userToken.actionMd5)) diff --git a/changeMatchModsEvent.py b/changeMatchModsEvent.py new file mode 100644 index 0000000..fb4ee02 --- /dev/null +++ b/changeMatchModsEvent.py @@ -0,0 +1,43 @@ +import glob +import clientPackets +import matchModModes +import mods + +def handle(userToken, packetData): + # Get token data + userID = userToken.userID + + # Get packet data + packetData = clientPackets.changeMods(packetData) + + # Make sure the match exists + matchID = userToken.matchID + if matchID not in glob.matches.matches: + return + match = glob.matches.matches[matchID] + + # Set slot or match mods according to modType + if match.matchModMode == matchModModes.freeMod: + # Freemod + + # Host can set global DT/HT + if userID == match.hostUserID: + # If host has selected DT/HT and Freemod is enabled, set DT/HT as match mod + if (packetData["mods"] & mods.DoubleTime) > 0: + match.changeMatchMods(mods.DoubleTime) + # Nighcore + if (packetData["mods"] & mods.Nightcore) > 0: + match.changeMatchMods(match.mods+mods.Nightcore) + elif (packetData["mods"] & mods.HalfTime) > 0: + match.changeMatchMods(mods.HalfTime) + else: + # No DT/HT, set global mods to 0 (we are in freemod mode) + match.changeMatchMods(0) + + # Set slot mods + slotID = match.getUserSlotID(userID) + if slotID != None: + match.setSlotMods(slotID, packetData["mods"]) + else: + # Not freemod, set match mods + match.changeMatchMods(packetData["mods"]) diff --git a/changeMatchPasswordEvent.py b/changeMatchPasswordEvent.py new file mode 100644 index 0000000..210d50f --- /dev/null +++ b/changeMatchPasswordEvent.py @@ -0,0 +1,17 @@ +import clientPackets +import glob + +def handle(userToken, packetData): + # Read packet data. Same structure as changeMatchSettings + packetData = clientPackets.changeMatchSettings(packetData) + + # Make sure the match exists + matchID = userToken.matchID + if matchID not in glob.matches.matches: + return + + # Get our match + match = glob.matches.matches[matchID] + + # Update match password + match.changePassword(packetData["matchPassword"]) diff --git a/changeMatchSettingsEvent.py b/changeMatchSettingsEvent.py new file mode 100644 index 0000000..c82cda5 --- /dev/null +++ b/changeMatchSettingsEvent.py @@ -0,0 +1,109 @@ +import glob +import clientPackets +import matchModModes +import consoleHelper +import bcolors +import random +import matchTeamTypes +import matchTeams +import slotStatuses + +def handle(userToken, packetData): + # Read new settings + packetData = clientPackets.changeMatchSettings(packetData) + + # Get match ID + matchID = userToken.matchID + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Get match object + match = glob.matches.matches[matchID] + + # Some dank memes easter egg + memeTitles = [ + "RWC 2020", + "Fokabot is a duck", + "Dank memes", + "1337ms Ping", + "Iscriviti a Xenotoze", + "...e i marò?", + "Superman dies", + "The brace is on fire", + "print_foot()", + "#FREEZEBARKEZ", + "Ripple devs are actually cats", + "Thank Mr Shaural", + "NEVER GIVE UP", + "T I E D W I T H U N I T E D", + "HIGHEST HDHR LOBBY OF ALL TIME", + "This is gasoline and I set myself on fire", + "Everyone is cheating apparently", + "Kurwa mac", + "TATOE", + "This is not your drama landfill.", + "I like cheese", + "NYO IS NOT A CAT HE IS A DO(N)G", + "Datingu startuato" + ] + + # Set match name + match.matchName = packetData["matchName"] if packetData["matchName"] != "meme" else random.choice(memeTitles) + + # Update match settings + match.inProgress = packetData["inProgress"] + match.matchPassword = packetData["matchPassword"] + match.beatmapName = packetData["beatmapName"] + match.beatmapID = packetData["beatmapID"] + match.hostUserID = packetData["hostUserID"] + match.gameMode = packetData["gameMode"] + + oldBeatmapMD5 = match.beatmapMD5 + oldMods = match.mods + + match.mods = packetData["mods"] + match.beatmapMD5 = packetData["beatmapMD5"] + match.matchScoringType = packetData["scoringType"] + match.matchTeamType = packetData["teamType"] + match.matchModMode = packetData["freeMods"] + + # Reset ready if needed + if oldMods != match.mods or oldBeatmapMD5 != match.beatmapMD5: + for i in range(0,16): + if match.slots[i]["status"] == slotStatuses.ready: + match.slots[i]["status"] = slotStatuses.notReady + + # Reset mods if needed + if match.matchModMode == matchModModes.normal: + # Reset slot mods if not freeMods + for i in range(0,16): + match.slots[i]["mods"] = 0 + else: + # Reset match mods if freemod + match.mods = 0 + + # Set/reset teams + if match.matchTeamType == matchTeamTypes.teamVs or match.matchTeamType == matchTeamTypes.tagTeamVs: + # Set teams + c=0 + for i in range(0,16): + if match.slots[i]["team"] == matchTeams.noTeam: + match.slots[i]["team"] = matchTeams.red if c % 2 == 0 else matchTeams.blue + c+=1 + else: + # Reset teams + for i in range(0,16): + match.slots[i]["team"] = matchTeams.noTeam + + # Force no freemods if tag coop + if match.matchTeamType == matchTeamTypes.tagCoop or match.matchTeamType == matchTeamTypes.tagTeamVs: + match.matchModMode = matchModModes.normal + + # Send updated settings + match.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: Updated room settings".format(match.matchID), bcolors.BLUE) + #consoleHelper.printColored("> MPROOM{}: DEBUG: Host is {}".format(match.matchID, match.hostUserID), bcolors.PINK) diff --git a/changeSlotEvent.py b/changeSlotEvent.py new file mode 100644 index 0000000..b20b2db --- /dev/null +++ b/changeSlotEvent.py @@ -0,0 +1,18 @@ +import clientPackets +import glob +import consoleHelper +import bcolors + +def handle(userToken, packetData): + # Get usertoken data + userID = userToken.userID + username = userToken.username + + # Read packet data + packetData = clientPackets.changeSlot(packetData) + + # Get match + match = glob.matches.matches[userToken.matchID] + + # Change slot + match.userChangeSlot(userID, packetData["slotID"]) diff --git a/channel.py b/channel.py new file mode 100644 index 0000000..607e1d8 --- /dev/null +++ b/channel.py @@ -0,0 +1,78 @@ +class channel: + """ + A chat channel + + name -- channel name + description -- channel description + connectedUsers -- connected users IDs list + publicRead -- bool + publicWrite -- bool + moderated -- bool + """ + + name = "" + description = "" + connectedUsers = [] + + publicRead = False + publicWrite = False + moderated = False + + def __init__(self, __name, __description, __publicRead, __publicWrite): + """ + Create a new chat channel object + + __name -- channel name + __description -- channel description + __publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins + __publicWrite -- bool, same as public read but relative to write permissions + """ + + self.name = __name + self.description = __description + self.publicRead = __publicRead + self.publicWrite = __publicWrite + self.connectedUsers = [] + + + def userJoin(self, __userID): + """ + Add a user to connected users + + __userID -- user ID that joined the channel + """ + + if __userID not in self.connectedUsers: + self.connectedUsers.append(__userID) + + + def userPart(self, __userID): + """ + Remove a user from connected users + + __userID -- user ID that left the channel + """ + + connectedUsers = self.connectedUsers + if __userID in connectedUsers: + connectedUsers.remove(__userID) + + + def getConnectedUsers(self): + """ + Get connected user IDs list + + return -- connectedUsers list + """ + + return self.connectedUsers + + + def getConnectedUsersCount(self): + """ + Count connected users + + return -- connected users number + """ + + return len(self.connectedUsers) diff --git a/channelJoinEvent.py b/channelJoinEvent.py new file mode 100644 index 0000000..25aaf2b --- /dev/null +++ b/channelJoinEvent.py @@ -0,0 +1,56 @@ +""" +Event called when someone joins a channel +""" + +import clientPackets +import consoleHelper +import bcolors +import serverPackets +import glob +import exceptions + +def handle(userToken, packetData): + # Channel join packet + packetData = clientPackets.channelJoin(packetData) + joinChannel(userToken, packetData["channel"]) + +def joinChannel(userToken, channelName): + ''' + Join a channel + + userToken -- user token object of user that joins the chanlle + channelName -- name of channel + ''' + try: + # Get usertoken data + username = userToken.username + userID = userToken.userID + userRank = userToken.rank + + # Check spectator channel + # If it's spectator channel, skip checks and list stuff + if channelName != "#spectator" and channelName != "#multiplayer": + # Normal channel, do check stuff + # Make sure the channel exists + if channelName not in glob.channels.channels: + raise exceptions.channelUnknownException + + # Check channel permissions + if glob.channels.channels[channelName].publicRead == False and userRank <= 2: + raise exceptions.channelNoPermissionsException + + # Add our userID to users in that channel + glob.channels.channels[channelName].userJoin(userID) + + # Add the channel to our joined channel + userToken.joinChannel(channelName) + + # Send channel joined + userToken.enqueue(serverPackets.channelJoinSuccess(userID, channelName)) + + # Console output + consoleHelper.printColored("> {} joined channel {}".format(username, channelName), bcolors.GREEN) + except exceptions.channelNoPermissionsException: + consoleHelper.printColored("[!] {} attempted to join channel {}, but they have no read permissions".format(username, channelName), bcolors.RED) + except exceptions.channelUnknownException: + consoleHelper.printColored("[!] {} attempted to join an unknown channel ({})".format(username, channelName), bcolors.RED) diff --git a/channelList.py b/channelList.py new file mode 100644 index 0000000..e0e9a4b --- /dev/null +++ b/channelList.py @@ -0,0 +1,40 @@ +import glob +import channel + +class channelList: + """ + Channel list + + channels -- dictionary. key: channel name, value: channel object + """ + + channels = {} + + + def loadChannels(self): + """ + Load chat channels from db and add them to channels dictionary + """ + + # Get channels from DB + channels = glob.db.fetchAll("SELECT * FROM bancho_channels") + + # Add each channel if needed + for i in channels: + if i["name"] not in self.channels: + publicRead = True if i["public_read"] == 1 else False + publicWrite = True if i["public_write"] == 1 else False + self.addChannel(i["name"], i["description"], publicRead, publicWrite) + + + def addChannel(self, __name, __description, __publicRead, __publicWrite): + """ + Add a channel object to channels dictionary + + __name -- channel name + __description -- channel description + __publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins + __publicWrite -- bool, same as public read but relative to write permissions + """ + + self.channels[__name] = channel.channel(__name, __description, __publicRead, __publicWrite) diff --git a/channelPartEvent.py b/channelPartEvent.py new file mode 100644 index 0000000..9c34cd7 --- /dev/null +++ b/channelPartEvent.py @@ -0,0 +1,36 @@ +""" +Event called when someone parts a channel +""" + +import consoleHelper +import bcolors +import glob +import clientPackets +import serverPackets + +def handle(userToken, packetData): + # Channel part packet + packetData = clientPackets.channelPart(packetData) + partChannel(userToken, packetData["channel"]) + +def partChannel(userToken, channelName, kick = False): + # Get usertoken data + username = userToken.username + userID = userToken.userID + + # Remove us from joined users and joined channels + if channelName in glob.channels.channels: + # Check that user is in channel + if channelName in userToken.joinedChannels: + userToken.partChannel(channelName) + + # Check if user is in channel + if userID in glob.channels.channels[channelName].connectedUsers: + glob.channels.channels[channelName].userPart(userID) + + # Force close tab if needed + if kick == True: + userToken.enqueue(serverPackets.channelKicked(channelName)) + + # Console output + consoleHelper.printColored("> {} parted channel {}".format(username, channelName), bcolors.YELLOW) diff --git a/clientPackets.py b/clientPackets.py new file mode 100644 index 0000000..734cf1f --- /dev/null +++ b/clientPackets.py @@ -0,0 +1,143 @@ +""" Contains functions used to read specific client packets from byte stream """ +import dataTypes +import packetHelper +import slotStatuses + + +""" General packets """ +def userActionChange(stream): + return packetHelper.readPacketData(stream, + [ + ["actionID", dataTypes.byte], + ["actionText", dataTypes.string], + ["actionMd5", dataTypes.string], + ["actionMods", dataTypes.uInt32], + ["gameMode", dataTypes.byte] + ]) + + + +""" Client chat packets """ +def sendPublicMessage(stream): + return packetHelper.readPacketData(stream, + [ + ["unknown", dataTypes.string], + ["message", dataTypes.string], + ["to", dataTypes.string] + ]) + +def sendPrivateMessage(stream): + return packetHelper.readPacketData(stream, + [ + ["unknown", dataTypes.string], + ["message", dataTypes.string], + ["to", dataTypes.string], + ["unknown2", dataTypes.uInt32] + ]) + +def setAwayMessage(stream): + return packetHelper.readPacketData(stream, + [ + ["unknown", dataTypes.string], + ["awayMessage", dataTypes.string] + ]) + +def channelJoin(stream): + return packetHelper.readPacketData(stream,[["channel", dataTypes.string]]) + +def channelPart(stream): + return packetHelper.readPacketData(stream,[["channel", dataTypes.string]]) + +def addRemoveFriend(stream): + return packetHelper.readPacketData(stream, [["friendID", dataTypes.sInt32]]) + + + +""" SPECTATOR PACKETS """ +def startSpectating(stream): + return packetHelper.readPacketData(stream,[["userID", dataTypes.sInt32]]) + + +""" MULTIPLAYER PACKETS """ +def matchSettings(stream): + # Data to return, will be merged later + data = [] + + # Some settings + struct = [ + ["matchID", dataTypes.uInt16], + ["inProgress", dataTypes.byte], + ["unknown", dataTypes.byte], + ["mods", dataTypes.uInt32], + ["matchName", dataTypes.string], + ["matchPassword", dataTypes.string], + ["beatmapName", dataTypes.string], + ["beatmapID", dataTypes.uInt32], + ["beatmapMD5", dataTypes.string] + ] + + # Slot statuses (not used) + for i in range(0,16): + struct.append(["slot{}Status".format(str(i)), dataTypes.byte]) + + # Slot statuses (not used) + for i in range(0,16): + struct.append(["slot{}Team".format(str(i)), dataTypes.byte]) + + # Read first part + data.append(packetHelper.readPacketData(stream, struct)) + + # Skip userIDs because fuck + start = 7+2+1+1+4+4+16+16+len(data[0]["matchName"])+len(data[0]["matchPassword"])+len(data[0]["beatmapMD5"])+len(data[0]["beatmapName"]) + start += 1 if (data[0]["matchName"] == "") else 2 + start += 1 if (data[0]["matchPassword"] == "") else 2 + start += 2 # If beatmap name and MD5 don't change, the client sends \x0b\x00 istead of \x00 only, so always add 2. ...WHY! + start += 2 + for i in range(0,16): + s = data[0]["slot{}Status".format(str(i))] + if s != slotStatuses.free and s != slotStatuses.locked: + start += 4 + + # Other settings + struct = [ + ["hostUserID", dataTypes.sInt32], + ["gameMode", dataTypes.byte], + ["scoringType", dataTypes.byte], + ["teamType", dataTypes.byte], + ["freeMods", dataTypes.byte], + ] + + # Read last part + data.append(packetHelper.readPacketData(stream[start:], struct, False)) + + # Mods if freemod (not used) + #if data[1]["freeMods"] == 1: + + result = {} + for i in data: + result.update(i) + return result + +def createMatch(stream): + return matchSettings(stream) + +def changeMatchSettings(stream): + return matchSettings(stream) + +def changeSlot(stream): + return packetHelper.readPacketData(stream, [["slotID", dataTypes.uInt32]]) + +def joinMatch(stream): + return packetHelper.readPacketData(stream, [["matchID", dataTypes.uInt32], ["password", dataTypes.string]]) + +def changeMods(stream): + return packetHelper.readPacketData(stream, [["mods", dataTypes.uInt32]]) + +def lockSlot(stream): + return packetHelper.readPacketData(stream, [["slotID", dataTypes.uInt32]]) + +def transferHost(stream): + return packetHelper.readPacketData(stream, [["slotID", dataTypes.uInt32]]) + +def matchInvite(stream): + return packetHelper.readPacketData(stream, [["userID", dataTypes.uInt32]]) diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..f265d29 --- /dev/null +++ b/config.ini @@ -0,0 +1,24 @@ +[db] +host = localhost +username = root +password = meme +database = heidi +pingtime = 600 + +[server] +server = tornado +host = 0.0.0.0 +port = 5001 +outputpackets = 0 +outputrequesttime = 0 +localizeusers = 0 +timeouttime = 100 +timeoutlooptime = 100 + +[flask] +threaded = 1 +debug = 1 +logger = 0 + +[ci] +key=rippleburgrw15gofmustard diff --git a/config.py b/config.py new file mode 100644 index 0000000..b48ef44 --- /dev/null +++ b/config.py @@ -0,0 +1,107 @@ +import os +import configparser + +class config: + """ + config.ini object + + config -- list with ini data + default -- if true, we have generated a default config.ini + """ + + config = configparser.ConfigParser() + fileName = "" # config filename + default = True + + # Check if config.ini exists and load/generate it + def __init__(self, __file): + """ + Initialize a config object + + __file -- filename + """ + + self.fileName = __file + if os.path.isfile(self.fileName): + # config.ini found, load it + self.config.read(self.fileName) + self.default = False + else: + # config.ini not found, generate a default one + self.generateDefaultConfig() + self.default = True + + + # Check if config.ini has all needed the keys + def checkConfig(self): + """ + Check if this config has the required keys + + return -- True if valid, False if not + """ + + try: + # Try to get all the required keys + self.config.get("db","host") + self.config.get("db","username") + self.config.get("db","password") + self.config.get("db","database") + self.config.get("db","pingtime") + + self.config.get("server","server") + self.config.get("server","host") + self.config.get("server","port") + self.config.get("server","localizeusers") + self.config.get("server","outputpackets") + self.config.get("server","outputrequesttime") + self.config.get("server","timeouttime") + self.config.get("server","timeoutlooptime") + + if self.config["server"]["server"] == "flask": + # Flask only config + self.config.get("flask","threaded") + self.config.get("flask","debug") + self.config.get("flask","logger") + + self.config.get("ci","key") + return True + except: + return False + + + # Generate a default config.ini + def generateDefaultConfig(self): + """Open and set default keys for that config file""" + + # Open config.ini in write mode + f = open(self.fileName, "w") + + # Set keys to config object + self.config.add_section("db") + self.config.set("db", "host", "localhost") + self.config.set("db", "username", "root") + self.config.set("db", "password", "") + self.config.set("db", "database", "ripple") + self.config.set("db", "pingtime", "600") + + self.config.add_section("server") + self.config.set("server", "server", "tornado") + self.config.set("server", "host", "0.0.0.0") + self.config.set("server", "port", "5001") + self.config.set("server", "localizeusers", "1") + self.config.set("server", "outputpackets", "0") + self.config.set("server", "outputrequesttime", "0") + self.config.set("server", "timeoutlooptime", "100") + self.config.set("server", "timeouttime", "100") + + self.config.add_section("flask") + self.config.set("flask", "threaded", "1") + self.config.set("flask", "debug", "0") + self.config.set("flask", "logger", "0") + + self.config.add_section("ci") + self.config.set("ci", "key", "changeme") + + # Write ini to file and close + self.config.write(f) + f.close() diff --git a/consoleHelper.py b/consoleHelper.py new file mode 100644 index 0000000..3fcfb0b --- /dev/null +++ b/consoleHelper.py @@ -0,0 +1,71 @@ +"""Some console related functions""" + +import bcolors +import glob + +def printServerStartHeader(asciiArt): + """Print server start header with optional ascii art + + asciiArt -- if True, will print ascii art too""" + + if asciiArt == True: + print("{} _ __".format(bcolors.GREEN)) + print(" (_) / /") + print(" ______ __ ____ ____ / /____") + print(" / ___/ / _ \\/ _ \\/ / _ \\") + print(" / / / / /_) / /_) / / ____/") + print("/__/ /__/ .___/ .___/__/ \\_____/") + print(" / / / /") + print(" /__/ /__/\r\n") + print(" .. o .") + print(" o.o o . o") + print(" oo...") + print(" __[]__") + print(" nyo --> _\\:D/_/o_o_o_|__ u wot m8") + print(" \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/") + print(" \\ . .. .. . /") + print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^{}".format(bcolors.ENDC)) + + printColored("> Welcome to pep.py osu!bancho server v{}".format(glob.VERSION), bcolors.GREEN) + printColored("> Made by the Ripple team", bcolors.GREEN) + printColored("> {}https://github.com/osuripple/ripple".format(bcolors.UNDERLINE), bcolors.GREEN) + printColored("> Press CTRL+C to exit\n",bcolors.GREEN) + + +def printNoNl(string): + """ + Print string without new line at the end + + string -- string to print + """ + + print(string, end="") + + +def printColored(string, color): + """ + Print colored string + + string -- string to print + color -- see bcolors.py + """ + + print("{}{}{}".format(color, string, bcolors.ENDC)) + + +def printError(): + """Print error text FOR LOADING""" + + printColored("Error", bcolors.RED) + + +def printDone(): + """Print error text FOR LOADING""" + + printColored("Done", bcolors.GREEN) + + +def printWarning(): + """Print error text FOR LOADING""" + + printColored("Warning", bcolors.YELLOW) diff --git a/countryHelper.py b/countryHelper.py new file mode 100644 index 0000000..d5808d3 --- /dev/null +++ b/countryHelper.py @@ -0,0 +1,282 @@ +"""Contains all country codes with their osu numeric code""" + +countryCodes = { + "LV": 132, + "AD": 3, + "LT": 130, + "KM": 116, + "QA": 182, + "VA": 0, + "PK": 173, + "KI": 115, + "SS": 0, + "KH": 114, + "NZ": 166, + "TO": 215, + "KZ": 122, + "GA": 76, + "BW": 35, + "AX": 247, + "GE": 79, + "UA": 222, + "CR": 50, + "AE": 0, + "NE": 157, + "ZA": 240, + "SK": 196, + "BV": 34, + "SH": 0, + "PT": 179, + "SC": 189, + "CO": 49, + "GP": 86, + "GY": 93, + "CM": 47, + "TJ": 211, + "AF": 5, + "IE": 101, + "AL": 8, + "BG": 24, + "JO": 110, + "MU": 149, + "PM": 0, + "LA": 0, + "IO": 104, + "KY": 121, + "SA": 187, + "KN": 0, + "OM": 167, + "CY": 54, + "BQ": 0, + "BT": 33, + "WS": 236, + "ES": 67, + "LR": 128, + "RW": 186, + "AQ": 12, + "PW": 180, + "JE": 250, + "TN": 214, + "ZW": 243, + "JP": 111, + "BB": 20, + "VN": 233, + "HN": 96, + "KP": 0, + "WF": 235, + "EC": 62, + "HU": 99, + "GF": 80, + "GQ": 87, + "TW": 220, + "MC": 135, + "BE": 22, + "PN": 176, + "SZ": 205, + "CZ": 55, + "LY": 0, + "IN": 103, + "FM": 0, + "PY": 181, + "PH": 172, + "MN": 142, + "GG": 248, + "CC": 39, + "ME": 242, + "DO": 60, + "KR": 0, + "PL": 174, + "MT": 148, + "MM": 141, + "AW": 17, + "MV": 150, + "BD": 21, + "NR": 164, + "AT": 15, + "GW": 92, + "FR": 74, + "LI": 126, + "CF": 41, + "DZ": 61, + "MA": 134, + "VG": 0, + "NC": 156, + "IQ": 105, + "BN": 0, + "BF": 23, + "BO": 30, + "GB": 77, + "CU": 51, + "LU": 131, + "YT": 238, + "NO": 162, + "SM": 198, + "GL": 83, + "IS": 107, + "AO": 11, + "MH": 138, + "SE": 191, + "ZM": 241, + "FJ": 70, + "SL": 197, + "CH": 43, + "RU": 0, + "CW": 0, + "CX": 53, + "TF": 208, + "NL": 161, + "AU": 16, + "FI": 69, + "MS": 147, + "GH": 81, + "BY": 36, + "IL": 102, + "VC": 0, + "NG": 159, + "HT": 98, + "LS": 129, + "MR": 146, + "YE": 237, + "MP": 144, + "SX": 0, + "RE": 183, + "RO": 184, + "NP": 163, + "CG": 0, + "FO": 73, + "CI": 0, + "TH": 210, + "HK": 94, + "TK": 212, + "XK": 0, + "DM": 59, + "LC": 0, + "ID": 100, + "MG": 137, + "JM": 109, + "IT": 108, + "CA": 38, + "TZ": 221, + "GI": 82, + "KG": 113, + "NU": 165, + "TV": 219, + "LB": 124, + "SY": 0, + "PR": 177, + "NI": 160, + "KE": 112, + "MO": 0, + "SR": 201, + "VI": 0, + "SV": 203, + "HM": 0, + "CD": 0, + "BI": 26, + "BM": 28, + "MW": 151, + "TM": 213, + "GT": 90, + "AG": 0, + "UM": 0, + "US": 225, + "AR": 13, + "DJ": 57, + "KW": 120, + "MY": 153, + "FK": 71, + "EG": 64, + "BA": 0, + "CN": 48, + "GN": 85, + "PS": 178, + "SO": 200, + "IM": 249, + "GS": 0, + "BR": 31, + "GM": 84, + "PF": 170, + "PA": 168, + "PG": 171, + "BH": 25, + "TG": 209, + "GU": 91, + "CK": 45, + "MF": 252, + "VE": 230, + "CL": 46, + "TR": 217, + "UG": 223, + "GD": 78, + "TT": 218, + "TL": 0, + "MD": 0, + "MK": 0, + "ST": 202, + "CV": 52, + "MQ": 145, + "GR": 88, + "HR": 97, + "BZ": 37, + "UZ": 227, + "DK": 58, + "SN": 199, + "ET": 68, + "VU": 234, + "ER": 66, + "BJ": 27, + "LK": 127, + "NA": 155, + "AS": 14, + "SG": 192, + "PE": 169, + "IR": 0, + "MX": 152, + "TD": 207, + "AZ": 18, + "AM": 9, + "BL": 0, + "SJ": 195, + "SB": 188, + "NF": 158, + "RS": 239, + "DE": 56, + "EH": 65, + "EE": 63, + "SD": 190, + "ML": 140, + "TC": 206, + "MZ": 154, + "BS": 32, + "UY": 226, + "SI": 194, + "AI": 7 +} + + +def getCountryID(code): + """ + Get country ID for osu client + + code -- country name abbreviation (eg: US) + return -- country code int + """ + + if code in countryCodes: + return countryCodes[code] + else: + return 0 + +def getCountryLetters(code): + """ + Get country letters from osu country ID + + code -- country code int + return -- country name (2 letters) (XX if code not found) + """ + + for key, value in countryCodes.items(): + if value == code: + return key + + return "XX" diff --git a/createMatchEvent.py b/createMatchEvent.py new file mode 100644 index 0000000..3eca989 --- /dev/null +++ b/createMatchEvent.py @@ -0,0 +1,44 @@ +import serverPackets +import clientPackets +import glob +import consoleHelper +import bcolors +import joinMatchEvent +import exceptions + +def handle(userToken, packetData): + try: + # get usertoken data + userID = userToken.userID + + # Read packet data + packetData = clientPackets.createMatch(packetData) + + # Create a match object + # TODO: Player number check + matchID = glob.matches.createMatch(packetData["matchName"], packetData["matchPassword"], packetData["beatmapID"], packetData["beatmapName"], packetData["beatmapMD5"], packetData["gameMode"], userID) + + # Make sure the match has been created + if matchID not in glob.matches.matches: + raise exceptions.matchCreateError + + # Get match object + match = glob.matches.matches[matchID] + + # Join that match + joinMatchEvent.joinMatch(userToken, matchID, packetData["matchPassword"]) + + # Give host to match creator + match.setHost(userID) + + # Send match create packet to everyone in lobby + for i in glob.matches.usersInLobby: + # Make sure this user is still connected + token = glob.tokens.getTokenFromUserID(i) + if token != None: + token.enqueue(serverPackets.createMatch(matchID)) + + # Console output + consoleHelper.printColored("> MPROOM{}: Room created!".format(matchID), bcolors.BLUE) + except exceptions.matchCreateError: + consoleHelper.printColored("[!] Error while creating match!", bcolors.RED) diff --git a/crypt.py b/crypt.py new file mode 100644 index 0000000..0ec8edc --- /dev/null +++ b/crypt.py @@ -0,0 +1,302 @@ +# Huge thanks to Cairnarvon +# https://gist.github.com/Cairnarvon/5075687 + +# Initial permutation +IP = ( + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7, +) + +# Final permutation, FP = IP^(-1) +FP = ( + 40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25, +) + +# Permuted-choice 1 from the key bits to yield C and D. +# Note that bits 8,16... are left out: They are intended for a parity check. +PC1_C = ( + 57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, +) +PC1_D = ( + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4, +) + +# Permuted-choice 2, to pick out the bits from the CD array that generate the +# key schedule. +PC2_C = ( + 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, +) +PC2_D = ( + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32, +) + +# The C and D arrays are used to calculate the key schedule. +C = [0] * 28 +D = [0] * 28 + +# The key schedule. Generated from the key. +KS = [[0] * 48 for _ in range(16)] + +# The E bit-selection table. +E = [0] * 48 +e2 = ( + 32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1, +) + +# S-boxes. +S = ( + ( + 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13 + ), + ( + 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9 + ), + ( + 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12 + ), + ( + 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14 + ), + ( + 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3 + ), + ( + 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13 + ), + ( + 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12 + ), + ( + 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11 + ) +) + +# P is a permutation on the selected combination of the current L and key. +P = ( + 16, 7, 20, 21, + 29, 12, 28, 17, + 1, 15, 23, 26, + 5, 18, 31, 10, + 2, 8, 24, 14, + 32, 27, 3, 9, + 19, 13, 30, 6, + 22, 11, 4, 25, +) + +# The combination of the key and the input, before selection. +preS = [0] * 48 + + +def __setkey(key): + """ + Set up the key schedule from the encryption key. + """ + global C, D, KS, E + + shifts = (1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1) + + # First, generate C and D by permuting the key. The lower order bit of each + # 8-bit char is not used, so C and D are only 28 bits apiece. + for i in range(28): + C[i] = key[PC1_C[i] - 1] + D[i] = key[PC1_D[i] - 1] + + for i in range(16): + # rotate + for k in range(shifts[i]): + temp = C[0] + + for j in range(27): + C[j] = C[j + 1] + + C[27] = temp + temp = D[0] + for j in range(27): + D[j] = D[j + 1] + + D[27] = temp + + # get Ki. Note C and D are concatenated + for j in range(24): + KS[i][j] = C[PC2_C[j] - 1] + KS[i][j + 24] = D[PC2_D[j] - 28 - 1] + + # load E with the initial E bit selections + for i in range(48): + E[i] = e2[i] + +def __encrypt(block): + global preS + + left, right = [], [] # block in two halves + f = [0] * 32 + + # First, permute the bits in the input + for j in range(32): + left.append(block[IP[j] - 1]) + + for j in range(32, 64): + right.append(block[IP[j] - 1]) + + # Perform an encryption operation 16 times. + for i in range(16): + # Save the right array, which will be the new left. + old = right[:] + + # Expand right to 48 bits using the E selector and exclusive-or with + # the current key bits. + for j in range(48): + preS[j] = right[E[j] - 1] ^ KS[i][j] + + # The pre-select bits are now considered in 8 groups of 6 bits each. + # The 8 selection functions map these 6-bit quantities into 4-bit + # quantities and the results are permuted to make an f(R, K). + # The indexing into the selection functions is peculiar; it could be + # simplified by rewriting the tables. + for j in range(8): + temp = 6 * j + k = S[j][(preS[temp + 0] << 5) + + (preS[temp + 1] << 3) + + (preS[temp + 2] << 2) + + (preS[temp + 3] << 1) + + (preS[temp + 4] << 0) + + (preS[temp + 5] << 4)] + + temp = 4 * j + f[temp + 0] = (k >> 3) & 1 + f[temp + 1] = (k >> 2) & 1 + f[temp + 2] = (k >> 1) & 1 + f[temp + 3] = (k >> 0) & 1 + + # The new right is left ^ f(R, K). + # The f here has to be permuted first, though. + for j in range(32): + right[j] = left[j] ^ f[P[j] - 1] + + # Finally the new left (the original right) is copied back. + left = old + + # The output left and right are reversed. + left, right = right, left + + # The final output gets the inverse permutation of the very original + for j in range(64): + i = FP[j] + if i < 33: + block[j] = left[i - 1] + else: + block[j] = right[i - 33] + + return block + +def crypt(pw, salt): + iobuf = [] + + # break pw into 64 bits + block = [] + for c in pw: + c = ord(c) + for j in range(7): + block.append((c >> (6 - j)) & 1) + block.append(0) + block += [0] * (64 - len(block)) + + # set key based on pw + __setkey(block) + + for i in range(2): + # store salt at beginning of results + iobuf.append(salt[i]) + c = ord(salt[i]) + + if c > ord('Z'): + c -= 6 + + if c > ord('9'): + c -= 7 + + c -= ord('.') + + # use salt to effect the E-bit selection + for j in range(6): + if (c >> j) & 1: + E[6 * i + j], E[6 * i + j + 24] = E[6 * i + j + 24], E[6 * i + j] + + # call DES encryption 25 times using pw as key and initial data = 0 + block = [0] * 66 + for i in range(25): + block = __encrypt(block) + + # format encrypted block for standard crypt(3) output + for i in range(11): + c = 0 + for j in range(6): + c <<= 1 + c |= block[6 * i + j] + + c += ord('.') + if c > ord('9'): + c += 7 + + if c > ord('Z'): + c += 6 + + iobuf.append(chr(c)) + + return ''.join(iobuf) diff --git a/dataTypes.py b/dataTypes.py new file mode 100644 index 0000000..f391e26 --- /dev/null +++ b/dataTypes.py @@ -0,0 +1,12 @@ +"""Bancho packets data types""" +#TODO: Uppercase, maybe? +byte = 0 +uInt16 = 1 +sInt16 = 2 +uInt32 = 3 +sInt32 = 4 +uInt64 = 5 +sInt64 = 6 +string = 7 +ffloat = 8 # because float is a keyword +bbytes = 9 diff --git a/databaseHelper.py b/databaseHelper.py new file mode 100644 index 0000000..711bcc9 --- /dev/null +++ b/databaseHelper.py @@ -0,0 +1,137 @@ +import pymysql +import bcolors +import consoleHelper +import threading + +class db: + """A MySQL database connection""" + + connection = None + disconnected = False + pingTime = 600 + + def __init__(self, __host, __username, __password, __database, __pingTime = 600): + """ + Connect to MySQL database + + __host -- MySQL host name + __username -- MySQL username + __password -- MySQL password + __database -- MySQL database name + __pingTime -- MySQL database ping time (default: 600) + """ + + self.connection = pymysql.connect(host=__host, user=__username, password=__password, db=__database, cursorclass=pymysql.cursors.DictCursor, autocommit=True) + self.pingTime = __pingTime + self.pingLoop() + + + def bindParams(self, __query, __params): + """ + Replace every ? with the respective **escaped** parameter in array + + __query -- query with ?s + __params -- array with params + + return -- new query + """ + + for i in __params: + escaped = self.connection.escape(i) + __query = __query.replace("?", str(escaped), 1) + + return __query + + + def execute(self, __query, __params = None): + """ + Execute a SQL query + + __query -- query, can contain ?s + __params -- array with params. Optional + """ + + + with self.connection.cursor() as cursor: + try: + # Bind params if needed + if __params != None: + __query = self.bindParams(__query, __params) + + # Execute the query + cursor.execute(__query) + finally: + # Close this connection + cursor.close() + + + def fetch(self, __query, __params = None, __all = False): + """ + Fetch the first (or all) element(s) of SQL query result + + __query -- query, can contain ?s + __params -- array with params. Optional + __all -- if true, will fetch all values. Same as fetchAll + + return -- dictionary with result data or False if failed + """ + + + with self.connection.cursor() as cursor: + try: + # Bind params if needed + if __params != None: + __query = self.bindParams(__query, __params) + + # Execute the query with binded params + cursor.execute(__query) + + # Get first result and return it + if __all == False: + return cursor.fetchone() + else: + return cursor.fetchall() + finally: + # Close this connection + cursor.close() + + + def fetchAll(self, __query, __params = None): + """ + Fetch the all elements of SQL query result + + __query -- query, can contain ?s + __params -- array with params. Optional + + return -- dictionary with result data + """ + + return self.fetch(__query, __params, True) + + def pingLoop(self): + """ + Pings MySQL server. We need to ping/execute a query at least once every 8 hours + or the connection will die. + If called once, will recall after 30 minutes and so on, forever + CALL THIS FUNCTION ONLY ONCE! + """ + + # Default loop time + time = self.pingTime + + # Make sure the connection is alive + try: + # Try to ping and reconnect if not connected + self.connection.ping() + if self.disconnected == True: + # If we were disconnected, set disconnected to false and print message + self.disconnected = False + consoleHelper.printColored("> Reconnected to MySQL server!", bcolors.GREEN) + except: + # Can't ping MySQL server. Show error and call loop in 5 seconds + consoleHelper.printColored("[!] CRITICAL!! MySQL connection died! Make sure your MySQL server is running! Checking again in 5 seconds...", bcolors.RED) + self.disconnected = True + time = 5 + + # Schedule a new check (endless loop) + threading.Timer(time, self.pingLoop).start() diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..4035405 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,58 @@ +"""Bancho exceptions""" +# TODO: Prints in exceptions +class loginFailedException(Exception): + pass + +class loginBannedException(Exception): + pass + +class tokenNotFoundException(Exception): + pass + +class channelNoPermissionsException(Exception): + pass + +class channelUnknownException(Exception): + pass + +class channelModeratedException(Exception): + pass + +class noAdminException(Exception): + pass + +class commandSyntaxException(Exception): + pass + +class banchoConfigErrorException(Exception): + pass + +class banchoMaintenanceException(Exception): + pass + +class moderatedPMException(Exception): + pass + +class userNotFoundException(Exception): + pass + +class alreadyConnectedException(Exception): + pass + +class stopSpectating(Exception): + pass + +class matchWrongPasswordException(Exception): + pass + +class matchNotFoundException(Exception): + pass + +class matchJoinErrorException(Exception): + pass + +class matchCreateError(Exception): + pass + +class banchoRestartingException(Exception): + pass diff --git a/fokabot.py b/fokabot.py new file mode 100644 index 0000000..169668c --- /dev/null +++ b/fokabot.py @@ -0,0 +1,55 @@ +"""FokaBot related functions""" +import userHelper +import glob +import actions +import serverPackets +import fokabotCommands + +def connect(): + """Add FokaBot to connected users and send userpanel/stats packet to everyone""" + + token = glob.tokens.addToken(999) + token.actionID = actions.idle + glob.tokens.enqueueAll(serverPackets.userPanel(999)) + glob.tokens.enqueueAll(serverPackets.userStats(999)) + +def disconnect(): + """Remove FokaBot from connected users""" + + glob.tokens.deleteToken(glob.tokens.getTokenFromUserID(999)) + +def fokabotResponse(fro, chan, message): + """ + Check if a message has triggered fokabot (and return its response) + + fro -- sender username (for permissions stuff with admin commands) + chan -- channel name + message -- message + + return -- fokabot's response string or False + """ + + for i in fokabotCommands.commands: + # Loop though all commands + if i["trigger"] in message: + # message has triggered a command + + # Make sure the user has right permissions + if i["minRank"] > 1: + # Get rank from db only if minrank > 1, so we save some CPU + if userHelper.getRankPrivileges(userHelper.getID(fro)) < i["minRank"]: + return False + + # Check argument number + message = message.split(" ") + if i["syntax"] != "" and len(message) <= len(i["syntax"].split(" ")): + return "Wrong syntax: {} {}".format(i["trigger"], i["syntax"]) + + # Return response or execute callback + if i["callback"] == None: + return i["response"] + else: + return i["callback"](fro, chan, message[1:]) + + # No commands triggered + return False diff --git a/fokabotCommands.py b/fokabotCommands.py new file mode 100644 index 0000000..0bdd615 --- /dev/null +++ b/fokabotCommands.py @@ -0,0 +1,355 @@ +import fokabot +import random +import glob +import serverPackets +import exceptions +import userHelper +import time +import systemHelper + +""" +Commands callbacks + +Must have fro, chan and messages as arguments +fro -- name of who triggered the command +chan -- channel where the message was sent +message -- list containing arguments passed from the message + [0] = first argument + [1] = second argument + . . . + +return the message or **False** if there's no response by the bot +""" + +def faq(fro, chan, message): + if message[0] == "rules": + return "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23]." + elif message[0] == "rules": + return "Please make sure to check (Ripple's rules)[http://ripple.moe/?p=23]." + elif message[0] == "swearing": + return "Please don't abuse swearing" + elif message[0] == "spam": + return "Please don't spam" + elif message[0] == "offend": + return "Please don't offend other players" + elif message[0] == "github": + return "(Ripple's Github page!)[https://github.com/osuripple/ripple]" + elif message[0] == "discord": + return "(Join Ripple's Discord!)[https://discord.gg/0rJcZruIsA6rXuIx]" + elif message[0] == "blog": + return "You can find the latest Ripple news on the (blog)[https://ripple.moe/blog/]!" + elif message[0] == "changelog": + return "Check the (changelog)[https://ripple.moe/index.php?p=17] !" + elif message[0] == "status": + return "Check the server status (here!)[https://ripple.moe/index.php?p=27]" + +def roll(fro, chan, message): + maxPoints = 100 + if len(message) >= 1: + if message[0].isdigit() == True and int(message[0]) > 0: + maxPoints = int(message[0]) + + points = random.randrange(0,maxPoints) + return "{} rolls {} points!".format(fro, str(points)) + +def ask(fro, chan, message): + return random.choice(["yes", "no", "maybe"]) + +def alert(fro, chan, message): + glob.tokens.enqueueAll(serverPackets.notification(' '.join(message[:]))) + return False + +def moderated(fro, chan, message): + try: + # Make sure we are in a channel and not PM + if chan.startswith("#") == False: + raise exceptions.moderatedPMException + + # Get on/off + enable = True + if len(message) >= 1: + if message[0] == "off": + enable = False + + # Turn on/off moderated mode + glob.channels.channels[chan].moderated = enable + return "This channel is {} in moderated mode!".format("now" if enable else "no longer") + except exceptions.moderatedPMException: + return "You are trying to put a private chat in moderated mode. Are you serious?!? You're fired." + +def kickAll(fro, chan, message): + # Kick everyone but mods/admins + toKick = [] + for key, value in glob.tokens.tokens.items(): + if value.rank < 3: + toKick.append(key) + + # Loop though users to kick (we can't change dictionary size while iterating) + for i in toKick: + if i in glob.tokens.tokens: + glob.tokens.tokens[i].kick() + + return "Whoops! Rip everyone." + +def kick(fro, chan, message): + # Get parameters + target = message[0].replace("_", " ") + + # Get target token and make sure is connected + targetToken = glob.tokens.getTokenFromUsername(target) + if targetToken == None: + return "{} is not online".format(target) + + # Kick user + targetToken.kick() + + # Bot response + return "{} has been kicked from the server.".format(target) + +def fokabotReconnect(fro, chan, message): + # Check if fokabot is already connected + if glob.tokens.getTokenFromUserID(999) != None: + return"Fokabot is already connected to Bancho" + + # Fokabot is not connected, connect it + fokabot.connect() + return False + +def silence(fro, chan, message): + for i in message: + i = i.lower() + target = message[0].replace("_", " ") + amount = message[1] + unit = message[2] + reason = ' '.join(message[3:]) + + # Get target user ID + targetUserID = userHelper.getID(target) + + # Make sure the user exists + if targetUserID == False: + return "{}: user not found".format(target) + + # Calculate silence seconds + if unit == 's': + silenceTime = int(amount) + elif unit == 'm': + silenceTime = int(amount)*60 + elif unit == 'h': + silenceTime = int(amount)*3600 + elif unit == 'd': + silenceTime = int(amount)*86400 + else: + return "Invalid time unit (s/m/h/d)." + + # Max silence time is 7 days + if silenceTime > 604800: + return "Invalid silence time. Max silence time is 7 days." + + # Calculate silence end time + endTime = int(time.time())+silenceTime + + # Update silence end in db + userHelper.silence(targetUserID, endTime, reason) + + # Send silence packet to target if he's connected + targetToken = glob.tokens.getTokenFromUsername(target) + if targetToken != None: + targetToken.enqueue(serverPackets.silenceEndTime(silenceTime)) + + return "{} has been silenced for the following reason: {}".format(target, reason) + +def removeSilence(fro, chan, message): + # Get parameters + for i in message: + i = i.lower() + target = message[0].replace("_", " ") + + # Make sure the user exists + targetUserID = userHelper.getID(target) + if targetUserID == False: + return "{}: user not found".format(target) + + # Reset user silence time and reason in db + userHelper.silence(targetUserID, 0, "") + + # Send new silence end packet to user if he's online + targetToken = glob.tokens.getTokenFromUsername(target) + if targetToken != None: + targetToken.enqueue(serverPackets.silenceEndTime(0)) + + return "{}'s silence reset".format(target) + +def restartShutdown(restart): + """Restart (if restart = True) or shutdown (if restart = False) pep.py safely""" + msg = "We are performing some maintenance. Bancho will {} in 5 seconds. Thank you for your patience.".format("restart" if restart else "shutdown") + systemHelper.scheduleShutdown(5, restart, msg) + return msg + +def systemRestart(fro, chan, message): + return restartShutdown(True) + +def systemShutdown(fro, chan, message): + return restartShutdown(False) + +def systemReload(fro, chan, message): + #Reload settings from bancho_settings + glob.banchoConf.loadSettings() + + # Reload channels too + glob.channels.loadChannels() + + # Send new channels and new bottom icon to everyone + glob.tokens.enqueueAll(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) + glob.tokens.enqueueAll(serverPackets.channelInfoEnd()) + for key, _ in glob.channels.channels.items(): + glob.tokens.enqueueAll(serverPackets.channelInfo(key)) + + return "Bancho settings reloaded!" + +def systemMaintenance(fro, chan, message): + # Turn on/off bancho maintenance + maintenance = True + + # Get on/off + if len(message) >= 2: + if message[1] == "off": + maintenance = False + + # Set new maintenance value in bancho_settings table + glob.banchoConf.setMaintenance(maintenance) + + if maintenance == True: + # We have turned on maintenance mode + # Users that will be disconnected + who = [] + + # Disconnect everyone but mod/admins + for _, value in glob.tokens.tokens.items(): + if value.rank < 3: + who.append(value.userID) + + glob.tokens.enqueueAll(serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.")) + glob.tokens.multipleEnqueue(serverPackets.loginError(), who) + msg = "The server is now in maintenance mode!" + else: + # We have turned off maintenance mode + # Send message if we have turned off maintenance mode + msg = "The server is no longer in maintenance mode!" + + # Chat output + return msg + +def systemStatus(fro, chan, message): + # Print some server info + data = systemHelper.getSystemInfo() + + # Final message + msg = "=== PEP.PY STATS ===\n" + msg += "Running pep.py server\n" + msg += "Webserver: {}\n".format(data["webServer"]) + msg += "\n" + msg += "=== BANCHO STATS ===\n" + msg += "Connected users: {}\n".format(str(data["connectedUsers"])) + msg += "\n" + msg += "=== SYSTEM STATS ===\n" + msg += "CPU: {}%\n".format(str(data["cpuUsage"])) + msg += "RAM: {}GB/{}GB\n".format(str(data["usedMemory"]), str(data["totalMemory"])) + if data["unix"] == True: + msg += "Load average: {}/{}/{}\n".format(str(data["loadAverage"][0]), str(data["loadAverage"][1]), str(data["loadAverage"][2])) + + return msg + +""" +Commands list + +trigger: message that triggers the command +callback: function to call when the command is triggered. Optional. +response: text to return when the command is triggered. Optional. +syntax: command syntax. Arguments must be separated by spaces (eg: ) +minRank: minimum rank to execute that command. Optional (default = 1) + +You MUST set trigger and callback/response, or the command won't work. +""" +commands = [ + { + "trigger": "!roll", + "callback": roll + }, { + "trigger": "!faq", + "syntax": "", + "callback": faq + }, { + "trigger": "!report", + "response": "Report command isn't here yet :c" + }, { + "trigger": "!help", + "response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for FokaBot's full command list" + }, { + "trigger": "!ask", + "syntax": "", + "callback": ask + }, { + "trigger": "!mm00", + "response": random.choice(["meme", "MA MAURO ESISTE?"]) + }, { + "trigger": "!alert", + "syntax": "", + "minRank": 4, + "callback": alert + }, { + "trigger": "!moderated", + "minRank": 3, + "callback": moderated + }, { + "trigger": "!kickall", + "minRank": 4, + "callback": kickAll + }, { + "trigger": "!kick", + "syntax": "", + "minRank": 3, + "callback": kick + }, { + "trigger": "!fokabot reconnect", + "minRank": 3, + "callback": fokabotReconnect + }, { + "trigger": "!silence", + "syntax": " ", + "minRank": 3, + "callback": silence + }, { + "trigger": "!removesilence", + "syntax": "", + "minRank": 3, + "callback": removeSilence + }, { + "trigger": "!system restart", + "minRank": 4, + "callback": systemRestart + }, { + "trigger": "!system shutdown", + "minRank": 4, + "callback": systemShutdown + }, { + "trigger": "!system reload", + "minRank": 3, + "callback": systemReload + }, { + "trigger": "!system maintenance", + "minRank": 3, + "callback": systemMaintenance + }, { + "trigger": "!system status", + "minRank": 3, + "callback": systemStatus + } +] + +# Commands list default values +for cmd in commands: + cmd.setdefault("syntax", "") + cmd.setdefault("minRank", 1) + cmd.setdefault("callback", None) + cmd.setdefault("response", "u w0t m8?") diff --git a/friendAddEvent.py b/friendAddEvent.py new file mode 100644 index 0000000..9ccf5dd --- /dev/null +++ b/friendAddEvent.py @@ -0,0 +1,10 @@ +import userHelper +import clientPackets + +def handle(userToken, packetData): + # Friend add packet + packetData = clientPackets.addRemoveFriend(packetData) + userHelper.addFriend(userToken.userID, packetData["friendID"]) + + # Console output + print("> {} have added {} to their friends".format(userToken.username, str(packetData["friendID"]))) diff --git a/friendRemoveEvent.py b/friendRemoveEvent.py new file mode 100644 index 0000000..450d369 --- /dev/null +++ b/friendRemoveEvent.py @@ -0,0 +1,10 @@ +import userHelper +import clientPackets + +def handle(userToken, packetData): + # Friend remove packet + packetData = clientPackets.addRemoveFriend(packetData) + userHelper.removeFriend(userToken.userID, packetData["friendID"]) + + # Console output + print("> {} have removed {} from their friends".format(userToken.username, str(packetData["friendID"]))) diff --git a/gameModes.py b/gameModes.py new file mode 100644 index 0000000..8716996 --- /dev/null +++ b/gameModes.py @@ -0,0 +1,23 @@ +"""Contains readable gamemodes with their codes""" +std = 0 +taiko = 1 +ctb = 2 +mania = 3 + +def getGameModeForDB(gameMode): + """ + Convert a gamemode number to string for database table/column + + gameMode -- gameMode int or variable (ex: gameMode.std) + + return -- game mode readable string for db + """ + + if gameMode == std: + return "std" + elif gameMode == taiko: + return "taiko" + elif gameMode == ctb: + return "ctb" + else: + return "mania" diff --git a/generalFunctions.py b/generalFunctions.py new file mode 100644 index 0000000..6fd96d6 --- /dev/null +++ b/generalFunctions.py @@ -0,0 +1,22 @@ +"""Some functions that don't fit in any other file""" + +def stringToBool(s): + """ + Convert a string (True/true/1) to bool + + s -- string/int value + return -- True/False + """ + + return (s == "True" or s== "true" or s == "1" or s == 1) + + +def hexString(s): + """ + Output s' bytes in HEX + + s -- string + return -- string with hex value + """ + + return ":".join("{:02x}".format(ord(c)) for c in s) diff --git a/glob.py b/glob.py new file mode 100644 index 0000000..42a6f93 --- /dev/null +++ b/glob.py @@ -0,0 +1,16 @@ +"""Global objects and variables""" + +import tokenList +import channelList +import matchList + +VERSION = "0.9" + +db = None +conf = None +banchoConf = None +tokens = tokenList.tokenList() +channels = channelList.channelList() +matches = matchList.matchList() +memes = True +restarting = False diff --git a/joinLobbyEvent.py b/joinLobbyEvent.py new file mode 100644 index 0000000..8f639c6 --- /dev/null +++ b/joinLobbyEvent.py @@ -0,0 +1,19 @@ +import serverPackets +import glob +import consoleHelper +import bcolors + +def handle(userToken, _): + # Get userToken data + username = userToken.username + userID = userToken.userID + + # Add user to users in lobby + glob.matches.lobbyUserJoin(userID) + + # Send matches data + for key, _ in glob.matches.matches.items(): + userToken.enqueue(serverPackets.createMatch(key)) + + # Console output + consoleHelper.printColored("> {} has joined multiplayer lobby".format(username), bcolors.BLUE) diff --git a/joinMatchEvent.py b/joinMatchEvent.py new file mode 100644 index 0000000..a0e0bb9 --- /dev/null +++ b/joinMatchEvent.py @@ -0,0 +1,60 @@ +import clientPackets +import serverPackets +import glob +import consoleHelper +import bcolors +import exceptions + +def handle(userToken, packetData): + # read packet data + packetData = clientPackets.joinMatch(packetData) + + # Get match from ID + joinMatch(userToken, packetData["matchID"], packetData["password"]) + + +def joinMatch(userToken, matchID, password): + try: + # TODO: leave other matches + # TODO: Stop spectating + + # get usertoken data + userID = userToken.userID + username = userToken.username + + # Make sure the match exists + if matchID not in glob.matches.matches: + raise exceptions.matchNotFoundException + + # Match exists, get object + match = glob.matches.matches[matchID] + + # Check password + # TODO: Admins can enter every match + if match.matchPassword != "": + if match.matchPassword != password: + raise exceptions.matchWrongPasswordException + + # Password is correct, join match + result = match.userJoin(userID) + + # Check if we've joined the match successfully + if result == False: + raise exceptions.matchJoinErrorException + + # Match joined, set matchID for usertoken + userToken.joinMatch(matchID) + + # Send packets + userToken.enqueue(serverPackets.matchJoinSuccess(matchID)) + userToken.enqueue(serverPackets.channelJoinSuccess(userID, "#multiplayer")) + userToken.enqueue(serverPackets.sendMessage("FokaBot", "#multiplayer", "Hi {}, and welcome to Ripple's multiplayer mode! This feature is still WIP and might have some issues. If you find any bugs, please report them (by clicking here)[https://ripple.moe/index.php?p=22].".format(username))) + except exceptions.matchNotFoundException: + userToken.enqueue(serverPackets.matchJoinFail()) + consoleHelper.printColored("[!] {} has tried to join a mp room, but it doesn't exist".format(userToken.username), bcolors.RED) + except exceptions.matchWrongPasswordException: + userToken.enqueue(serverPackets.matchJoinFail()) + consoleHelper.printColored("[!] {} has tried to join a mp room, but he typed the wrong password".format(userToken.username), bcolors.RED) + except exceptions.matchJoinErrorException: + userToken.enqueue(serverPackets.matchJoinFail()) + consoleHelper.printColored("[!] {} has tried to join a mp room, but an error has occured".format(userToken.username), bcolors.RED) diff --git a/locationHelper.py b/locationHelper.py new file mode 100644 index 0000000..1c09602 --- /dev/null +++ b/locationHelper.py @@ -0,0 +1,48 @@ +import urllib.request +import json + +import consoleHelper +import bcolors + +# API URL +url = "http://ip.zxq.co/" + + +def getCountry(ip): + """ + Get country from IP address + + ip -- IP Address + return -- Country code (2 letters) + """ + + # Default value, sent if API is memeing + country = "XX" + + try: + # Try to get country from Pikolo Aul's Go-Sanic ip API + country = json.loads(urllib.request.urlopen("{}/{}".format(url, ip)).read().decode())["country"] + except: + consoleHelper.printColored("[!] Error in get country", bcolors.RED) + + return country + + +def getLocation(ip): + """ + Get latitude and longitude from IP address + + ip -- IP address + return -- [latitude, longitude] + """ + + # Default value, sent if API is memeing + data = [0,0] + + try: + # Try to get position from Pikolo Aul's Go-Sanic ip API + data = json.loads(urllib.request.urlopen("{}/{}".format(url, ip)).read().decode())["loc"].split(",") + except: + consoleHelper.printColored("[!] Error in get position", bcolors.RED) + + return [float(data[0]), float(data[1])] diff --git a/loginEvent.py b/loginEvent.py new file mode 100644 index 0000000..43ee8bb --- /dev/null +++ b/loginEvent.py @@ -0,0 +1,172 @@ +import userHelper +import serverPackets +import exceptions +import glob +import consoleHelper +import bcolors +import locationHelper +import countryHelper +import time +import generalFunctions +import channelJoinEvent + +def handle(flaskRequest): + # Data to return + responseTokenString = "ayy" + responseData = bytes() + + # Get IP from flask request + requestIP = flaskRequest.headers.get('X-Real-IP') + if requestIP == None: + requestIP = flaskRequest.remote_addr + + # Console output + print("> Accepting connection from {}...".format(requestIP)) + + # Split POST body so we can get username/password/hardware data + # 2:-3 thing is because requestData has some escape stuff that we don't need + loginData = str(flaskRequest.data)[2:-3].split("\\n") + + # Process login + print("> Processing login request for {}...".format(loginData[0])) + try: + # If true, print error to console + err = False + + # Try to get the ID from username + userID = userHelper.getID(str(loginData[0])) + + if userID == False: + # Invalid username + raise exceptions.loginFailedException() + if userHelper.checkLogin(userID, loginData[1]) == False: + # Invalid password + raise exceptions.loginFailedException() + + # Make sure we are not banned + userAllowed = userHelper.getAllowed(userID) + if userAllowed == 0: + # Banned + raise exceptions.loginBannedException() + + # No login errors! + # Delete old tokens for that user and generate a new one + glob.tokens.deleteOldTokens(userID) + responseToken = glob.tokens.addToken(userID) + responseTokenString = responseToken.token + + # Get silence end + userSilenceEnd = max(0, userHelper.getSilenceEnd(userID)-int(time.time())) + + # Get supporter/GMT + userRank = userHelper.getRankPrivileges(userID) + userGMT = False + userSupporter = True + if userRank >= 3: + userGMT = True + + # Server restarting check + if glob.restarting == True: + raise exceptions.banchoRestartingException() + + # Maintenance check + if glob.banchoConf.config["banchoMaintenance"] == True: + if userGMT == False: + # We are not mod/admin, delete token, send notification and logout + glob.tokens.deleteToken(responseTokenString) + raise exceptions.banchoMaintenanceException() + else: + # We are mod/admin, send warning notification and continue + responseToken.enqueue(serverPackets.notification("Bancho is in maintenance mode. Only mods/admins have full access to the server.\nType !system maintenance off in chat to turn off maintenance mode.")) + + # Send all needed login packets + responseToken.enqueue(serverPackets.silenceEndTime(userSilenceEnd)) + responseToken.enqueue(serverPackets.userID(userID)) + responseToken.enqueue(serverPackets.protocolVersion()) + responseToken.enqueue(serverPackets.userSupporterGMT(userSupporter, userGMT)) + responseToken.enqueue(serverPackets.userPanel(userID)) + responseToken.enqueue(serverPackets.userStats(userID)) + + # Channel info end (before starting!?! wtf bancho?) + responseToken.enqueue(serverPackets.channelInfoEnd()) + + # Default opened channels + # TODO: Configurable default channels + channelJoinEvent.joinChannel(responseToken, "#osu") + channelJoinEvent.joinChannel(responseToken, "#announce") + if userRank >= 3: + # Join admin chanenl if we are mod/admin + # TODO: Separate channels for mods and admins + channelJoinEvent.joinChannel(responseToken, "#admin") + + # Output channels info + for key, value in glob.channels.channels.items(): + if value.publicRead == True: + responseToken.enqueue(serverPackets.channelInfo(key)) + + responseToken.enqueue(serverPackets.friendList(userID)) + + # Send main menu icon and login notification if needed + if glob.banchoConf.config["menuIcon"] != "": + responseToken.enqueue(serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"])) + + if glob.banchoConf.config["loginNotification"] != "": + responseToken.enqueue(serverPackets.notification(glob.banchoConf.config["loginNotification"])) + + # Get everyone else userpanel + # TODO: Better online users handling + for key, value in glob.tokens.tokens.items(): + responseToken.enqueue(serverPackets.userPanel(value.userID)) + responseToken.enqueue(serverPackets.userStats(value.userID)) + + # Send online users IDs array + responseToken.enqueue(serverPackets.onlineUsers()) + + # Get location and country from ip.zxq.co or database + if generalFunctions.stringToBool(glob.conf.config["server"]["localizeusers"]): + # Get location and country from IP + location = locationHelper.getLocation(requestIP) + country = countryHelper.getCountryID(locationHelper.getCountry(requestIP)) + else: + # Set location to 0,0 and get country from db + print("[!] Location skipped") + location = [0,0] + country = countryHelper.getCountryID(userHelper.getCountry(userID)) + + # Set location and country + responseToken.setLocation(location) + responseToken.setCountry(country) + + # Send to everyone our userpanel and userStats (so they now we have logged in) + glob.tokens.enqueueAll(serverPackets.userPanel(userID)) + glob.tokens.enqueueAll(serverPackets.userStats(userID)) + + # Set reponse data to right value and reset our queue + responseData = responseToken.queue + responseToken.resetQueue() + + # Print logged in message + consoleHelper.printColored("> {} logged in ({})".format(loginData[0], responseToken.token), bcolors.GREEN) + except exceptions.loginFailedException: + # Login failed error packet + # (we don't use enqueue because we don't have a token since login has failed) + err = True + responseData += serverPackets.loginFailed() + except exceptions.loginBannedException: + # Login banned error packet + err = True + responseData += serverPackets.loginBanned() + except exceptions.banchoMaintenanceException: + # Bancho is in maintenance mode + responseData += serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.") + responseData += serverPackets.loginError() + except exceptions.banchoRestartingException: + # Bancho is restarting + responseData += serverPackets.notification("Bancho is restarting. Try again in a few minutes.") + responseData += serverPackets.loginError() + finally: + # Print login failed message to console if needed + if err == True: + consoleHelper.printColored("> {}'s login failed".format(loginData[0]), bcolors.YELLOW) + + return (responseTokenString, responseData) diff --git a/logoutEvent.py b/logoutEvent.py new file mode 100644 index 0000000..fbbff40 --- /dev/null +++ b/logoutEvent.py @@ -0,0 +1,39 @@ +import glob +import consoleHelper +import bcolors +import serverPackets +import time + +def handle(userToken, _): + # get usertoken data + userID = userToken.userID + username = userToken.username + requestToken = userToken.token + + # Big client meme here. If someone logs out and logs in right after, + # the old logout packet will still be in the queue and will be sent to + # the server, so we accept logout packets sent at least 5 seconds after login + # if the user logs out before 5 seconds, he will be disconnected later with timeout check + if int(time.time()-userToken.loginTime) >= 5: + # Stop spectating if needed + if userToken.spectating != 0: + # The user was spectating someone + spectatorHostToken = glob.tokens.getTokenFromUserID(userToken.spectating) + if spectatorHostToken != None: + # The host is still online, send removeSpectator to him + spectatorHostToken.enqueue(serverPackets.removeSpectator(userID)) + + # Part all joined channels + for i in userToken.joinedChannels: + glob.channels.channels[i].userPart(userID) + + # TODO: Lobby left if joined + + # Enqueue our disconnection to everyone else + glob.tokens.enqueueAll(serverPackets.userLogout(userID)) + + # Delete token + glob.tokens.deleteToken(requestToken) + + # Console output + consoleHelper.printColored("> {} have been disconnected.".format(username), bcolors.YELLOW) diff --git a/match.py b/match.py new file mode 100644 index 0000000..a309ba0 --- /dev/null +++ b/match.py @@ -0,0 +1,656 @@ +# TODO: Enqueue all +import gameModes +import matchScoringTypes +import matchTeamTypes +import matchModModes +import slotStatuses +import glob +import consoleHelper +import bcolors +import serverPackets +import dataTypes +import matchTeams + +class match: + """Multiplayer match object""" + matchID = 0 + inProgress = False + mods = 0 + matchName = "" + matchPassword = "" + beatmapName = "" + beatmapID = 0 + beatmapMD5 = "" + slots = [] # list of dictionaries {"status": 0, "team": 0, "userID": -1, "mods": 0, "loaded": False, "skip": False, "complete": False} + hostUserID = 0 + gameMode = gameModes.std + matchScoringType = matchScoringTypes.score + matchTeamType = matchTeamTypes.headToHead + matchModMode = matchModModes.normal + seed = 0 + + def __init__(self, __matchID, __matchName, __matchPassword, __beatmapID, __beatmapName, __beatmapMD5, __gameMode, __hostUserID): + """ + Create a new match object + + __matchID -- match progressive identifier + __matchName -- match name, string + __matchPassword -- match md5 password. Leave empty for no password + __beatmapID -- beatmap ID + __beatmapName -- beatmap name, string + __beatmapMD5 -- beatmap md5 hash, string + __gameMode -- game mode ID. See gameModes.py + __hostUserID -- user id of the host + """ + self.matchID = __matchID + self.inProgress = False + self.mods = 0 + self.matchName = __matchName + self.matchPassword = __matchPassword + self.beatmapID = __beatmapID + self.beatmapName = __beatmapName + self.beatmapMD5 = __beatmapMD5 + self.hostUserID = __hostUserID + self.gameMode = __gameMode + self.matchScoringTypes = matchScoringTypes.score # default values + self.matchTeamType = matchTeamTypes.headToHead # default value + self.matchModMode = matchModModes.normal # default value + self.seed = 0 + + # Create all slots and reset them + self.slots = [] + for _ in range(0,16): + self.slots.append({"status": slotStatuses.free, "team": 0, "userID": -1, "mods": 0, "loaded": False, "skip": False, "complete": False}) + + + def getMatchData(self): + """ + Return binary match data structure for packetHelper + """ + # General match info + struct = [ + [self.matchID, dataTypes.uInt16], + [int(self.inProgress), dataTypes.byte], + [0, dataTypes.byte], + [self.mods, dataTypes.uInt32], + [self.matchName, dataTypes.string], + [self.matchPassword, dataTypes.string], + [self.beatmapName, dataTypes.string], + [self.beatmapID, dataTypes.uInt32], + [self.beatmapMD5, dataTypes.string], + ] + + # Slots status IDs, always 16 elements + for i in range(0,16): + struct.append([self.slots[i]["status"], dataTypes.byte]) + + # Slot teams, always 16 elements + for i in range(0,16): + struct.append([self.slots[i]["team"], dataTypes.byte]) + + # Slot user ID. Write only if slot is occupied + for i in range(0,16): + uid = self.slots[i]["userID"] + if uid > -1: + struct.append([uid, dataTypes.uInt32]) + + # Other match data + struct.extend([ + [self.hostUserID, dataTypes.sInt32], + [self.gameMode, dataTypes.byte], + [self.matchScoringType, dataTypes.byte], + [self.matchTeamType, dataTypes.byte], + [self.matchModMode, dataTypes.byte], + ]) + + # Slot mods if free mod is enabled + if self.matchModMode == matchModModes.freeMod: + for i in range(0,16): + struct.append([self.slots[i]["mods"], dataTypes.uInt32]) + + # Seed idk + struct.append([self.seed, dataTypes.uInt32]) + + return struct + + + + def setHost(self, newHost): + """ + Set room host to newHost and send him host packet + + newHost -- new host userID + """ + self.hostUserID = newHost + + # Send host packet to new host + token = glob.tokens.getTokenFromUserID(newHost) + if token != None: + token.enqueue(serverPackets.matchTransferHost()) + + consoleHelper.printColored("> MPROOM{}: {} is now the host".format(self.matchID, newHost), bcolors.BLUE) + + def setSlot(self, slotID, slotStatus = None, slotTeam = None, slotUserID = None, slotMods = None, slotLoaded = None, slotSkip = None, slotComplete = None): + """ + Set a slot to a specific userID and status + + slotID -- id of that slot (0-15) + slotStatus -- see slotStatuses.py + slotTeam -- team id + slotUserID -- user ID of user in that slot + slotMods -- mods enabled in that slot. 0 if not free mod. + slotLoaded -- loaded status True/False + slotSkip -- skip status True/False + slotComplete -- completed status True/False + + If Null is passed, that value won't be edited + """ + if slotStatus != None: + self.slots[slotID]["status"] = slotStatus + + if slotTeam != None: + self.slots[slotID]["team"] = slotTeam + + if slotUserID != None: + self.slots[slotID]["userID"] = slotUserID + + if slotMods != None: + self.slots[slotID]["mods"] = slotMods + + if slotLoaded != None: + self.slots[slotID]["loaded"] = slotLoaded + + if slotSkip != None: + self.slots[slotID]["skip"] = slotSkip + + if slotComplete != None: + self.slots[slotID]["complete"] = slotComplete + + + def setSlotMods(self, slotID, mods): + """ + Set slotID mods. Same as calling setSlot and then sendUpdate + + slotID -- slot number + mods -- new mods + """ + # Set new slot data and send update + self.setSlot(slotID, None, None, None, mods) + self.sendUpdate() + consoleHelper.printColored("> MPROOM{}: Slot{} mods changed to {}".format(self.matchID, slotID, mods), bcolors.BLUE) + + + def toggleSlotReady(self, slotID): + """ + Switch slotID ready/not ready status + Same as calling setSlot and then sendUpdate + + slotID -- slot number + """ + # Update ready status and setnd update + oldStatus = self.slots[slotID]["status"] + if oldStatus == slotStatuses.ready: + newStatus = slotStatuses.notReady + else: + newStatus = slotStatuses.ready + self.setSlot(slotID, newStatus, None, None, None) + self.sendUpdate() + consoleHelper.printColored("> MPROOM{}: Slot{} changed ready status to {}".format(self.matchID, slotID, self.slots[slotID]["status"]), bcolors.BLUE) + + def toggleSlotLock(self, slotID): + """ + Lock a slot + Same as calling setSlot and then sendUpdate + + slotID -- slot number + """ + # Get token of user in that slot (if there's someone) + if self.slots[slotID]["userID"] > -1: + token = glob.tokens.getTokenFromUserID(self.slots[slotID]["userID"]) + else: + token = None + + # Check if slot is already locked + if self.slots[slotID]["status"] == slotStatuses.locked: + newStatus = slotStatuses.free + else: + newStatus = slotStatuses.locked + + # Set new slot status + self.setSlot(slotID, newStatus, 0, -1, 0) + if token != None: + # Send updated settings to kicked user, so he returns to lobby + token.enqueue(serverPackets.updateMatch(self.matchID)) + + # Send updates to everyone else + self.sendUpdate() + consoleHelper.printColored("> MPROOM{}: Slot{} {}".format(self.matchID, slotID, "locked" if newStatus == slotStatuses.locked else "unlocked"), bcolors.BLUE) + + def playerLoaded(self, userID): + """ + Set a player loaded status to True + + userID -- ID of user + """ + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Set loaded to True + self.slots[slotID]["loaded"] = True + consoleHelper.printColored("> MPROOM{}: User {} loaded".format(self.matchID, userID), bcolors.BLUE) + + # Check all loaded + total = 0 + loaded = 0 + for i in range(0,16): + if self.slots[i]["status"] == slotStatuses.playing: + total+=1 + if self.slots[i]["loaded"] == True: + loaded+=1 + + if total == loaded: + self.allPlayersLoaded() + + + def allPlayersLoaded(self): + """Send allPlayersLoaded packet to every playing usr in match""" + for i in range(0,16): + if self.slots[i]["userID"] > -1 and self.slots[i]["status"] == slotStatuses.playing: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.allPlayersLoaded()) + + consoleHelper.printColored("> MPROOM{}: All players loaded! Corrispondere iniziare in 3...".format(self.matchID), bcolors.BLUE) + + + def playerSkip(self, userID): + """ + Set a player skip status to True + + userID -- ID of user + """ + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Set skip to True + self.slots[slotID]["skip"] = True + consoleHelper.printColored("> MPROOM{}: User {} skipped".format(self.matchID, userID), bcolors.BLUE) + + # Send skip packet to every playing useR + for i in range(0,16): + uid = self.slots[i]["userID"] + if self.slots[i]["status"] == slotStatuses.playing and uid > -1: + token = glob.tokens.getTokenFromUserID(uid) + if token != None: + print("Enqueueueue {}".format(uid)) + token.enqueue(serverPackets.playerSkipped(uid)) + + # Check all skipped + total = 0 + skipped = 0 + for i in range(0,16): + if self.slots[i]["status"] == slotStatuses.playing: + total+=1 + if self.slots[i]["skip"] == True: + skipped+=1 + + if total == skipped: + self.allPlayersSkipped() + + def allPlayersSkipped(self): + """Send allPlayersSkipped packet to every playing usr in match""" + for i in range(0,16): + if self.slots[i]["userID"] > -1 and self.slots[i]["status"] == slotStatuses.playing: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.allPlayersSkipped()) + + consoleHelper.printColored("> MPROOM{}: All players skipped!".format(self.matchID), bcolors.BLUE) + + def playerCompleted(self, userID): + """ + Set userID's slot completed to True + + userID -- ID of user + """ + slotID = self.getUserSlotID(userID) + if slotID == None: + return + self.setSlot(slotID, None, None, None, None, None, None, True) + + # Console output + consoleHelper.printColored("> MPROOM{}: User {} has completed".format(self.matchID, userID), bcolors.BLUE) + + # Check all completed + total = 0 + completed = 0 + for i in range(0,16): + if self.slots[i]["status"] == slotStatuses.playing: + total+=1 + if self.slots[i]["complete"] == True: + completed+=1 + + if total == completed: + self.allPlayersCompleted() + + def allPlayersCompleted(self): + """Cleanup match stuff and send match end packet to everyone""" + + # Reset inProgress + self.inProgress = False + + # Reset slots + for i in range(0,16): + if self.slots[i]["userID"] > -1 and self.slots[i]["status"] == slotStatuses.playing: + self.slots[i]["status"] = slotStatuses.notReady + self.slots[i]["loaded"] = False + self.slots[i]["skip"] = False + self.slots[i]["complete"] = False + + # Send match update + self.sendUpdate() + + # Send match complete + for i in range(0,16): + if self.slots[i]["userID"] > -1: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.matchComplete()) + + # Console output + consoleHelper.printColored("> MPROOM{}: Match completed".format(self.matchID), bcolors.BLUE) + + + + def getUserSlotID(self, userID): + """ + Get slot ID occupied by userID + + return -- slot id if found, None if user is not in room + """ + + for i in range(0,16): + if self.slots[i]["userID"] == userID: + return i + + return None + + def userJoin(self, userID): + """ + Add someone to users in match + + userID -- user id of the user + return -- True if join success, False if fail (room is full) + """ + + # Find first free slot + for i in range(0,16): + if self.slots[i]["status"] == slotStatuses.free: + # Occupy slot + self.setSlot(i, slotStatuses.notReady, 0, userID, 0) + + # Send updated match data + self.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: {} joined the room".format(self.matchID, userID), bcolors.BLUE) + + return True + + return False + + def userLeft(self, userID): + """ + Remove someone from users in match + + userID -- user if of the user + """ + + # Make sure the user is in room + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Set that slot to free + self.setSlot(slotID, slotStatuses.free, 0, -1, 0) + + # Check if everyone left + if self.countUsers() == 0: + # Dispose match + glob.matches.disposeMatch(self.matchID) + consoleHelper.printColored("> MPROOM{}: Room disposed".format(self.matchID), bcolors.BLUE) + return + + # Check if host left + if userID == self.hostUserID: + # Give host to someone else + for i in range(0,16): + uid = self.slots[i]["userID"] + if uid > -1: + self.setHost(uid) + break + + # Send updated match data + self.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: {} left the room".format(self.matchID, userID), bcolors.BLUE) + + + def userChangeSlot(self, userID, newSlotID): + """ + Change userID slot to newSlotID + + userID -- user that changed slot + newSlotID -- slot id of new slot + """ + + # Make sure the user is in room + oldSlotID = self.getUserSlotID(userID) + if oldSlotID == None: + return + + # Make sure there is no one inside new slot + if self.slots[newSlotID]["userID"] > -1: + return + + # Get old slot data + oldData = self.slots[oldSlotID].copy() + + # Free old slot + self.setSlot(oldSlotID, slotStatuses.free, 0, -1, 0) + + # Occupy new slot + self.setSlot(newSlotID, oldData["status"], oldData["team"], userID, oldData["mods"]) + + # Send updated match data + self.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: {} moved to slot {}".format(self.matchID, userID, newSlotID), bcolors.BLUE) + + def changePassword(self, newPassword): + """ + Change match password to newPassword + + newPassword -- new password string + """ + self.matchPassword = newPassword + + # Send password change to every user in match + for i in range(0,16): + if self.slots[i]["userID"] > -1: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.changeMatchPassword(self.matchPassword)) + + # Send new match settings too + self.sendUpdate() + + # Console output + consoleHelper.printColored("> MPROOM{}: Password changed to {}".format(self.matchID, self.matchPassword), bcolors.BLUE) + + + def changeMatchMods(self, mods): + """ + Set match global mods + + mods -- mods bitwise int thing + """ + # Set new mods and send update + self.mods = mods + self.sendUpdate() + consoleHelper.printColored("> MPROOM{}: Mods changed to {}".format(self.matchID, self.mods), bcolors.BLUE) + + def userHasBeatmap(self, userID, has = True): + """ + Set no beatmap status for userID + + userID -- ID of user + has -- True if has beatmap, false if not + """ + # Make sure the user is in room + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Set slot + self.setSlot(slotID, slotStatuses.noMap if not has else slotStatuses.notReady) + + # Send updates + self.sendUpdate() + + def transferHost(self, slotID): + """ + Transfer host to slotID + + slotID -- ID of slot + """ + # Make sure there is someone in that slot + uid = self.slots[slotID]["userID"] + if uid == -1: + return + + # Transfer host + self.setHost(uid) + + # Send updates + self.sendUpdate() + + + def playerFailed(self, userID): + """ + Send userID's failed packet to everyone in match + + userID -- ID of user + """ + # Make sure the user is in room + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Send packet to everyone + for i in range(0,16): + uid = self.slots[i]["userID"] + if uid > -1: + token = glob.tokens.getTokenFromUserID(uid) + if token != None: + token.enqueue(serverPackets.playerFailed(slotID)) + + # Console output + consoleHelper.printColored("> MPROOM{}: {} has failed!".format(self.matchID, userID), bcolors.BLUE) + + + def invite(self, fro, to): + """ + Fro invites to in this match. + + fro -- sender userID + to -- receiver userID + """ + + # Get tokens + froToken = glob.tokens.getTokenFromUserID(fro) + toToken = glob.tokens.getTokenFromUserID(to) + if froToken == None or toToken == None: + return + + # FokaBot is too busy + if to == 999: + froToken.enqueue(serverPackets.sendMessage("FokaBot", froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.")) + + # Send message + message = "Come join my multiplayer match: \"[osump://{}/{} {}]\"".format(self.matchID, self.matchPassword.replace(" ", "_"), self.matchName) + toToken.enqueue(serverPackets.sendMessage(froToken.username, toToken.username, message)) + + + def countUsers(self): + """ + Return how many players are in that match + + return -- number of users + """ + + c = 0 + for i in range(0,16): + if self.slots[i]["userID"] > -1: + c+=1 + + return c + + def changeTeam(self, userID): + """ + Change userID's team + + userID -- id of user + """ + # Make sure the user is in room + slotID = self.getUserSlotID(userID) + if slotID == None: + return + + # Update slot and send update + newTeam = matchTeams.blue if self.slots[slotID]["team"] == matchTeams.red else matchTeams.red + self.setSlot(slotID, None, newTeam) + self.sendUpdate() + + + + def sendUpdate(self): + # Send to users in room + for i in range(0,16): + if self.slots[i]["userID"] > -1: + token = glob.tokens.getTokenFromUserID(self.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.updateMatch(self.matchID)) + + # Send to users in lobby + for i in glob.matches.usersInLobby: + token = glob.tokens.getTokenFromUserID(i) + if token != None: + token.enqueue(serverPackets.updateMatch(self.matchID)) + + def checkTeams(self): + """ + Check if match teams are valid + + return -- True if valid, False if invalid + """ + if match.matchTeamType != matchTeamTypes.teamVs or matchTeamTypes != matchTeamTypes.tagTeamVs: + # Teams are always valid if we have no teams + return True + + # We have teams, check if they are valid + firstTeam = -1 + for i in range(0,16): + if self.slots[i]["userID"] > -1 and (self.slots[i]["status"]&slotStatuses.noMap) == 0: + if firstTeam == -1: + firstTeam = self.slots[i]["team"] + elif firstTeam != self.slots[i]["teams"]: + consoleHelper.printColored("> MPROOM{}: Teams are valid".format(self.matchID), bcolors.BLUE) + return True + + consoleHelper.printColored("> MPROOM{}: Invalid teams!".format(self.matchID), bcolors.RED) + return False diff --git a/matchBeatmapEvent.py b/matchBeatmapEvent.py new file mode 100644 index 0000000..06ddd0b --- /dev/null +++ b/matchBeatmapEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, packetData, has): + # Get usertoken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Set has beatmap/no beatmap + match.userHasBeatmap(userID, has) diff --git a/matchChangeTeamEvent.py b/matchChangeTeamEvent.py new file mode 100644 index 0000000..915c30e --- /dev/null +++ b/matchChangeTeamEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, _): + # Read token data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Get match object + match = glob.matches.matches[matchID] + + # Change team + match.changeTeam(userID) diff --git a/matchCompleteEvent.py b/matchCompleteEvent.py new file mode 100644 index 0000000..0a45e18 --- /dev/null +++ b/matchCompleteEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, packetData): + # Get usertoken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Set our match complete + match.playerCompleted(userID) diff --git a/matchFailedEvent.py b/matchFailedEvent.py new file mode 100644 index 0000000..b040ad7 --- /dev/null +++ b/matchFailedEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, _): + # Get usertoken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Match exists, get object + match = glob.matches.matches[matchID] + + # Fail user + match.playerFailed(userID) diff --git a/matchFramesEvent.py b/matchFramesEvent.py new file mode 100644 index 0000000..50a9ca1 --- /dev/null +++ b/matchFramesEvent.py @@ -0,0 +1,35 @@ +import glob +import slotStatuses +import serverPackets + +def handle(userToken, packetData): + # Get usertoken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Change slot id in packetData + slotID = match.getUserSlotID(userID) + '''opd = packetData[4] + packetData = bytearray(packetData) + packetData[4] = slotID + print("User: {}, slot {}, oldPackData: {}, packData {}".format(userID, slotID, opd, packetData[4]))''' + + # Enqueue frames to who's playing + for i in range(0,16): + if match.slots[i]["userID"] > -1 and match.slots[i]["status"] == slotStatuses.playing: + token = glob.tokens.getTokenFromUserID(match.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.matchFrames(slotID, packetData)) diff --git a/matchHasBeatmapEvent.py b/matchHasBeatmapEvent.py new file mode 100644 index 0000000..118afb2 --- /dev/null +++ b/matchHasBeatmapEvent.py @@ -0,0 +1,3 @@ +import matchBeatmapEvent +def handle(userToken, packetData): + matchBeatmapEvent.handle(userToken, packetData, True) diff --git a/matchInviteEvent.py b/matchInviteEvent.py new file mode 100644 index 0000000..f27ccaa --- /dev/null +++ b/matchInviteEvent.py @@ -0,0 +1,24 @@ +import clientPackets +import glob + +def handle(userToken, packetData): + # Read token and packet data + userID = userToken.userID + packetData = clientPackets.matchInvite(packetData) + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Get match object + match = glob.matches.matches[matchID] + + # Send invite + match.invite(userID, packetData["userID"]) diff --git a/matchList.py b/matchList.py new file mode 100644 index 0000000..c78ac4f --- /dev/null +++ b/matchList.py @@ -0,0 +1,80 @@ +import match +import glob +import serverPackets + +class matchList: + matches = {} + usersInLobby = [] + lastID = 1 + + def __init__(self): + """Initialize a matchList object""" + self.matches = {} + self.usersInLobby = [] + self.lastID = 1 + + def createMatch(self, __matchName, __matchPassword, __beatmapID, __beatmapName, __beatmapMD5, __gameMode, __hostUserID): + """ + Add a new match to matches list + + __matchName -- match name, string + __matchPassword -- match md5 password. Leave empty for no password + __beatmapID -- beatmap ID + __beatmapName -- beatmap name, string + __beatmapMD5 -- beatmap md5 hash, string + __gameMode -- game mode ID. See gameModes.py + __hostUserID -- user id of who created the match + return -- match ID + """ + # Add a new match to matches list + matchID = self.lastID + self.lastID+=1 + self.matches[matchID] = match.match(matchID, __matchName, __matchPassword, __beatmapID, __beatmapName, __beatmapMD5, __gameMode, __hostUserID) + return matchID + + + def lobbyUserJoin(self, __userID): + """ + Add userID to users in lobby + + __userID -- user who joined mp lobby + """ + + # Make sure the user is not already in mp lobby + if __userID not in self.usersInLobby: + # We don't need to join #lobby, client will automatically send a packet for it + self.usersInLobby.append(__userID) + + + def lobbyUserPart(self, __userID): + """ + Remove userID from users in lobby + + __userID -- user who left mp lobby + """ + + # Make sure the user is in mp lobby + if __userID in self.usersInLobby: + # Part lobby and #lobby channel + self.usersInLobby.remove(__userID) + + + def disposeMatch(self, __matchID): + """ + Destroy match object with id = __matchID + + __matchID -- ID of match to dispose + """ + + # Make sure the match exists + if __matchID not in self.matches: + return + + # Remove match object + self.matches.pop(__matchID) + + # Send match dispose packet to everyone in lobby + for i in self.usersInLobby: + token = glob.tokens.getTokenFromUserID(i) + if token != None: + token.enqueue(serverPackets.disposeMatch(__matchID)) diff --git a/matchLockEvent.py b/matchLockEvent.py new file mode 100644 index 0000000..4041b2e --- /dev/null +++ b/matchLockEvent.py @@ -0,0 +1,23 @@ +import glob +import clientPackets + +def handle(userToken, packetData): + # Get token data + userID = userToken.userID + + # Get packet data + packetData = clientPackets.lockSlot(packetData) + + # Make sure the match exists + matchID = userToken.matchID + if matchID not in glob.matches.matches: + return + match = glob.matches.matches[matchID] + + # Make sure we aren't locking our slot + ourSlot = match.getUserSlotID(userID) + if packetData["slotID"] == ourSlot: + return + + # Lock/Unlock slot + match.toggleSlotLock(packetData["slotID"]) diff --git a/matchModModes.py b/matchModModes.py new file mode 100644 index 0000000..0b8ea87 --- /dev/null +++ b/matchModModes.py @@ -0,0 +1,2 @@ +normal = 0 +freeMod = 1 diff --git a/matchNoBeatmapEvent.py b/matchNoBeatmapEvent.py new file mode 100644 index 0000000..efbff0d --- /dev/null +++ b/matchNoBeatmapEvent.py @@ -0,0 +1,3 @@ +import matchBeatmapEvent +def handle(userToken, packetData): + matchBeatmapEvent.handle(userToken, packetData, False) diff --git a/matchPlayerLoadEvent.py b/matchPlayerLoadEvent.py new file mode 100644 index 0000000..449a56b --- /dev/null +++ b/matchPlayerLoadEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, packetData): + # Get userToken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Set our load status + match.playerLoaded(userID) diff --git a/matchReadyEvent.py b/matchReadyEvent.py new file mode 100644 index 0000000..7ac992a --- /dev/null +++ b/matchReadyEvent.py @@ -0,0 +1,16 @@ +import glob + +def handle(userToken, _): + # Get usertoken data + userID = userToken.userID + + # Make sure the match exists + matchID = userToken.matchID + if matchID not in glob.matches.matches: + return + match = glob.matches.matches[matchID] + + # Get our slotID and change ready status + slotID = match.getUserSlotID(userID) + if slotID != None: + match.toggleSlotReady(slotID) diff --git a/matchScoringTypes.py b/matchScoringTypes.py new file mode 100644 index 0000000..888f851 --- /dev/null +++ b/matchScoringTypes.py @@ -0,0 +1,3 @@ +score = 0 +accuracy = 1 +combo = 2 diff --git a/matchSkipEvent.py b/matchSkipEvent.py new file mode 100644 index 0000000..c06f114 --- /dev/null +++ b/matchSkipEvent.py @@ -0,0 +1,22 @@ +import glob + +def handle(userToken, packetData): + # Get userToken data + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Skip + match.playerSkip(userID) diff --git a/matchStartEvent.py b/matchStartEvent.py new file mode 100644 index 0000000..b294229 --- /dev/null +++ b/matchStartEvent.py @@ -0,0 +1,47 @@ +import glob +import slotStatuses +import serverPackets + +def handle(userToken, _): + # TODO: Host check + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + force = False # TODO: Force thing + + # Make sure we have enough players + if (match.countUsers() < 2 or not match.checkTeams()) and not force: + return + + # Change inProgress value + match.inProgress = True + + # Set playing to ready players and set load, skip and complete to False + for i in range(0,16): + if (match.slots[i]["status"] & slotStatuses.ready) > 0: + match.slots[i]["status"] = slotStatuses.playing + match.slots[i]["loaded"] = False + match.slots[i]["skip"] = False + match.slots[i]["complete"] = False + + # Send match start packet + for i in range(0,16): + if (match.slots[i]["status"] & slotStatuses.playing) > 0 and match.slots[i]["userID"] != -1: + token = glob.tokens.getTokenFromUserID(match.slots[i]["userID"]) + if token != None: + token.enqueue(serverPackets.matchStart(matchID)) + + # Send updates + match.sendUpdate() diff --git a/matchTeamTypes.py b/matchTeamTypes.py new file mode 100644 index 0000000..07d43bd --- /dev/null +++ b/matchTeamTypes.py @@ -0,0 +1,4 @@ +headToHead = 0 +tagCoop = 1 +teamVs = 2 +tagTeamVs = 3 diff --git a/matchTeams.py b/matchTeams.py new file mode 100644 index 0000000..ef47898 --- /dev/null +++ b/matchTeams.py @@ -0,0 +1,3 @@ +noTeam = 0 +blue = 1 +red = 2 diff --git a/matchTransferHostEvent.py b/matchTransferHostEvent.py new file mode 100644 index 0000000..4dff540 --- /dev/null +++ b/matchTransferHostEvent.py @@ -0,0 +1,23 @@ +import glob +import clientPackets + +def handle(userToken, packetData): + # Get packet data + packetData = clientPackets.transferHost(packetData) + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # Match exists, get object + match = glob.matches.matches[matchID] + + # Transfer host + match.transferHost(packetData["slotID"]) diff --git a/mods.py b/mods.py new file mode 100644 index 0000000..ee305dd --- /dev/null +++ b/mods.py @@ -0,0 +1,30 @@ +Nomod = 0 +NoFail = 1 +Easy = 2 +NoVideo = 4 +Hidden = 8 +HardRock = 16 +SuddenDeath = 32 +DoubleTime = 64 +Relax = 128 +HalfTime = 256 +Nightcore = 512 +Flashlight = 1024 +Autoplay = 2048 +SpunOut = 4096 +Relax2 = 8192 +Perfect = 16384 +Key4 = 32768 +Key5 = 65536 +Key6 = 131072 +Key7 = 262144 +Key8 = 524288 +keyMod = 1015808 +FadeIn = 1048576 +Random = 2097152 +LastMod = 4194304 +Key9 = 16777216 +Key10 = 33554432 +Key1 = 67108864 +Key3 = 134217728 +Key2 = 268435456 diff --git a/osuToken.py b/osuToken.py new file mode 100644 index 0000000..7ac5493 --- /dev/null +++ b/osuToken.py @@ -0,0 +1,227 @@ +import uuid +import actions +import gameModes +import userHelper +import time +import consoleHelper +import bcolors +import serverPackets +import logoutEvent + +class token: + """Osu Token object + + token -- token string + userID -- userID associated to that token + username -- username relative to userID (cache) + rank -- rank (permissions) relative to userID (cache) + actionID -- current user action (see actions.py) + actionText -- current user action text + actionMd5 -- md5 relative to user action + actionMods -- current acton mods + gameMode -- current user game mode + location -- [latitude,longitude] + queue -- packets queue + joinedChannels -- list. Contains joined channel names + spectating -- userID of spectating user. 0 if not spectating. + spectators -- list. Contains userIDs of spectators + country -- osu country code. Use countryHelper to convert from letter country code to osu country code + pingTime -- latest packet received UNIX time + loginTime -- login UNIX time + """ + + token = "" + userID = 0 + username = "" + rank = 0 + actionID = actions.idle + actionText = "" + actionMd5 = "" + actionMods = 0 + gameMode = gameModes.std + + country = 0 + location = [0,0] + + queue = bytes() + joinedChannels = [] + + spectating = 0 + spectators = [] + + pingTime = 0 + loginTime = 0 + + awayMessage = "" + + matchID = -1 + + + def __init__(self, __userID, __token = None): + """ + Create a token object and set userID and token + + __userID -- user associated to this token + __token -- if passed, set token to that value + if not passed, token will be generated + """ + + # Set stuff + self.userID = __userID + self.username = userHelper.getUsername(self.userID) + self.rank = userHelper.getRankPrivileges(self.userID) + self.loginTime = int(time.time()) + self.pingTime = self.loginTime + + # Default variables + self.spectators = [] + self.spectating = 0 + self.location = [0,0] + self.joinedChannels = [] + self.actionID = actions.idle + self.actionText = "" + self.actionMods = 0 + self.gameMode = gameModes.std + self.awayMessage = "" + self.matchID = -1 + + # Generate/set token + if __token != None: + self.token = __token + else: + self.token = str(uuid.uuid4()) + + + def enqueue(self, __bytes): + """ + Add bytes (packets) to queue + + __bytes -- (packet) bytes to enqueue + """ + + self.queue += __bytes + + + def resetQueue(self): + """Resets the queue. Call when enqueued packets have been sent""" + self.queue = bytes() + + + def joinChannel(self, __channel): + """Add __channel to joined channels list + + __channel -- channel name""" + + if __channel not in self.joinedChannels: + self.joinedChannels.append(__channel) + + + def partChannel(self, __channel): + """Remove __channel from joined channels list + + __channel -- channel name""" + + if __channel in self.joinedChannels: + self.joinedChannels.remove(__channel) + + + def setLocation(self, __location): + """Set location (latitude and longitude) + + __location -- [latitude, longitude]""" + + self.location = __location + + + def getLatitude(self): + """Get latitude + + return -- latitude""" + + return self.location[0] + + + def getLongitude(self): + """Get longitude + + return -- longitude""" + return self.location[1] + + + def startSpectating(self, __userID): + """Set the spectating user to __userID + + __userID -- target userID""" + self.spectating = __userID + + + def stopSpectating(self): + """Set the spectating user to 0, aka no user""" + self.spectating = 0 + + + def addSpectator(self, __userID): + """Add __userID to our spectators + + userID -- new spectator userID""" + + # Add userID to spectators if not already in + if __userID not in self.spectators: + self.spectators.append(__userID) + + + def removeSpectator(self, __userID): + """Remove __userID from our spectators + + userID -- old spectator userID""" + + # Remove spectator + if __userID in self.spectators: + self.spectators.remove(__userID) + + + def setCountry(self, __countryID): + """Set country to __countryID + + __countryID -- numeric country ID. See countryHelper.py""" + + self.country = __countryID + + + def getCountry(self): + """Get numeric country ID + + return -- numeric country ID. See countryHelper.py""" + + return self.country + + + def updatePingTime(self): + """Update latest ping time""" + self.pingTime = int(time.time()) + + def setAwayMessage(self, __awayMessage): + """Set a new away message""" + self.awayMessage = __awayMessage + + def joinMatch(self, __matchID): + """ + Set match to matchID + + __matchID -- new match ID + """ + self.matchID = __matchID + + def partMatch(self): + """Set match to -1""" + self.matchID = -1 + + def kick(self): + """Kick this user from the server""" + # Send packet to target + consoleHelper.printColored("> {} has been disconnected. (kick)".format(self.username), bcolors.YELLOW) + self.enqueue(serverPackets.notification("You have been kicked from the server. Please login again.")) + self.enqueue(serverPackets.loginFailed()) + + # Logout event + logoutEvent.handle(self, None) diff --git a/packetHelper.py b/packetHelper.py new file mode 100644 index 0000000..c86e8f3 --- /dev/null +++ b/packetHelper.py @@ -0,0 +1,249 @@ +import struct +import dataTypes + +def uleb128Encode(num): + """ + Encode int -> uleb128 + + num -- int to encode + return -- bytearray with encoded number + """ + + arr = bytearray() + length = 0 + + if num == 0: + return bytearray(b"\x00") + + while num > 0: + arr.append(num & 127) + num = num >> 7 + if num != 0: + arr[length] = arr[length] | 128 + length+=1 + + return arr + + +def uleb128Decode(num): + """ + Decode uleb128 -> int + + num -- encoded uleb128 + return -- list. [total, length] + """ + + shift = 0 + + arr = [0,0] #total, length + + while True: + b = num[arr[1]] + arr[1]+=1 + arr[0] = arr[0] | (int(b & 127) << shift) + if b & 128 == 0: + break + shift += 7 + + return arr + + +def unpackData(__data, __dataType): + """ + Unpacks data according to dataType + + __data -- bytes array to unpack + __dataType -- data type. See dataTypes.py + + return -- unpacked bytes + """ + + # Get right pack Type + if __dataType == dataTypes.uInt16: + unpackType = " {} has left multiplayer lobby".format(username), bcolors.BLUE) diff --git a/partMatchEvent.py b/partMatchEvent.py new file mode 100644 index 0000000..b8b3122 --- /dev/null +++ b/partMatchEvent.py @@ -0,0 +1,27 @@ +import glob +import serverPackets + +def handle(userToken, _): + # get data from usertoken + userID = userToken.userID + + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Set slot to free + match.userLeft(userID) + + # Set usertoken match to -1 + userToken.partMatch() + userToken.enqueue(serverPackets.channelKicked("#multiplayer")) diff --git a/passwordHelper.py b/passwordHelper.py new file mode 100644 index 0000000..8523899 --- /dev/null +++ b/passwordHelper.py @@ -0,0 +1,36 @@ +import crypt +import base64 +import bcrypt + +def checkOldPassword(password, salt, rightPassword): + """ + Check if password+salt corresponds to rightPassword + + password -- input password + salt -- password's salt + rightPassword -- right password + return -- bool + """ + + return (rightPassword == crypt.crypt(password, "$2y$"+str(base64.b64decode(salt)))) + +def checkNewPassword(password, dbPassword): + """ + Check if a password (version 2) is right. + + password -- input password + dbPassword -- the password in the database + return -- bool + """ + password = password.encode("utf8") + dbPassword = dbPassword.encode("utf8") + return bcrypt.hashpw(password, dbPassword) == dbPassword + +def genBcrypt(password): + """ + Bcrypts a password. + + password -- the password to hash. + return -- bytestring + """ + return bcrypt.hashpw(password.encode("utf8"), bcrypt.gensalt(10, b'2a')) diff --git a/pep.py b/pep.py new file mode 100644 index 0000000..5c041cc --- /dev/null +++ b/pep.py @@ -0,0 +1,352 @@ +"""Hello, pep.py here, ex-owner of ripple and prime minister of Ripwot.""" +import logging +import sys +import flask +import datetime + +# Tornado server +from tornado.wsgi import WSGIContainer +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop + +# pep.py files +import bcolors +import packetIDs +import serverPackets +import config +import exceptions +import glob +import fokabot +import banchoConfig + +import sendPublicMessageEvent +import sendPrivateMessageEvent +import channelJoinEvent +import channelPartEvent +import changeActionEvent +import cantSpectateEvent +import startSpectatingEvent +import stopSpectatingEvent +import spectateFramesEvent +import friendAddEvent +import friendRemoveEvent +import logoutEvent +import loginEvent +import setAwayMessageEvent +import joinLobbyEvent +import createMatchEvent +import partLobbyEvent +import changeSlotEvent +import joinMatchEvent +import partMatchEvent +import changeMatchSettingsEvent +import changeMatchPasswordEvent +import changeMatchModsEvent +import matchReadyEvent +import matchLockEvent +import matchStartEvent +import matchPlayerLoadEvent +import matchSkipEvent +import matchFramesEvent +import matchCompleteEvent +import matchNoBeatmapEvent +import matchHasBeatmapEvent +import matchTransferHostEvent +import matchFailedEvent +import matchInviteEvent +import matchChangeTeamEvent + +# pep.py helpers +import packetHelper +import consoleHelper +import databaseHelper +import responseHelper +import generalFunctions +import systemHelper + +# Create flask instance +app = flask.Flask(__name__) + +# Get flask logger +flaskLogger = logging.getLogger("werkzeug") + +# Ci trigger +@app.route("/ci-trigger") +@app.route("/api/ci-trigger") +def ciTrigger(): + # Ci restart trigger + + # Get ket from GET + key = flask.request.args.get('k') + + # Get request ip + requestIP = flask.request.headers.get('X-Real-IP') + if requestIP == None: + requestIP = flask.request.remote_addr + + # Check key + if key is None or key != glob.conf.config["ci"]["key"]: + consoleHelper.printColored("[!] Invalid ci trigger from {}".format(requestIP), bcolors.RED) + return flask.jsonify({"response" : "-1"}) + + # Ci event triggered, schedule server shutdown + consoleHelper.printColored("[!] Ci event triggered from {}".format(requestIP), bcolors.PINK) + systemHelper.scheduleShutdown(5, False, "A new Bancho update is available and the server will be restarted in 5 seconds. Thank you for your patience.") + + return flask.jsonify({"response" : 1}) + + +@app.route("/api/server-status") +def serverStatus(): + # Server status api + # 1: Online + # -1: Restarting + return flask.jsonify({ + "response" : 200, + "status" : -1 if glob.restarting == True else 1 + }) + + +# Main bancho server +@app.route("/", methods=['GET', 'POST']) +def banchoServer(): + if flask.request.method == 'POST': + + # Track time if needed + if serverOutputRequestTime == True: + # Start time + st = datetime.datetime.now() + + # Client's token string and request data + requestTokenString = flask.request.headers.get('osu-token') + requestData = flask.request.data + + # Server's token string and request data + responseTokenString = "ayy" + responseData = bytes() + + if requestTokenString == None: + # No token, first request. Handle login. + responseTokenString, responseData = loginEvent.handle(flask.request) + else: + try: + # This is not the first packet, send response based on client's request + # Packet start position, used to read stacked packets + pos = 0 + + # Make sure the token exists + if requestTokenString not in glob.tokens.tokens: + raise exceptions.tokenNotFoundException() + + # Token exists, get its object + userToken = glob.tokens.tokens[requestTokenString] + + # Keep reading packets until everything has been read + while pos < len(requestData): + # Get packet from stack starting from new packet + leftData = requestData[pos:] + + # Get packet ID, data length and data + packetID = packetHelper.readPacketID(leftData) + dataLength = packetHelper.readPacketLength(leftData) + packetData = requestData[pos:(pos+dataLength+7)] + + # Console output if needed + if serverOutputPackets == True and packetID != 4: + consoleHelper.printColored("Incoming packet ({})({}):".format(requestTokenString, userToken.username), bcolors.GREEN) + consoleHelper.printColored("Packet code: {}\nPacket length: {}\nSingle packet data: {}\n".format(str(packetID), str(dataLength), str(packetData)), bcolors.YELLOW) + + # Event handler + def handleEvent(ev): + def wrapper(): + ev.handle(userToken, packetData) + return wrapper + + eventHandler = { + # TODO: Rename packets and events + # TODO: Host check for multi + packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent), + packetIDs.client_sendPrivateMessage: handleEvent(sendPrivateMessageEvent), + packetIDs.client_setAwayMessage: handleEvent(setAwayMessageEvent), + packetIDs.client_channelJoin: handleEvent(channelJoinEvent), + packetIDs.client_channelPart: handleEvent(channelPartEvent), + packetIDs.client_changeAction: handleEvent(changeActionEvent), + packetIDs.client_startSpectating: handleEvent(startSpectatingEvent), + packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent), + packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent), + packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent), + packetIDs.client_friendAdd: handleEvent(friendAddEvent), + packetIDs.client_friendRemove: handleEvent(friendRemoveEvent), + packetIDs.client_logout: handleEvent(logoutEvent), + packetIDs.client_joinLobby: handleEvent(joinLobbyEvent), + packetIDs.client_partLobby: handleEvent(partLobbyEvent), + packetIDs.client_createMatch: handleEvent(createMatchEvent), + packetIDs.client_joinMatch: handleEvent(joinMatchEvent), + packetIDs.client_partMatch: handleEvent(partMatchEvent), + packetIDs.client_matchChangeSlot: handleEvent(changeSlotEvent), + packetIDs.client_matchChangeSettings: handleEvent(changeMatchSettingsEvent), + packetIDs.client_matchChangePassword: handleEvent(changeMatchPasswordEvent), + packetIDs.client_matchChangeMods: handleEvent(changeMatchModsEvent), + packetIDs.client_matchReady: handleEvent(matchReadyEvent), + packetIDs.client_matchNotReady: handleEvent(matchReadyEvent), + packetIDs.client_matchLock: handleEvent(matchLockEvent), + packetIDs.client_matchStart: handleEvent(matchStartEvent), + packetIDs.client_matchLoadComplete: handleEvent(matchPlayerLoadEvent), + packetIDs.client_matchSkipRequest: handleEvent(matchSkipEvent), + packetIDs.client_matchScoreUpdate: handleEvent(matchFramesEvent), + packetIDs.client_matchComplete: handleEvent(matchCompleteEvent), + packetIDs.client_matchNoBeatmap: handleEvent(matchNoBeatmapEvent), + packetIDs.client_matchHasBeatmap: handleEvent(matchHasBeatmapEvent), + packetIDs.client_matchTransferHost: handleEvent(matchTransferHostEvent), + packetIDs.client_matchFailed: handleEvent(matchFailedEvent), + packetIDs.client_invite: handleEvent(matchInviteEvent), + packetIDs.client_matchChangeTeam: handleEvent(matchChangeTeamEvent) + } + + if packetID != 4: + if packetID in eventHandler: + eventHandler[packetID]() + else: + consoleHelper.printColored("[!] Unknown packet id from {} ({})".format(requestTokenString, packetID), bcolors.RED) + + # Update pos so we can read the next stacked packet + # +7 because we add packet ID bytes, unused byte and data length bytes + pos += dataLength+7 + + # Token queue built, send it + responseTokenString = userToken.token + responseData = userToken.queue + userToken.resetQueue() + + # Update ping time for timeout + userToken.updatePingTime() + except exceptions.tokenNotFoundException: + # Token not found. Disconnect that user + responseData = serverPackets.loginError() + responseData += serverPackets.notification("Whoops! Something went wrong, please login again.") + consoleHelper.printColored("[!] Received packet from unknown token ({}).".format(requestTokenString), bcolors.RED) + consoleHelper.printColored("> {} have been disconnected (invalid token)".format(requestTokenString), bcolors.YELLOW) + + if serverOutputRequestTime == True: + # End time + et = datetime.datetime.now() + + # Total time: + tt = float((et.microsecond-st.microsecond)/1000) + consoleHelper.printColored("Request time: {}ms".format(tt), bcolors.PINK) + + # Send server's response to client + # We don't use token object because we might not have a token (failed login) + return responseHelper.generateResponse(responseTokenString, responseData) + else: + # Not a POST request, send html page + return responseHelper.HTMLResponse() + + +if __name__ == "__main__": + # Server start + consoleHelper.printServerStartHeader(True) + + # Read config.ini + consoleHelper.printNoNl("> Loading config file... ") + glob.conf = config.config("config.ini") + + if glob.conf.default == True: + # We have generated a default config.ini, quit server + consoleHelper.printWarning() + consoleHelper.printColored("[!] config.ini not found. A default one has been generated.", bcolors.YELLOW) + consoleHelper.printColored("[!] Please edit your config.ini and run the server again.", bcolors.YELLOW) + sys.exit() + + # If we haven't generated a default config.ini, check if it's valid + if glob.conf.checkConfig() == False: + consoleHelper.printError() + consoleHelper.printColored("[!] Invalid config.ini. Please configure it properly", bcolors.RED) + consoleHelper.printColored("[!] Delete your config.ini to generate a default one", bcolors.RED) + sys.exit() + else: + consoleHelper.printDone() + + + # Connect to db + try: + consoleHelper.printNoNl("> Connecting to MySQL db... ") + glob.db = databaseHelper.db(glob.conf.config["db"]["host"], glob.conf.config["db"]["username"], glob.conf.config["db"]["password"], glob.conf.config["db"]["database"], int(glob.conf.config["db"]["pingtime"])) + consoleHelper.printDone() + except: + # Exception while connecting to db + consoleHelper.printError() + consoleHelper.printColored("[!] Error while connection to database. Please check your config.ini and run the server again", bcolors.RED) + raise + + # Load bancho_settings + try: + consoleHelper.printNoNl("> Loading bancho settings from DB... ") + glob.banchoConf = banchoConfig.banchoConfig() + consoleHelper.printDone() + except: + consoleHelper.printError() + consoleHelper.printColored("[!] Error while loading bancho_settings. Please make sure the table in DB has all the required rows", bcolors.RED) + raise + + # Initialize chat channels + consoleHelper.printNoNl("> Initializing chat channels... ") + glob.channels.loadChannels() + consoleHelper.printDone() + + # Start fokabot + consoleHelper.printNoNl("> Connecting FokaBot... ") + fokabot.connect() + consoleHelper.printDone() + + # Initialize user timeout check loop + try: + consoleHelper.printNoNl("> Initializing user timeout check loop... ") + glob.tokens.usersTimeoutCheckLoop(int(glob.conf.config["server"]["timeouttime"]), int(glob.conf.config["server"]["timeoutlooptime"])) + consoleHelper.printDone() + except: + consoleHelper.printError() + consoleHelper.printColored("[!] Error while initializing user timeout check loop", bcolors.RED) + consoleHelper.printColored("[!] Make sure that 'timeouttime' and 'timeoutlooptime' in config.ini are numbers", bcolors.RED) + raise + + # Localize warning + if(generalFunctions.stringToBool(glob.conf.config["server"]["localizeusers"]) == False): + consoleHelper.printColored("[!] Warning! users localization is disabled!", bcolors.YELLOW) + + # Get server parameters from config.ini + serverName = glob.conf.config["server"]["server"] + serverHost = glob.conf.config["server"]["host"] + serverPort = int(glob.conf.config["server"]["port"]) + serverOutputPackets = generalFunctions.stringToBool(glob.conf.config["server"]["outputpackets"]) + serverOutputRequestTime = generalFunctions.stringToBool(glob.conf.config["server"]["outputrequesttime"]) + + # Run server sanic way + if serverName == "tornado": + # Tornado server + consoleHelper.printColored("> Tornado listening for clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN) + webServer = HTTPServer(WSGIContainer(app)) + webServer.listen(serverPort) + IOLoop.instance().start() + elif serverName == "flask": + # Flask server + # Get flask settings + flaskThreaded = generalFunctions.stringToBool(glob.conf.config["flask"]["threaded"]) + flaskDebug = generalFunctions.stringToBool(glob.conf.config["flask"]["debug"]) + flaskLoggerStatus = not generalFunctions.stringToBool(glob.conf.config["flask"]["logger"]) + + # Set flask debug mode and logger + app.debug = flaskDebug + flaskLogger.disabled = flaskLoggerStatus + + # Console output + if flaskDebug == False: + consoleHelper.printColored("> Flask listening for clients on {}.{}...".format(serverHost, serverPort), bcolors.GREEN) + else: + consoleHelper.printColored("> Flask "+bcolors.YELLOW+"(debug mode)"+bcolors.ENDC+" listening for clients on {}:{}...".format(serverHost, serverPort), bcolors.GREEN) + + # Run flask server + app.run(host=serverHost, port=serverPort, threaded=flaskThreaded) + else: + print(bcolors.RED+"[!] Unknown server. Please set the server key in config.ini to "+bcolors.ENDC+bcolors.YELLOW+"tornado"+bcolors.ENDC+bcolors.RED+" or "+bcolors.ENDC+bcolors.YELLOW+"flask"+bcolors.ENDC) + sys.exit() diff --git a/responseHelper.py b/responseHelper.py new file mode 100644 index 0000000..a2fbd8f --- /dev/null +++ b/responseHelper.py @@ -0,0 +1,47 @@ +import flask +import gzip + +def generateResponse(token, data = None): + """ + Return a flask response with required headers for osu! client, token and gzip compressed data + + token -- user token + data -- plain response body + return -- flask response + """ + + resp = flask.Response(gzip.compress(data, 6)) + resp.headers['cho-token'] = token + resp.headers['cho-protocol'] = '19' + resp.headers['Keep-Alive'] = 'timeout=5, max=100' + resp.headers['Connection'] = 'keep-alive' + resp.headers['Content-Type'] = 'text/html; charset=UTF-8' + resp.headers['Vary'] = 'Accept-Encoding' + resp.headers['Content-Encoding'] = 'gzip' + return resp + + +def HTMLResponse(): + """Return HTML bancho meme response""" + + html = "MA MAURO ESISTE?

"
+	html += "           _                 __
" + html += " (_) / /
" + html += " ______ __ ____ ____ / /____
" + html += " / ___/ / _ \\/ _ \\/ / _ \\
" + html += " / / / / /_) / /_) / / ____/
" + html += "/__/ /__/ .___/ .___/__/ \\_____/
" + html += " / / / /
" + html += " /__/ /__/
" + html += "PYTHON > ALL VERSION

" + html += "
" + html += " .. o .
" + html += " o.o o . o
" + html += " oo...
" + html += " __[]__
" + html += " phwr--> _\\:D/_/o_o_o_|__ u wot m8
" + html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/
" + html += " \\ . .. .. . /
" + html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
" + html += "

reverse engineering a protocol impossible to reverse engineer since always
we are actually reverse engineering bancho successfully. for the third time.
" + return html diff --git a/routes.py b/routes.py new file mode 100644 index 0000000..b10df9a --- /dev/null +++ b/routes.py @@ -0,0 +1,52 @@ +""" +WIP feature that will come in the future. +Don't import +""" +import flask +import glob +import exceptions + +@app.route("/api/online-users-count") +def APIonlineUsersCount(): + return flask.jsonify({"count" : len(glob.tokens.tokens)-1}) + +@app.route("/api/user-info") +def APIonlineUsers(): + resp = {} + + try: + u = flask.request.args.get('u') + + # Username/userID + if u.isdigit(): + u = int(u) + else: + u = userHelper.getID(u) + if u == None: + raise exceptions.userNotFoundException + + # Make sure this user is online + userToken = glob.tokens.getTokenFromUserID(u) + if userToken == None: + raise exceptions.tokenNotFoundException + + # Build response dictionary + resp["response"] = "1" + resp[userToken.username] = { + "userID" : userToken.userID, + "actionID" : userToken.actionID, + "actionText" : userToken.actionText, + "actionMd5" : userToken.actionMd5, + "actionMods": userToken.actionMods, + "gameMode": userToken.gameMode, + "country": countryHelper.getCountryLetters(userToken.country), + "position": userToken.location, + "spectating": userToken.spectating, + "spectators": userToken.spectators + } + except exceptions.userNotFoundException: + resp["response"] = "-1" + except exceptions.tokenNotFoundException: + resp["response"] = "-2" + finally: + return flask.jsonify(resp) diff --git a/runserver.bat b/runserver.bat new file mode 100644 index 0000000..a041e09 --- /dev/null +++ b/runserver.bat @@ -0,0 +1,4 @@ +D: +cd D:\DevStuff\ripple-v15\c.ppy.sh +python pep.py +pause diff --git a/sendPrivateMessageEvent.py b/sendPrivateMessageEvent.py new file mode 100644 index 0000000..d2056df --- /dev/null +++ b/sendPrivateMessageEvent.py @@ -0,0 +1,47 @@ +import consoleHelper +import bcolors +import clientPackets +import serverPackets +import glob +import fokabot +import exceptions + +def handle(userToken, packetData): + """ + Event called when someone sends a private message + + userToken -- request user token + packetData -- request data bytes + """ + + try: + # Get usertoken username + username = userToken.username + + # Private message packet + packetData = clientPackets.sendPrivateMessage(packetData) + + if packetData["to"] == "FokaBot": + # FokaBot command check + fokaMessage = fokabot.fokabotResponse(username, packetData["to"], packetData["message"]) + if fokaMessage != False: + userToken.enqueue(serverPackets.sendMessage("FokaBot", username, fokaMessage)) + consoleHelper.printColored("> FokaBot>{}: {}".format(packetData["to"], str(fokaMessage.encode("UTF-8"))), bcolors.PINK) + else: + # Send packet message to target if it exists + token = glob.tokens.getTokenFromUsername(packetData["to"]) + if token == None: + raise exceptions.tokenNotFoundException() + + # Send message to target + token.enqueue(serverPackets.sendMessage(username, packetData["to"], packetData["message"])) + + # Send away message to sender if needed + if token.awayMessage != "": + userToken.enqueue(serverPackets.sendMessage(packetData["to"], username, "This user is away: {}".format(token.awayMessage))) + + # Console output + consoleHelper.printColored("> {}>{}: {}".format(username, packetData["to"], packetData["message"]), bcolors.PINK) + except exceptions.tokenNotFoundException: + # Token not found, user disconnected + consoleHelper.printColored("[!] {} tried to send a message to {}, but their token couldn't be found".format(username, packetData["to"]), bcolors.RED) diff --git a/sendPublicMessageEvent.py b/sendPublicMessageEvent.py new file mode 100644 index 0000000..a20d6e3 --- /dev/null +++ b/sendPublicMessageEvent.py @@ -0,0 +1,108 @@ +import exceptions +import clientPackets +import glob +import fokabot +import consoleHelper +import bcolors +import serverPackets + +def handle(userToken, packetData): + """ + Event called when someone sends a public message + + userToken -- request user token + packetData -- request data bytes + """ + + try: + # Get uesrToken data + userID = userToken.userID + username = userToken.username + userRank = userToken.rank + + # Public chat packet + packetData = clientPackets.sendPublicMessage(packetData) + + # Receivers + who = [] + + # Check #spectator + if packetData["to"] == "#spectator": + # Spectator channel + # Send this packet to every spectator and host + if userToken.spectating == 0: + # We have sent to send a message to our #spectator channel + targetToken = userToken + who = targetToken.spectators[:] + # No need to remove us because we are the host so we are not in spectators list + else: + # We have sent a message to someone else's #spectator + targetToken = glob.tokens.getTokenFromUserID(userToken.spectating) + who = targetToken.spectators[:] + + # Remove us + if userID in who: + who.remove(userID) + + # Add host + who.append(targetToken.userID) + elif packetData["to"] == "#multiplayer": + # Multiplayer Channel + # Get match ID and match object + matchID = userToken.matchID + + # Make sure we are in a match + if matchID == -1: + return + + # Make sure the match exists + if matchID not in glob.matches.matches: + return + + # The match exists, get object + match = glob.matches.matches[matchID] + + # Create targets list + who = [] + for i in range(0,16): + uid = match.slots[i]["userID"] + if uid > -1 and uid != userID: + who.append(uid) + else: + # Standard channel + # Make sure the channel exists + if packetData["to"] not in glob.channels.channels: + raise exceptions.channelUnknownException + + # Make sure the channel is not in moderated mode + if glob.channels.channels[packetData["to"]].moderated == True and userRank <= 2: + raise exceptions.channelModeratedException + + # Make sure we have write permissions + if glob.channels.channels[packetData["to"]].publicWrite == False and userRank <= 2: + raise exceptions.channelNoPermissionsException + + # Send this packet to everyone in that channel except us + who = glob.channels.channels[packetData["to"]].getConnectedUsers()[:] + if userID in who: + who.remove(userID) + + + # Send packet to required users + glob.tokens.multipleEnqueue(serverPackets.sendMessage(username, packetData["to"], packetData["message"]), who, False) + + # Fokabot command check + fokaMessage = fokabot.fokabotResponse(username, packetData["to"], packetData["message"]) + if fokaMessage != False: + who.append(userID) + glob.tokens.multipleEnqueue(serverPackets.sendMessage("FokaBot", packetData["to"], fokaMessage), who, False) + consoleHelper.printColored("> FokaBot@{}: {}".format(packetData["to"], str(fokaMessage.encode("UTF-8"))), bcolors.PINK) + + # Console output + consoleHelper.printColored("> {}@{}: {}".format(username, packetData["to"], str(packetData["message"].encode("UTF-8"))), bcolors.PINK) + except exceptions.channelModeratedException: + consoleHelper.printColored("[!] {} tried to send a message to a channel that is in moderated mode ({})".format(username, packetData["to"]), bcolors.RED) + except exceptions.channelUnknownException: + consoleHelper.printColored("[!] {} tried to send a message to an unknown channel ({})".format(username, packetData["to"]), bcolors.RED) + except exceptions.channelNoPermissionsException: + consoleHelper.printColored("[!] {} tried to send a message to channel {}, but they have no write permissions".format(username, packetData["to"]), bcolors.RED) diff --git a/serverPackets.py b/serverPackets.py new file mode 100644 index 0000000..2cd6c2c --- /dev/null +++ b/serverPackets.py @@ -0,0 +1,273 @@ +""" Contains functions used to write specific server packets to byte streams """ +import packetHelper +import dataTypes +import userHelper +import glob +import userRanks +import packetIDs +import slotStatuses +import matchModModes +import random + +""" Login errors packets +(userID packets derivates) """ +def loginFailed(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-1, dataTypes.sInt32]]) + +def forceUpdate(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-2, dataTypes.sInt32]]) + +def loginBanned(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-3, dataTypes.sInt32]]) + +def loginError(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-5, dataTypes.sInt32]]) + +def needSupporter(): + return packetHelper.buildPacket(packetIDs.server_userID, [[-6, dataTypes.sInt32]]) + + +""" Login packets """ +def userID(uid): + return packetHelper.buildPacket(packetIDs.server_userID, [[uid, dataTypes.sInt32]]) + +def silenceEndTime(seconds): + return packetHelper.buildPacket(packetIDs.server_silenceEnd, [[seconds, dataTypes.uInt32]]) + +def protocolVersion(version = 19): + return packetHelper.buildPacket(packetIDs.server_protocolVersion, [[version, dataTypes.uInt32]]) + +def mainMenuIcon(icon): + return packetHelper.buildPacket(packetIDs.server_mainMenuIcon, [[icon, dataTypes.string]]) + +def userSupporterGMT(supporter, GMT): + result = 1 + if supporter == True: + result += 4 + if GMT == True: + result += 2 + return packetHelper.buildPacket(packetIDs.server_supporterGMT, [[result, dataTypes.uInt32]]) + +def friendList(userID): + friendsData = [] + + # Get friend IDs from db + friends = userHelper.getFriendList(userID) + + # Friends number + friendsData.append([len(friends), dataTypes.uInt16]) + + # Add all friend user IDs to friendsData + for i in friends: + friendsData.append([i, dataTypes.sInt32]) + + return packetHelper.buildPacket(packetIDs.server_friendsList, friendsData) + +def onlineUsers(): + onlineUsersData = [] + + users = glob.tokens.tokens + + # Users number + onlineUsersData.append([len(users), dataTypes.uInt16]) + + # Add all users user IDs to onlineUsersData + for _,value in users.items(): + onlineUsersData.append([value.userID, dataTypes.sInt32]) + + return packetHelper.buildPacket(packetIDs.server_userPresenceBundle, onlineUsersData) + + +""" Users packets """ +def userLogout(userID): + return packetHelper.buildPacket(packetIDs.server_userLogout, [[userID, dataTypes.sInt32], [0, dataTypes.byte]]) + +def userPanel(userID): + # Get user data + userToken = glob.tokens.getTokenFromUserID(userID) + username = userHelper.getUsername(userID) + timezone = 24 # TODO: Timezone + country = userToken.getCountry() + gameRank = userHelper.getGameRank(userID, userToken.gameMode) + latitude = userToken.getLatitude() + longitude = userToken.getLongitude() + + # Get username color according to rank + # Only admins and normal users are currently supported + rank = userHelper.getRankPrivileges(userID) + if username == "FokaBot": + userRank = userRanks.MOD + elif rank == 4: + userRank = userRanks.ADMIN + elif rank == 3: + userRank = userRank.MOD + elif rank == 2: + userRank = userRanks.SUPPORTER + else: + userRank = userRanks.NORMAL + + + return packetHelper.buildPacket(packetIDs.server_userPanel, + [ + [userID, dataTypes.sInt32], + [username, dataTypes.string], + [timezone, dataTypes.byte], + [country, dataTypes.byte], + [userRank, dataTypes.byte], + [longitude, dataTypes.ffloat], + [latitude, dataTypes.ffloat], + [gameRank, dataTypes.uInt32] + ]) + + +def userStats(userID): + # Get userID's token from tokens list + userToken = glob.tokens.getTokenFromUserID(userID) + + # Get stats from DB + # TODO: Caching system + rankedScore = userHelper.getRankedScore(userID, userToken.gameMode) + accuracy = userHelper.getAccuracy(userID, userToken.gameMode)/100 + playcount = userHelper.getPlaycount(userID, userToken.gameMode) + totalScore = userHelper.getTotalScore(userID, userToken.gameMode) + gameRank = userHelper.getGameRank(userID, userToken.gameMode) + pp = int(userHelper.getPP(userID, userToken.gameMode)) + + return packetHelper.buildPacket(packetIDs.server_userStats, + [ + [userID, dataTypes.uInt32], + [userToken.actionID, dataTypes.byte], + [userToken.actionText, dataTypes.string], + [userToken.actionMd5, dataTypes.string], + [userToken.actionMods, dataTypes.sInt32], + [userToken.gameMode, dataTypes.byte], + [0, dataTypes.sInt32], + [rankedScore, dataTypes.uInt64], + [accuracy, dataTypes.ffloat], + [playcount, dataTypes.uInt32], + [totalScore, dataTypes.uInt64], + [gameRank, dataTypes.uInt32], + [pp, dataTypes.uInt16] + ]) + + +""" Chat packets """ +def sendMessage(fro, to, message): + return packetHelper.buildPacket(packetIDs.server_sendMessage, [[fro, dataTypes.string], [message, dataTypes.string], [to, dataTypes.string], [userHelper.getID(fro), dataTypes.sInt32]]) + +def channelJoinSuccess(userID, chan): + return packetHelper.buildPacket(packetIDs.server_channelJoinSuccess, [[chan, dataTypes.string]]) + +def channelInfo(chan): + channel = glob.channels.channels[chan] + return packetHelper.buildPacket(packetIDs.server_channelInfo, [[chan, dataTypes.string], [channel.description, dataTypes.string], [channel.getConnectedUsersCount(), dataTypes.uInt16]]) + +def channelInfoEnd(): + return packetHelper.buildPacket(packetIDs.server_channelInfoEnd, [[0, dataTypes.uInt32]]) + +def channelKicked(chan): + return packetHelper.buildPacket(packetIDs.server_channelKicked, [[chan, dataTypes.string]]) + + +""" Spectator packets """ +def addSpectator(userID): + return packetHelper.buildPacket(packetIDs.server_spectatorJoined, [[userID, dataTypes.sInt32]]) + +def removeSpectator(userID): + return packetHelper.buildPacket(packetIDs.server_spectatorLeft, [[userID, dataTypes.sInt32]]) + +def spectatorFrames(data): + return packetHelper.buildPacket(packetIDs.server_spectateFrames, [[data, dataTypes.bbytes]]) + +def noSongSpectator(userID): + return packetHelper.buildPacket(packetIDs.server_spectatorCantSpectate, [[userID, dataTypes.sInt32]]) + + +""" Multiplayer Packets """ +def createMatch(matchID): + # Make sure the match exists + if matchID not in glob.matches.matches: + return None + + # Get match binary data and build packet + match = glob.matches.matches[matchID] + return packetHelper.buildPacket(packetIDs.server_newMatch, match.getMatchData()) + + +def updateMatch(matchID): + # Make sure the match exists + if matchID not in glob.matches.matches: + return None + + # Get match binary data and build packet + match = glob.matches.matches[matchID] + return packetHelper.buildPacket(packetIDs.server_updateMatch, match.getMatchData()) + + +def matchStart(matchID): + # Make sure the match exists + if matchID not in glob.matches.matches: + return None + + # Get match binary data and build packet + match = glob.matches.matches[matchID] + return packetHelper.buildPacket(packetIDs.server_matchStart, match.getMatchData()) + + +def disposeMatch(matchID): + return packetHelper.buildPacket(packetIDs.server_disposeMatch, [[matchID, dataTypes.uInt16]]) + +def matchJoinSuccess(matchID): + # Make sure the match exists + if matchID not in glob.matches.matches: + return None + + # Get match binary data and build packet + match = glob.matches.matches[matchID] + data = packetHelper.buildPacket(packetIDs.server_matchJoinSuccess, match.getMatchData()) + return data + +def matchJoinFail(): + return packetHelper.buildPacket(packetIDs.server_matchJoinFail) + +def changeMatchPassword(newPassword): + return packetHelper.buildPacket(packetIDs.server_matchChangePassword, [[newPassword, dataTypes.string]]) + +def allPlayersLoaded(): + return packetHelper.buildPacket(packetIDs.server_matchAllPlayersLoaded) + +def playerSkipped(userID): + return packetHelper.buildPacket(packetIDs.server_matchPlayerSkipped, [[userID, dataTypes.sInt32]]) + +def allPlayersSkipped(): + return packetHelper.buildPacket(packetIDs.server_matchSkip) + +def matchFrames(slotID, data): + return packetHelper.buildPacket(packetIDs.server_matchScoreUpdate, [[data[7:11], dataTypes.bbytes], [slotID, dataTypes.byte], [data[12:], dataTypes.bbytes]]) + +def matchComplete(): + return packetHelper.buildPacket(packetIDs.server_matchComplete) + +def playerFailed(slotID): + return packetHelper.buildPacket(packetIDs.server_matchPlayerFailed, [[slotID, dataTypes.uInt32]]) + +def matchTransferHost(): + return packetHelper.buildPacket(packetIDs.server_matchTransferHost) + +""" Other packets """ +def notification(message): + return packetHelper.buildPacket(packetIDs.server_notification, [[message, dataTypes.string]]) + +def jumpscare(message): + return packetHelper.buildPacket(packetIDs.server_jumpscare, [[message, dataTypes.string]]) + +def banchoRestart(msUntilReconnection): + return packetHelper.buildPacket(packetIDs.server_restart, [[msUntilReconnection, dataTypes.uInt32]]) + + +""" WIP Packets """ +def getAttention(): + return packetHelper.buildPacket(packetIDs.server_getAttention) + +def packet80(): + return packetHelper.buildPacket(packetIDs.server_topBotnet) diff --git a/setAwayMessageEvent.py b/setAwayMessageEvent.py new file mode 100644 index 0000000..76e8f2a --- /dev/null +++ b/setAwayMessageEvent.py @@ -0,0 +1,20 @@ +import clientPackets +import serverPackets + +def handle(userToken, packetData): + # get token data + username = userToken.username + + # Read packet data + packetData = clientPackets.setAwayMessage(packetData) + + # Set token away message + userToken.setAwayMessage(packetData["awayMessage"]) + + # Send private message from fokabot + if packetData["awayMessage"] == "": + fokaMessage = "Your away message has been reset" + else: + fokaMessage = "Your away message is now: {}".format(packetData["awayMessage"]) + userToken.enqueue(serverPackets.sendMessage("FokaBot", username, fokaMessage)) + print("{} has changed their away message to: {}".format(username, packetData["awayMessage"])) diff --git a/slotStatuses.py b/slotStatuses.py new file mode 100644 index 0000000..be36c78 --- /dev/null +++ b/slotStatuses.py @@ -0,0 +1,8 @@ +free = 1 +locked = 2 +notReady = 4 +ready = 8 +noMap = 16 +playing = 32 +occupied = 124 +playingQuit = 128 diff --git a/spectateFramesEvent.py b/spectateFramesEvent.py new file mode 100644 index 0000000..7522392 --- /dev/null +++ b/spectateFramesEvent.py @@ -0,0 +1,33 @@ +import glob +import consoleHelper +import bcolors +import serverPackets +import exceptions + +def handle(userToken, packetData): + # get token data + userID = userToken.userID + + # Send spectator frames to every spectator + consoleHelper.printColored("> {}'s spectators: {}".format(str(userID), str(userToken.spectators)), bcolors.BLUE) + for i in userToken.spectators: + # Send to every user but host + if i != userID: + try: + # Get spectator token object + spectatorToken = glob.tokens.getTokenFromUserID(i) + + # Make sure the token exists + if spectatorToken == None: + raise exceptions.stopSpectating + + # Make sure this user is spectating us + if spectatorToken.spectating != userID: + raise exceptions.stopSpectating + + # Everything seems fine, send spectator frames to this spectator + spectatorToken.enqueue(serverPackets.spectatorFrames(packetData[7:])) + except exceptions.stopSpectating: + # Remove this user from spectators + userToken.removeSpectator(i) + userToken.enqueue(serverPackets.removeSpectator(i)) diff --git a/startSpectatingEvent.py b/startSpectatingEvent.py new file mode 100644 index 0000000..6e8414d --- /dev/null +++ b/startSpectatingEvent.py @@ -0,0 +1,51 @@ +import consoleHelper +import bcolors +import clientPackets +import serverPackets +import exceptions +import glob +import userHelper + +def handle(userToken, packetData): + try: + # Get usertoken data + userID = userToken.userID + username = userToken.username + + # Start spectating packet + packetData = clientPackets.startSpectating(packetData) + + # Stop spectating old user if needed + if userToken.spectating != 0: + oldTargetToken = glob.tokens.getTokenFromUserID(userToken.spectating) + oldTargetToken.enqueue(serverPackets.removeSpectator(userID)) + userToken.stopSpectating() + + # Start spectating new user + userToken.startSpectating(packetData["userID"]) + + # Get host token + targetToken = glob.tokens.getTokenFromUserID(packetData["userID"]) + if targetToken == None: + raise exceptions.tokenNotFoundException + + # Add us to host's spectators + targetToken.addSpectator(userID) + + # Send spectator join packet to host + targetToken.enqueue(serverPackets.addSpectator(userID)) + + # Join #spectator channel + userToken.enqueue(serverPackets.channelJoinSuccess(userID, "#spectator")) + + if len(targetToken.spectators) == 1: + # First spectator, send #spectator join to host too + targetToken.enqueue(serverPackets.channelJoinSuccess(userID, "#spectator")) + + # Console output + consoleHelper.printColored("> {} are spectating {}".format(username, userHelper.getUsername(packetData["userID"])), bcolors.PINK) + consoleHelper.printColored("> {}'s spectators: {}".format(str(packetData["userID"]), str(targetToken.spectators)), bcolors.BLUE) + except exceptions.tokenNotFoundException: + # Stop spectating if token not found + consoleHelper.printColored("[!] Spectator start: token not found", bcolors.RED) + userToken.stopSpectating() diff --git a/stopSpectatingEvent.py b/stopSpectatingEvent.py new file mode 100644 index 0000000..acd9761 --- /dev/null +++ b/stopSpectatingEvent.py @@ -0,0 +1,31 @@ +import consoleHelper +import bcolors +import glob +import serverPackets +import exceptions + +def handle(userToken, _): + try: + # get user token data + userID = userToken.userID + username = userToken.username + + # Remove our userID from host's spectators + target = userToken.spectating + targetToken = glob.tokens.getTokenFromUserID(target) + if targetToken == None: + raise exceptions.tokenNotFoundException + targetToken.removeSpectator(userID) + + # Send the spectator left packet to host + targetToken.enqueue(serverPackets.removeSpectator(userID)) + + # Console output + # TODO: Move messages in stop spectating + consoleHelper.printColored("> {} are no longer spectating whoever they were spectating".format(username), bcolors.PINK) + consoleHelper.printColored("> {}'s spectators: {}".format(str(target), str(targetToken.spectators)), bcolors.BLUE) + except exceptions.tokenNotFoundException: + consoleHelper.printColored("[!] Spectator stop: token not found", bcolors.RED) + finally: + # Set our spectating user to 0 + userToken.stopSpectating() diff --git a/systemHelper.py b/systemHelper.py new file mode 100644 index 0000000..b381487 --- /dev/null +++ b/systemHelper.py @@ -0,0 +1,91 @@ +import glob +import serverPackets +import psutil +import os +import sys + +import consoleHelper +import bcolors +import threading +import signal + +def runningUnderUnix(): + """ + Get if the server is running under UNIX or NT + + return --- True if running under UNIX, otherwise False + """ + + return True if os.name == "posix" else False + + +def scheduleShutdown(sendRestartTime, restart, message = ""): + """ + Schedule a server shutdown/restart + + sendRestartTime -- time (seconds) to wait before sending server restart packets to every client + restart -- if True, server will restart. if False, server will shudown + message -- if set, send that message to every client to warn about the shutdown/restart + """ + + # Console output + consoleHelper.printColored("[!] Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+20), bcolors.PINK) + consoleHelper.printColored("[!] Sending server restart packets in {} seconds...".format(sendRestartTime), bcolors.PINK) + + # Send notification if set + if message != "": + glob.tokens.enqueueAll(serverPackets.notification(message)) + + # Schedule server restart packet + threading.Timer(sendRestartTime, glob.tokens.enqueueAll, [serverPackets.banchoRestart(50000)]).start() + glob.restarting = True + + # Restart/shutdown + if restart: + action = restartServer + else: + action = shutdownServer + + # Schedule actual server shutdown/restart 20 seconds after server restart packet, so everyone gets it + threading.Timer(sendRestartTime+20, action).start() + + +def restartServer(): + """Restart pep.py script""" + print("> Restarting pep.py...") + os.execv(sys.executable, [sys.executable] + sys.argv) + + +def shutdownServer(): + """Shutdown pep.py""" + print("> Shutting down pep.py...") + sig = signal.SIGKILL if runningUnderUnix() else signal.CTRL_C_EVENT + os.kill(os.getpid(), sig) + + +def getSystemInfo(): + """ + Get a dictionary with some system/server info + + return -- ["unix", "connectedUsers", "webServer", "cpuUsage", "totalMemory", "usedMemory", "loadAverage"] + """ + + data = {} + + # Get if server is running under unix/nt + data["unix"] = runningUnderUnix() + + # General stats + data["connectedUsers"] = len(glob.tokens.tokens) + data["webServer"] = glob.conf.config["server"]["server"] + data["cpuUsage"] = psutil.cpu_percent() + data["totalMemory"] = "{0:.2f}".format(psutil.virtual_memory()[0]/1074000000) + data["usedMemory"] = "{0:.2f}".format(psutil.virtual_memory()[3]/1074000000) + + # Unix only stats + if data["unix"] == True: + data["loadAverage"] = os.getloadavg() + else: + data["loadAverage"] = (0,0,0) + + return data diff --git a/tokenList.py b/tokenList.py new file mode 100644 index 0000000..39a6eac --- /dev/null +++ b/tokenList.py @@ -0,0 +1,165 @@ +import osuToken +import time +import threading +import logoutEvent + +class tokenList: + """ + List of connected osu tokens + + tokens -- dictionary. key: token string, value: token object + """ + + tokens = {} + + def addToken(self, __userID): + """ + Add a token object to tokens list + + __userID -- user id associated to that token + return -- token object + """ + + newToken = osuToken.token(__userID) + self.tokens[newToken.token] = newToken + return newToken + + def deleteToken(self, __token): + """ + Delete a token from token list if it exists + + __token -- token string + """ + + if __token in self.tokens: + self.tokens.pop(__token) + + + def getUserIDFromToken(self, __token): + """ + Get user ID from a token + + __token -- token to find + + return: false if not found, userID if found + """ + + # Make sure the token exists + if __token not in self.tokens: + return False + + # Get userID associated to that token + return self.tokens[__token].userID + + + def getTokenFromUserID(self, __userID): + """ + Get token from a user ID + + __userID -- user ID to find + return -- False if not found, token object if found + """ + + # Make sure the token exists + for _, value in self.tokens.items(): + if value.userID == __userID: + return value + + # Return none if not found + return None + + + def getTokenFromUsername(self, __username): + """ + Get token from a username + + __username -- username to find + return -- False if not found, token object if found + """ + + # lowercase + who = __username.lower() + + # Make sure the token exists + for _, value in self.tokens.items(): + if value.username.lower() == who: + return value + + # Return none if not found + return None + + + def deleteOldTokens(self, __userID): + """ + Delete old userID's tokens if found + + __userID -- tokens associated to this user will be deleted + """ + + # Delete older tokens + for key, value in self.tokens.items(): + if value.userID == __userID: + # Delete this token from the dictionary + self.tokens.pop(key) + + # break or items() function throws errors + break + + + def multipleEnqueue(self, __packet, __who, __but = False): + """ + Enqueue a packet to multiple users + + __packet -- packet bytes to enqueue + __who -- userIDs array + __but -- if True, enqueue to everyone but users in __who array + """ + + for _, value in self.tokens.items(): + shouldEnqueue = False + if value.userID in __who and not __but: + shouldEnqueue = True + elif value.userID not in __who and __but: + shouldEnqueue = True + + if shouldEnqueue: + value.enqueue(__packet) + + + + def enqueueAll(self, __packet): + """ + Enqueue packet(s) to every connected user + + __packet -- packet bytes to enqueue + """ + + for _, value in self.tokens.items(): + value.enqueue(__packet) + + def usersTimeoutCheckLoop(self, __timeoutTime = 100, __checkTime = 100): + """ + Deletes all timed out users. + If called once, will recall after __checkTime seconds and so on, forever + CALL THIS FUNCTION ONLY ONCE! + + __timeoutTime - seconds of inactivity required to disconnect someone (Default: 100) + __checkTime - seconds between loops (Default: 100) + """ + + timedOutTokens = [] # timed out users + timeoutLimit = time.time()-__timeoutTime + for key, value in self.tokens.items(): + # Check timeout (fokabot is ignored) + if value.pingTime < timeoutLimit and value.userID != 999: + # That user has timed out, add to disconnected tokens + # We can't delete it while iterating or items() throws an error + timedOutTokens.append(key) + + # Delete timed out users from self.tokens + # i is token string (dictionary key) + for i in timedOutTokens: + logoutEvent.handle(self.tokens[i], None) + + # Schedule a new check (endless loop) + threading.Timer(__checkTime, self.usersTimeoutCheckLoop, [__timeoutTime, __checkTime]).start() diff --git a/userHelper.py b/userHelper.py new file mode 100644 index 0000000..7d0aa41 --- /dev/null +++ b/userHelper.py @@ -0,0 +1,268 @@ +import passwordHelper +import gameModes +import glob + +def getID(username): + """ + Get username's user ID + + db -- database connection + username -- user + return -- user id or False + """ + + # Get user ID from db + userID = glob.db.fetch("SELECT id FROM users WHERE username = ?", [username]) + + # Make sure the query returned something + if userID == None: + return False + + # Return user ID + return userID["id"] + + +def checkLogin(userID, password): + """ + Check userID's login with specified password + + db -- database connection + userID -- user id + password -- plain md5 password + return -- True or False + """ + + # Get password data + passwordData = glob.db.fetch("SELECT password_md5, salt, password_version FROM users WHERE id = ?", [userID]) + + # Make sure the query returned something + if passwordData == None: + return False + + + # Return valid/invalid based on the password version. + if passwordData["password_version"] == 2: + return passwordHelper.checkNewPassword(password, passwordData["password_md5"]) + if passwordData["password_version"] == 1: + ok = passwordHelper.checkOldPassword(password, passwordData["salt"], passwordData["password_md5"]) + if not ok: return False + newpass = passwordHelper.genBcrypt(password) + glob.db.execute("UPDATE users SET password_md5=?, salt='', password_version='2' WHERE id = ?", [newpass, userID]) + + +def exists(userID): + """ + Check if userID exists + + userID -- user ID to check + return -- bool + """ + + result = glob.db.fetch("SELECT id FROM users WHERE id = ?", [userID]) + if result == None: + return False + else: + return True + +def getAllowed(userID): + """ + Get allowed status for userID + + db -- database connection + userID -- user ID + return -- allowed int + """ + + return glob.db.fetch("SELECT allowed FROM users WHERE id = ?", [userID])["allowed"] + + +def getRankPrivileges(userID): + """ + This returns rank **(PRIVILEGES)**, not game rank (like #1337) + If you want to get that rank, user getUserGameRank instead + """ + + return glob.db.fetch("SELECT rank FROM users WHERE id = ?", [userID])["rank"] + + +def getSilenceEnd(userID): + """ + Get userID's **ABSOLUTE** silence end UNIX time + Remember to subtract time.time() to get the actual silence time + + userID -- userID + return -- UNIX time + """ + + return glob.db.fetch("SELECT silence_end FROM users WHERE id = ?", [userID])["silence_end"] + + +def silence(userID, silenceEndTime, silenceReason): + """ + Set userID's **ABSOLUTE** silence end UNIX time + Remember to add time.time() to the silence length + + userID -- userID + silenceEndtime -- UNIX time when the silence ends + silenceReason -- Silence reason shown on website + """ + + glob.db.execute("UPDATE users SET silence_end = ?, silence_reason = ? WHERE id = ?", [silenceEndTime, silenceReason, userID]) + +def getRankedScore(userID, gameMode): + """ + Get userID's ranked score relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- ranked score + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT ranked_score_"+modeForDB+" FROM users_stats WHERE id = ?", [userID])["ranked_score_"+modeForDB] + + +def getTotalScore(userID, gameMode): + """ + Get userID's total score relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- total score + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT total_score_"+modeForDB+" FROM users_stats WHERE id = ?", [userID])["total_score_"+modeForDB] + + +def getAccuracy(userID, gameMode): + """ + Get userID's average accuracy relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- accuracy + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT avg_accuracy_"+modeForDB+" FROM users_stats WHERE id = ?", [userID])["avg_accuracy_"+modeForDB] + + +def getGameRank(userID, gameMode): + """ + Get userID's **in-game rank** (eg: #1337) relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- game rank + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + result = glob.db.fetch("SELECT position FROM leaderboard_"+modeForDB+" WHERE user = ?", [userID]) + if result == None: + return 0 + else: + return result["position"] + + +def getPlaycount(userID, gameMode): + """ + Get userID's playcount relative to gameMode + + userID -- userID + gameMode -- int value, see gameModes + return -- playcount + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT playcount_"+modeForDB+" FROM users_stats WHERE id = ?", [userID])["playcount_"+modeForDB] + + +def getUsername(userID): + """ + Get userID's username + + userID -- userID + return -- username + """ + + return glob.db.fetch("SELECT username FROM users WHERE id = ?", [userID])["username"] + + +def getFriendList(userID): + """ + Get userID's friendlist + + userID -- userID + return -- list with friends userIDs. [0] if no friends. + """ + + # Get friends from db + friends = glob.db.fetchAll("SELECT user2 FROM users_relationships WHERE user1 = ?", [userID]) + + if friends == None or len(friends) == 0: + # We have no friends, return 0 list + return [0] + else: + # Get only friends + friends = [i["user2"] for i in friends] + + # Return friend IDs + return friends + + +def addFriend(userID, friendID): + """ + Add friendID to userID's friend list + + userID -- user + friendID -- new friend + """ + + # Make sure we aren't adding us to our friends + if userID == friendID: + return + + # check user isn't already a friend of ours + if glob.db.fetch("SELECT id FROM users_relationships WHERE user1 = ? AND user2 = ?", [userID, friendID]) != None: + return + + # Set new value + glob.db.execute("INSERT INTO users_relationships (user1, user2) VALUES (?, ?)", [userID, friendID]) + + +def removeFriend(userID, friendID): + """ + Remove friendID from userID's friend list + + userID -- user + friendID -- old friend + """ + + # Delete user relationship. We don't need to check if the relationship was there, because who gives a shit, + # if they were not friends and they don't want to be anymore, be it. ¯\_(ツ)_/¯ + glob.db.execute("DELETE FROM users_relationships WHERE user1 = ? AND user2 = ?", [userID, friendID]) + + +def getCountry(userID): + """ + Get userID's country **(two letters)**. + Use countryHelper.getCountryID with what that function returns + to get osu! country ID relative to that user + + userID -- user + return -- country code (two letters) + """ + + return glob.db.fetch("SELECT country FROM users_stats WHERE id = ?", [userID])["country"] + +def getPP(userID, gameMode): + """ + Get userID's PP relative to gameMode + + userID -- user + return -- gameMode number + """ + + modeForDB = gameModes.getGameModeForDB(gameMode) + return glob.db.fetch("SELECT pp_{} FROM users_stats WHERE id = ?".format(modeForDB), [userID])["pp_{}".format(modeForDB)] diff --git a/userRanks.py b/userRanks.py new file mode 100644 index 0000000..923bede --- /dev/null +++ b/userRanks.py @@ -0,0 +1,9 @@ +"""Bancho user ranks""" +# TODO: Uppercase, maybe? +NORMAL = 0 +PLAYER = 1 +SUPPORTER = 4 +MOD = 6 +PEPPY = 8 +ADMIN = 16 +TOURNAMENTSTAFF = 32